Skip to content

Implement clipping masks #2644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub enum DocumentMessage {
SelectedLayersReorder {
relative_index_offset: isize,
},
ClipLayer {
id: NodeId,
},
SelectLayer {
id: NodeId,
ctrl: bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::SelectedLayersReorder { relative_index_offset } => {
self.selected_layers_reorder(relative_index_offset, responses);
}
DocumentMessage::ClipLayer { id } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);

responses.add(DocumentMessage::AddTransaction);
responses.add(GraphOperationMessage::ClipModeToggle { layer });
}
DocumentMessage::SelectLayer { id, ctrl, shift } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
blend_mode: BlendMode,
},
ClipModeToggle {
layer: LayerNodeIdentifier,
},
StrokeSet {
layer: LayerNodeIdentifier,
stroke: Stroke,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
Expand Down Expand Up @@ -51,6 +52,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.blend_mode_set(blend_mode);
}
}
GraphOperationMessage::ClipModeToggle { layer } => {
let clip_mode = get_clip_mode(layer, network_interface);
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.clip_mode_toggle(clip_mode);
}
}
GraphOperationMessage::StrokeSet { layer, stroke } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.stroke_set(stroke);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ impl<'a> ModifyInputsContext<'a> {
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
/// -----> Post node
/// | if insert_index == 0, return (Post node, Some(Layer1))
/// -> Layer1
/// -> Layer1
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
/// -> Layer2
/// -> Layer2
/// ↑
/// -> NonLayerNode
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
/// -> Layer3
/// -> Layer3
/// if insert_index == 3, return (Layer3, None)
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
Expand Down Expand Up @@ -333,20 +333,25 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
}

pub fn opacity_set(&mut self, opacity: f64) {
let Some(opacity_node_id) = self.existing_node_id("Opacity", true) else { return };
let input_connector = InputConnector::node(opacity_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
}

pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_mode_node_id) = self.existing_node_id("Blend Mode", true) else {
return;
};
let input_connector = InputConnector::node(blend_mode_node_id, 1);
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
}

pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
let clip = !clip_mode.map_or(false, |x| x);
let Some(clip_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(clip_node_id, 4);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
}

pub fn opacity_set(&mut self, opacity: f64) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 2);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
}

pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,16 +278,16 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
Some(color.to_linear_srgb())
}

/// Get the current blend mode of a layer from the closest Blend Mode node
/// Get the current blend mode of a layer from the closest Blending node
pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<BlendMode> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blend Mode")?;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
return None;
};
Some(*blend_mode)
}

/// Get the current opacity of a layer from the closest Opacity node.
/// Get the current opacity of a layer from the closest Blending node.
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
/// - Multiplied with additional opacity nodes earlier in the chain
/// - Set by an Opacity node with an exposed input value driven by another node
Expand All @@ -296,13 +296,21 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
///
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Opacity")?;
let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else {
return None;
};
Some(*opacity)
}

pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else {
return None;
};
Some(*clip)
}

pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
}
Expand Down
2 changes: 1 addition & 1 deletion editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ impl NodeGraphExecutor {
fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false);
let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false, false);

// Render SVG
render_object.render_svg(&mut render, &render_params);
Expand Down
2 changes: 1 addition & 1 deletion editor/src/node_graph_executor/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ impl NodeRuntime {
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY, true);

// Render the thumbnail from a `GraphicElement` into an SVG string
let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false);
let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false, false);
let mut render = SvgRender::new();
graphic_element.render_svg(&mut render, &render_params);

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/panels/Layers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,15 @@
// Select the layer only if the accel and/or shift keys are pressed
if (!oppositeAccel && !alt) selectLayer(listing, accel, shift);

if (alt) clipLayer(listing);

e.stopPropagation();
}

function clipLayer(listing: LayerListingInfo) {
editor.handle.clipLayer(listing.entry.id);
}

