mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Add a checkered background to transparent artboards and the infinite canvas (#4022)
* Add checkered transparency rendering to infinite canvas and artboards * Enable artboard clipping by default * Make new infinite canvas documents begin with a white background layer * Remove the export dialog's transparency option now that it's redundant * Make exporting transparent JPGs use white not black * Code review
This commit is contained in:
@@ -6,7 +6,6 @@ use crate::messages::prelude::*;
|
||||
pub enum ExportDialogMessage {
|
||||
FileType { file_type: FileType },
|
||||
ScaleFactor { factor: f64 },
|
||||
TransparentBackground { transparent: bool },
|
||||
ExportBounds { bounds: ExportBounds },
|
||||
|
||||
Submit,
|
||||
|
||||
@@ -14,7 +14,6 @@ pub struct ExportDialogMessageHandler {
|
||||
pub file_type: FileType,
|
||||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
pub transparent_background: bool,
|
||||
pub artboards: HashMap<LayerNodeIdentifier, String>,
|
||||
pub has_selection: bool,
|
||||
}
|
||||
@@ -25,7 +24,6 @@ impl Default for ExportDialogMessageHandler {
|
||||
file_type: Default::default(),
|
||||
scale_factor: 1.,
|
||||
bounds: Default::default(),
|
||||
transparent_background: false,
|
||||
artboards: Default::default(),
|
||||
has_selection: false,
|
||||
}
|
||||
@@ -40,11 +38,17 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
||||
match message {
|
||||
ExportDialogMessage::FileType { file_type } => self.file_type = file_type,
|
||||
ExportDialogMessage::ScaleFactor { factor } => self.scale_factor = factor,
|
||||
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
|
||||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||
|
||||
ExportDialogMessage::Submit => {
|
||||
let artboard_name = match self.bounds {
|
||||
// Fall back to "All Artwork" if "Selection" was chosen but nothing is currently selected
|
||||
let bounds = if !self.has_selection && self.bounds == ExportBounds::Selection {
|
||||
ExportBounds::AllArtwork
|
||||
} else {
|
||||
self.bounds
|
||||
};
|
||||
|
||||
let artboard_name = match bounds {
|
||||
ExportBounds::Artboard(layer) => self.artboards.get(&layer).cloned(),
|
||||
_ => None,
|
||||
};
|
||||
@@ -52,8 +56,7 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
|
||||
bounds,
|
||||
artboard_name,
|
||||
artboard_count: self.artboards.len(),
|
||||
})
|
||||
@@ -127,6 +130,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
let artboards = self.artboards.iter().map(|(&layer, name)| (ExportBounds::Artboard(layer), name.to_string(), false)).collect();
|
||||
let choices = [standard_bounds, artboards];
|
||||
|
||||
// Fall back to "All Artwork" if "Selection" was chosen but nothing is currently selected
|
||||
let current_bounds = if !self.has_selection && self.bounds == ExportBounds::Selection {
|
||||
ExportBounds::AllArtwork
|
||||
} else {
|
||||
@@ -159,22 +163,6 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_instance(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let transparent_background = vec![
|
||||
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(self.transparent_background)
|
||||
.disabled(self.file_type == FileType::Jpg)
|
||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![
|
||||
LayoutGroup::row(export_type),
|
||||
LayoutGroup::row(resolution),
|
||||
LayoutGroup::row(export_area),
|
||||
LayoutGroup::row(transparent_background),
|
||||
])
|
||||
Layout(vec![LayoutGroup::row(export_type), LayoutGroup::row(resolution), LayoutGroup::row(export_area)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{IVec2, UVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
|
||||
/// A dialog to allow users to set some initial options about a new document.
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
@@ -22,26 +24,40 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
|
||||
NewDocumentDialogMessage::Submit => {
|
||||
responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() });
|
||||
|
||||
let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0;
|
||||
if create_artboard {
|
||||
if self.infinite {
|
||||
// Infinite canvas: add a locked white background layer
|
||||
let node_id = NodeId::new();
|
||||
responses.add(GraphOperationMessage::NewColorFillLayer {
|
||||
node_id,
|
||||
color: Color::WHITE,
|
||||
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||
insert_index: 0,
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetDisplayNameImpl {
|
||||
node_id,
|
||||
alias: "Background".to_string(),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetLocked { node_id, locked: true });
|
||||
} else if self.dimensions.x > 0 && self.dimensions.y > 0 {
|
||||
// Finite canvas: create an artboard with the specified dimensions
|
||||
responses.add(GraphOperationMessage::NewArtboard {
|
||||
id: NodeId::new(),
|
||||
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
|
||||
});
|
||||
responses.add(NavigationMessage::CanvasPan { delta: self.dimensions.as_dvec2() });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
responses.add(ViewportMessage::RepropagateUpdate);
|
||||
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![
|
||||
DocumentMessage::ZoomCanvasToFitAll.into(),
|
||||
DocumentMessage::DeselectAllLayers.into(),
|
||||
PortfolioMessage::AutoSaveActiveDocument.into(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(ViewportMessage::RepropagateUpdate);
|
||||
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![
|
||||
DocumentMessage::ZoomCanvasToFitAll.into(),
|
||||
DocumentMessage::DeselectAllLayers.into(),
|
||||
PortfolioMessage::AutoSaveActiveDocument.into(),
|
||||
],
|
||||
});
|
||||
|
||||
responses.add(DocumentMessage::MarkAsSaved);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1400,8 +1400,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
let node_layer_id = LayerNodeIdentifier::new_unchecked(node_id);
|
||||
let new_artboard_node = document_node_definitions::resolve_network_node_type("Artboard")
|
||||
.expect("Failed to create artboard node")
|
||||
// Enable clipping by default (input index 5) so imported content is masked to the artboard bounds
|
||||
.node_template_input_override([None, None, None, None, None, Some(NodeInput::value(TaggedValue::Bool(true), false))]);
|
||||
.default_node_template();
|
||||
responses.add(NodeGraphMessage::InsertNode {
|
||||
node_id,
|
||||
node_template: Box::new(new_artboard_node),
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Artboard;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{CPU, Raster};
|
||||
@@ -14,6 +13,7 @@ use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::PointId;
|
||||
use graphene_std::vector::VectorModificationType;
|
||||
use graphene_std::vector::style::{Fill, Stroke};
|
||||
use graphene_std::{Artboard, Color};
|
||||
|
||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
@@ -97,6 +97,12 @@ pub enum GraphOperationMessage {
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
NewColorFillLayer {
|
||||
node_id: NodeId,
|
||||
color: Color,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
NewVectorLayer {
|
||||
id: NodeId,
|
||||
subpaths: Vec<Subpath<PointId>>,
|
||||
|
||||
@@ -274,6 +274,13 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||
let layer = modify_inputs.create_layer(node_id);
|
||||
modify_inputs.insert_color_value(color, layer);
|
||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
|
||||
@@ -7,7 +7,6 @@ use glam::{DAffine2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::Artboard;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{CPU, Raster};
|
||||
@@ -17,7 +16,7 @@ use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::style::{Fill, Stroke};
|
||||
use graphene_std::vector::{PointId, VectorModificationType};
|
||||
use graphene_std::{Graphic, NodeInputDecleration};
|
||||
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransformIn {
|
||||
@@ -289,6 +288,19 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
||||
}
|
||||
|
||||
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
|
||||
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||
.expect("Color Value node does not exist")
|
||||
.node_template_input_override([
|
||||
Some(NodeInput::value(TaggedValue::None, false)),
|
||||
Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(color)), false)),
|
||||
]);
|
||||
|
||||
let color_value_id = NodeId::new();
|
||||
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&color_value_id, layer, &[], self.import);
|
||||
}
|
||||
|
||||
pub fn insert_image_data(&mut self, image_frame: Table<Raster<CPU>>, layer: LayerNodeIdentifier) {
|
||||
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
|
||||
let image = resolve_proto_node_type(graphene_std::raster_nodes::std_nodes::image_value::IDENTIFIER)
|
||||
|
||||
@@ -380,7 +380,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
|
||||
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
|
||||
NodeInput::value(TaggedValue::Color(Table::new_from_element(Color::WHITE)), false),
|
||||
NodeInput::value(TaggedValue::Bool(false), false),
|
||||
NodeInput::value(TaggedValue::Bool(true), false),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -168,7 +168,6 @@ pub enum PortfolioMessage {
|
||||
file_type: FileType,
|
||||
scale_factor: f64,
|
||||
bounds: ExportBounds,
|
||||
transparent_background: bool,
|
||||
artboard_name: Option<String>,
|
||||
artboard_count: usize,
|
||||
},
|
||||
|
||||
@@ -1382,7 +1382,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
file_type,
|
||||
scale_factor,
|
||||
bounds,
|
||||
transparent_background,
|
||||
artboard_name,
|
||||
artboard_count,
|
||||
} => {
|
||||
@@ -1392,7 +1391,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
file_type,
|
||||
scale_factor,
|
||||
bounds,
|
||||
transparent_background,
|
||||
artboard_name,
|
||||
artboard_count,
|
||||
..Default::default()
|
||||
|
||||
@@ -395,7 +395,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||
location: start.min(end).round().as_ivec2(),
|
||||
dimensions: (start.round() - end.round()).abs().as_ivec2(),
|
||||
background: graphene_std::Color::WHITE,
|
||||
clip: false,
|
||||
clip: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,7 +153,6 @@ impl NodeGraphExecutor {
|
||||
pointer,
|
||||
export_format: graphene_std::application_io::ExportFormat::Raster,
|
||||
render_mode: document.render_mode,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_eyedropper: false,
|
||||
};
|
||||
@@ -218,7 +217,6 @@ impl NodeGraphExecutor {
|
||||
pointer,
|
||||
export_format: graphene_std::application_io::ExportFormat::Raster,
|
||||
render_mode,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_eyedropper: true,
|
||||
};
|
||||
@@ -241,10 +239,10 @@ impl NodeGraphExecutor {
|
||||
graphene_std::application_io::ExportFormat::Raster
|
||||
};
|
||||
|
||||
// Calculate the bounding box of the region to be exported
|
||||
// Calculate the bounding box of the region to be exported (artboard bounds always contribute)
|
||||
let bounds = match export_config.bounds {
|
||||
ExportBounds::AllArtwork => document.network_interface.document_bounds_document_space(!export_config.transparent_background),
|
||||
ExportBounds::Selection => document.network_interface.selected_bounds_document_space(!export_config.transparent_background, &[]),
|
||||
ExportBounds::AllArtwork => document.network_interface.document_bounds_document_space(true),
|
||||
ExportBounds::Selection => document.network_interface.selected_bounds_document_space(true, &[]),
|
||||
ExportBounds::Artboard(id) => document.metadata().bounding_box_document(id),
|
||||
}
|
||||
.ok_or_else(|| "No bounding box".to_string())?;
|
||||
@@ -266,7 +264,6 @@ impl NodeGraphExecutor {
|
||||
pointer: DVec2::ZERO,
|
||||
export_format,
|
||||
render_mode: document.render_mode,
|
||||
hide_artboards: export_config.transparent_background,
|
||||
for_export: true,
|
||||
for_eyedropper: false,
|
||||
};
|
||||
@@ -481,7 +478,7 @@ impl NodeGraphExecutor {
|
||||
use image::buffer::ConvertBuffer;
|
||||
use image::{ImageFormat, RgbImage, RgbaImage};
|
||||
|
||||
let Some(image) = RgbaImage::from_raw(width, height, data) else {
|
||||
let Some(mut image) = RgbaImage::from_raw(width, height, data) else {
|
||||
return Err("Failed to create image buffer for export".to_string());
|
||||
};
|
||||
|
||||
@@ -496,6 +493,14 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
}
|
||||
FileType::Jpg => {
|
||||
// Composite onto a white background since JPG doesn't support transparency
|
||||
for pixel in image.pixels_mut() {
|
||||
let [r, g, b, a] = pixel.0;
|
||||
let alpha = a as f32 / 255.;
|
||||
let blend = |channel: u8| (channel as f32 * alpha + 255. * (1. - alpha)).round() as u8;
|
||||
*pixel = image::Rgba([blend(r), blend(g), blend(b), 255]);
|
||||
}
|
||||
|
||||
let image: RgbImage = image.convert();
|
||||
let result = image.write_to(&mut cursor, ImageFormat::Jpeg);
|
||||
if let Err(err) = result {
|
||||
|
||||
@@ -86,7 +86,6 @@ pub struct ExportConfig {
|
||||
pub file_type: FileType,
|
||||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
pub transparent_background: bool,
|
||||
pub size: UVec2,
|
||||
pub artboard_name: Option<String>,
|
||||
pub artboard_count: usize,
|
||||
|
||||
Reference in New Issue
Block a user