function selectLayer(listing: LayerListingInfo, accel: boolean, shift: boolean) {
// Don't select while we are entering text to rename the layer
if (listing.editingName) return;
Expand Down
7 changes: 7 additions & 0 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,13 @@ impl EditorHandle {
self.dispatch(message);
}

#[wasm_bindgen(js_name = clipLayer)]
pub fn clip_layer(&self, id: u64) {
let id = NodeId(id);
let message = DocumentMessage::ClipLayer { id };
self.dispatch(message);
}

/// Modify the layer selection based on the layer which is clicked while holding down the <kbd>Ctrl</kbd> and/or <kbd>Shift</kbd> modifier keys used for range selection behavior
#[wasm_bindgen(js_name = selectLayer)]
pub fn select_layer(&self, id: u64, ctrl: bool, shift: bool) {
Expand Down
8 changes: 7 additions & 1 deletion node-graph/gcore/src/graphic_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ pub mod renderer;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlphaBlending {
pub opacity: f32,
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
impl Default for AlphaBlending {
fn default() -> Self {
Expand All @@ -26,14 +28,18 @@ impl Default for AlphaBlending {
impl core::hash::Hash for AlphaBlending {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
fill: 1.,
blend_mode: BlendMode::Normal,
clip: false,
}
}
}
Expand Down
63 changes: 53 additions & 10 deletions node-graph/gcore/src/graphic_element/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,19 @@ pub struct RenderParams {
pub hide_artboards: bool,
/// Are we exporting? Causes the text above an artboard to be hidden.
pub for_export: bool,
/// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha.
pub for_mask: bool,
}

impl RenderParams {
pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool, for_mask: bool) -> Self {
Self {
view_mode,
culling_bounds,
thumbnail,
hide_artboards,
for_export,
for_mask,
}
}
}
Expand Down Expand Up @@ -299,7 +302,9 @@ pub trait GraphicElementRendered {

impl GraphicElementRendered for GraphicGroupTable {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for instance in self.instance_ref_iter() {
let mut iter = self.instance_ref_iter().peekable();
let mut uuid_state = None;
while let Some(instance) = iter.next() {
render.parent_tag(
"g",
|attributes| {
Expand All @@ -308,13 +313,46 @@ impl GraphicElementRendered for GraphicGroupTable {
attributes.push("transform", matrix);
}

if instance.alpha_blending.opacity < 1. {
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
let opacity = instance.alpha_blending.opacity * factor;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}

if instance.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", instance.alpha_blending.blend_mode.render());
}

let next_clips = iter.peek().map_or(false, |next_instance| {
let instance = next_instance.instance;
instance.as_vector_data().is_some_and(|data| data.instance_ref_iter().all(|instance| instance.alpha_blending.clip))
|| instance.as_group().is_some_and(|data| data.instance_ref_iter().all(|instance| instance.alpha_blending.clip))
|| instance.as_raster().is_some_and(|data| match data {
RasterFrame::ImageFrame(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
RasterFrame::TextureFrame(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
})
});

if next_clips && uuid_state.is_none() {
let uuid = generate_uuid();
let id = format!("mask-{}", uuid);
uuid_state = Some(uuid);
let mut svg = SvgRender::new();
let render_params = RenderParams { for_mask: true, ..*render_params };
instance.instance.render_svg(&mut svg, &render_params);

write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap();
write!(&mut attributes.0.svg_defs, r##"<mask id="{id}" mask-type="alpha">{}</mask>"##, svg.svg.to_svg_string()).unwrap();
} else if let Some(uuid) = uuid_state {
if !next_clips {
uuid_state = None;
}

let id = format!("mask-{}", uuid);
let selector = format!("url(#{id})");

attributes.push("mask", selector);
}
},
|render| {
instance.instance.render_svg(render, render_params);
Expand Down Expand Up @@ -452,11 +490,13 @@ impl GraphicElementRendered for VectorDataTable {
let fill_and_stroke = instance
.instance
.style
.render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds);
.render(defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds, render_params);
attributes.push_val(fill_and_stroke);

if instance.alpha_blending.opacity < 1. {
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
let opacity = instance.alpha_blending.opacity * factor;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}

if instance.alpha_blending.blend_mode != BlendMode::default() {
Expand Down Expand Up @@ -843,7 +883,7 @@ impl GraphicElementRendered for ArtboardGroupTable {
}

impl GraphicElementRendered for ImageFrameTable<Color> {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for instance in self.instance_ref_iter() {
let transform = *instance.transform * render.transform;

Expand All @@ -869,8 +909,10 @@ impl GraphicElementRendered for ImageFrameTable<Color> {
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if instance.alpha_blending.opacity < 1. {
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
let opacity = instance.alpha_blending.opacity * factor;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if instance.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", instance.alpha_blending.blend_mode.render());
Expand Down Expand Up @@ -1136,6 +1178,7 @@ impl GraphicElementRendered for Vec<Color> {
attributes.push("x", (index * 120).to_string());
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
debug!("{}", color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
Expand Down
Loading
Loading