From 5558deba5ebc3488ffd930cfd9b500d2b271abf0 Mon Sep 17 00:00:00 2001 From: Orson Peters Date: Fri, 2 Jun 2023 21:59:55 +0200 Subject: [PATCH] Brush blend modes and erase/restore (#1261) * Made blit node numerically stable. * Added blend mode parameter to brush strokes. * Fixed difference blend mode. * Added erase/restore blend modes. * Added blend mode and draw mode widgets. * Added comment explaining the ImageFrame.transform. * Initial blit/blend version. * Working version of erase/restore. * Improved inlining for blend functions. * Dsiable the blend mode selector in erase/draw mode. * Fixed incorrect bounds calculation. * Use factor instead of percentage for opacity * Rearrange options bar widgets * Tidy up blend modes * Code review --------- Co-authored-by: Keavon Chambers --- document-legacy/src/layers/blend_mode.rs | 36 +++- .../node_properties.rs | 15 +- .../messages/tool/tool_messages/brush_tool.rs | 85 +++++++++- node-graph/gcore/src/raster.rs | 1 + node-graph/gcore/src/raster/adjustments.rs | 155 +++++++----------- node-graph/gcore/src/raster/bbox.rs | 9 + node-graph/gcore/src/raster/color.rs | 24 +++ node-graph/gcore/src/raster/image.rs | 14 ++ node-graph/gcore/src/vector/brush_stroke.rs | 13 +- node-graph/graph-craft/src/document/value.rs | 4 +- node-graph/gstd/src/brush.rs | 103 ++++++++++-- node-graph/gstd/src/raster.rs | 13 +- .../interpreted-executor/src/node_registry.rs | 78 ++++++--- 13 files changed, 397 insertions(+), 153 deletions(-) diff --git a/document-legacy/src/layers/blend_mode.rs b/document-legacy/src/layers/blend_mode.rs index ee50a2ca..8257ea59 100644 --- a/document-legacy/src/layers/blend_mode.rs +++ b/document-legacy/src/layers/blend_mode.rs @@ -5,19 +5,19 @@ use std::fmt; /// See the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#examples) for examples. #[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum BlendMode { - // Basic group + // Normal group Normal, // Not supported by SVG, but we should someday support: Dissolve // Darken group - Multiply, Darken, + Multiply, ColorBurn, // Not supported by SVG, but we should someday support: Linear Burn, Darker Color // Lighten group - Screen, Lighten, + Screen, ColorDodge, // Not supported by SVG, but we should someday support: Linear Dodge (Add), Lighter Color @@ -42,23 +42,29 @@ pub enum BlendMode { impl fmt::Display for BlendMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + // Normal group BlendMode::Normal => write!(f, "Normal"), - BlendMode::Multiply => write!(f, "Multiply"), + // Darken group BlendMode::Darken => write!(f, "Darken"), + BlendMode::Multiply => write!(f, "Multiply"), BlendMode::ColorBurn => write!(f, "Color Burn"), - BlendMode::Screen => write!(f, "Screen"), + // Lighten group BlendMode::Lighten => write!(f, "Lighten"), + BlendMode::Screen => write!(f, "Screen"), BlendMode::ColorDodge => write!(f, "Color Dodge"), + // Contrast group BlendMode::Overlay => write!(f, "Overlay"), BlendMode::SoftLight => write!(f, "Soft Light"), BlendMode::HardLight => write!(f, "Hard Light"), + // Inversion group BlendMode::Difference => write!(f, "Difference"), BlendMode::Exclusion => write!(f, "Exclusion"), + // Component group BlendMode::Hue => write!(f, "Hue"), BlendMode::Saturation => write!(f, "Saturation"), BlendMode::Color => write!(f, "Color"), @@ -72,18 +78,24 @@ impl BlendMode { /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values) pub fn to_svg_style_name(&self) -> &str { match self { + // Normal group BlendMode::Normal => "normal", - BlendMode::Multiply => "multiply", + // Darken group BlendMode::Darken => "darken", + BlendMode::Multiply => "multiply", BlendMode::ColorBurn => "color-burn", - BlendMode::Screen => "screen", + // Lighten group BlendMode::Lighten => "lighten", + BlendMode::Screen => "screen", BlendMode::ColorDodge => "color-dodge", + // Contrast group BlendMode::Overlay => "overlay", BlendMode::SoftLight => "soft-light", BlendMode::HardLight => "hard-light", + // Inversion group BlendMode::Difference => "difference", BlendMode::Exclusion => "exclusion", + // Component group BlendMode::Hue => "hue", BlendMode::Saturation => "saturation", BlendMode::Color => "color", @@ -94,11 +106,17 @@ impl BlendMode { /// List of all the blend modes in their conventional ordering and grouping. pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] { [ + // Normal group &[BlendMode::Normal], - &[BlendMode::Multiply, BlendMode::Darken, BlendMode::ColorBurn], - &[BlendMode::Screen, BlendMode::Lighten, BlendMode::ColorDodge], + // Darken group + &[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn], + // Lighten group + &[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge], + // Contrast group &[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight], + // Inversion group &[BlendMode::Difference, BlendMode::Exclusion], + // Component group &[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity], ] } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index fcc219b6..6454112f 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -309,12 +309,15 @@ fn blend_mode(document_node: &DocumentNode, node_id: u64, index: usize, name: &s exposed: false, } = &document_node.inputs[index] { - let calculation_modes = BlendMode::list(); - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push(DropdownEntryData::new(method.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(method), node_id, index))); - } - let entries = vec![entries]; + let entries = BlendMode::list() + .iter() + .map(|category| { + category + .iter() + .map(|mode| DropdownEntryData::new(mode.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(*mode), node_id, index))) + .collect() + }) + .collect(); widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder()]); } diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 24765552..138b988e 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -15,13 +15,51 @@ use document_legacy::layers::layer_layer::CachedOutputData; use document_legacy::LayerId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput, NodeNetwork}; -use graphene_core::raster::ImageFrame; +use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle}; use graphene_core::Color; use glam::DAffine2; use serde::{Deserialize, Serialize}; +const EXPOSED_BLEND_MODES: &'static [&'static [BlendMode]] = { + use BlendMode::*; + &[ + // Basic group + &[Normal], + // Darken group + &[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor], + // Lighten group + &[Lighten, Screen, ColorDodge, LinearDodge, LighterColor], + // Contrast group + &[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix], + // Inversion group + &[Difference, Exclusion, Subtract, Divide], + // Component group + &[Hue, Saturation, Color, Luminosity], + ] +}; + +fn blend_mode_dropdown_idx(target_blend_mode: BlendMode) -> Option { + let mut i = 0; + for group in EXPOSED_BLEND_MODES { + for &blend_mode in group.iter() { + if blend_mode == target_blend_mode { + return Some(i); + } + i += 1; + } + } + None +} + +#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub enum DrawMode { + Draw = 0, + Erase, + Restore, +} + #[derive(Default)] pub struct BrushTool { fsm_state: BrushToolFsmState, @@ -35,6 +73,8 @@ pub struct BrushOptions { flow: f64, spacing: f64, color: ToolColorOptions, + blend_mode: BlendMode, + draw_mode: DrawMode, } impl Default for BrushOptions { @@ -45,6 +85,8 @@ impl Default for BrushOptions { flow: 100., spacing: 20., color: ToolColorOptions::default(), + blend_mode: BlendMode::Normal, + draw_mode: DrawMode::Draw, } } } @@ -69,10 +111,12 @@ pub enum BrushToolMessage { #[remain::sorted] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum BrushToolMessageOptionsUpdate { + BlendMode(BlendMode), ChangeDiameter(f64), Color(Option), ColorType(ToolColorType), Diameter(f64), + DrawMode(DrawMode), Flow(f64), Hardness(f64), Spacing(f64), @@ -135,6 +179,14 @@ impl PropertyHolder for BrushTool { widgets.push(WidgetHolder::section_separator()); + let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore] + .into_iter() + .map(|draw_mode| RadioEntryData::new(format!("{draw_mode:?}")).on_update(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::DrawMode(draw_mode)).into())) + .collect(); + widgets.push(RadioInput::new(draw_mode_entries).selected_index(self.options.draw_mode as u32).widget_holder()); + + widgets.push(WidgetHolder::section_separator()); + widgets.append(&mut self.options.color.create_widgets( "Color", false, @@ -143,6 +195,29 @@ impl PropertyHolder for BrushTool { WidgetCallback::new(|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into()), )); + widgets.push(WidgetHolder::related_separator()); + + let blend_mode_entries: Vec> = EXPOSED_BLEND_MODES + .iter() + .map(|group| { + group + .iter() + .map(|blend_mode| { + DropdownEntryData::new(format!("{blend_mode}")) + .value(format!("{blend_mode:?}")) + .on_update(|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::BlendMode(*blend_mode)).into()) + }) + .collect() + }) + .collect(); + widgets.push( + DropdownInput::new(blend_mode_entries) + .selected_index(blend_mode_dropdown_idx(self.options.blend_mode)) + .tooltip("The blend mode used with the background when performing a brush stroke. Only used in draw mode.") + .disabled(self.options.draw_mode != DrawMode::Draw) + .widget_holder(), + ); + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -151,6 +226,7 @@ impl<'a> MessageHandler> for BrushTo fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message { match action { + BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode, BrushToolMessageOptionsUpdate::ChangeDiameter(change) => { let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5; if needs_rounding && change > 0. { @@ -164,6 +240,7 @@ impl<'a> MessageHandler> for BrushTo self.register_properties(responses, LayoutTarget::ToolOptions); } BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter, + BrushToolMessageOptionsUpdate::DrawMode(draw_mode) => self.options.draw_mode = draw_mode, BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness, BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow, BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing, @@ -297,6 +374,11 @@ impl Fsm for BrushToolFsmState { .max((tool_data.transform.matrix2 * glam::DVec2::Y).length()); // Start a new stroke with a single sample + let blend_mode = match tool_options.draw_mode { + DrawMode::Draw => tool_options.blend_mode, + DrawMode::Erase => BlendMode::Erase, + DrawMode::Restore => BlendMode::Restore, + }; tool_data.strokes.push(BrushStroke { trace: vec![BrushInputSample { position: layer_position }], style: BrushStyle { @@ -305,6 +387,7 @@ impl Fsm for BrushToolFsmState { hardness: tool_options.hardness, flow: tool_options.flow, spacing: tool_options.spacing, + blend_mode, }, }); diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 13c649c5..a3511c38 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -199,6 +199,7 @@ pub trait Sample { impl<'i, T: Sample> Sample for &'i T { type Pixel = T::Pixel; + #[inline(always)] fn sample(&self, pos: DVec2, area: DVec2) -> Option { (**self).sample(pos, area) } diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 1d17115a..87ee6ba7 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -46,37 +46,28 @@ impl core::fmt::Display for LuminanceCalculation { } impl BlendMode { - pub fn list() -> [BlendMode; 29] { + pub fn list() -> [&'static [BlendMode]; 6] { [ - BlendMode::Normal, - BlendMode::Multiply, - BlendMode::Darken, - BlendMode::ColorBurn, - BlendMode::LinearBurn, - BlendMode::DarkerColor, - BlendMode::Screen, - BlendMode::Lighten, - BlendMode::ColorDodge, - BlendMode::LinearDodge, - BlendMode::LighterColor, - BlendMode::Overlay, - BlendMode::SoftLight, - BlendMode::HardLight, - BlendMode::VividLight, - BlendMode::LinearLight, - BlendMode::PinLight, - BlendMode::HardMix, - BlendMode::Difference, - BlendMode::Exclusion, - BlendMode::Subtract, - BlendMode::Divide, - BlendMode::Hue, - BlendMode::Saturation, - BlendMode::Color, - BlendMode::Luminosity, - BlendMode::InsertRed, - BlendMode::InsertGreen, - BlendMode::InsertBlue, + // Normal group + &[BlendMode::Normal], + // Darken group + &[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor], + // Lighten group + &[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge, BlendMode::LinearDodge, BlendMode::LighterColor], + // Contrast group + &[ + BlendMode::Overlay, + BlendMode::SoftLight, + BlendMode::HardLight, + BlendMode::VividLight, + BlendMode::LinearLight, + BlendMode::PinLight, + BlendMode::HardMix, + ], + // Inversion group + &[BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract, BlendMode::Divide], + // Component group + &[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity], ] } } @@ -84,7 +75,7 @@ impl BlendMode { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] -#[repr(i32)] // TODO: Enable Int8 capability for SPRIV so that we don't need this? +#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this? pub enum BlendMode { #[default] // Basic group @@ -126,29 +117,30 @@ pub enum BlendMode { Color, Luminosity, - // Other Stuff - InsertRed, - InsertGreen, - InsertBlue, + // Other stuff + Erase, + Restore, + MultiplyAlpha, } impl core::fmt::Display for BlendMode { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { + // Normal group BlendMode::Normal => write!(f, "Normal"), - - BlendMode::Multiply => write!(f, "Multiply"), + // Darken group BlendMode::Darken => write!(f, "Darken"), + BlendMode::Multiply => write!(f, "Multiply"), BlendMode::ColorBurn => write!(f, "Color Burn"), BlendMode::LinearBurn => write!(f, "Linear Burn"), BlendMode::DarkerColor => write!(f, "Darker Color"), - - BlendMode::Screen => write!(f, "Screen"), + // Lighten group BlendMode::Lighten => write!(f, "Lighten"), + BlendMode::Screen => write!(f, "Screen"), BlendMode::ColorDodge => write!(f, "Color Dodge"), BlendMode::LinearDodge => write!(f, "Linear Dodge"), BlendMode::LighterColor => write!(f, "Lighter Color"), - + // Contrast group BlendMode::Overlay => write!(f, "Overlay"), BlendMode::SoftLight => write!(f, "Soft Light"), BlendMode::HardLight => write!(f, "Hard Light"), @@ -156,64 +148,24 @@ impl core::fmt::Display for BlendMode { BlendMode::LinearLight => write!(f, "Linear Light"), BlendMode::PinLight => write!(f, "Pin Light"), BlendMode::HardMix => write!(f, "Hard Mix"), - + // Inversion group BlendMode::Difference => write!(f, "Difference"), BlendMode::Exclusion => write!(f, "Exclusion"), BlendMode::Subtract => write!(f, "Subtract"), BlendMode::Divide => write!(f, "Divide"), - + // Component group BlendMode::Hue => write!(f, "Hue"), BlendMode::Saturation => write!(f, "Saturation"), BlendMode::Color => write!(f, "Color"), BlendMode::Luminosity => write!(f, "Luminosity"), - - BlendMode::InsertRed => write!(f, "Insert Red"), - BlendMode::InsertGreen => write!(f, "Insert Green"), - BlendMode::InsertBlue => write!(f, "Insert Blue"), + // Other utility blend modes (hidden from the normal list) + BlendMode::Erase => write!(f, "Erase"), + BlendMode::Restore => write!(f, "Restore"), + BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"), } } } -pub fn to_primtive_string(blend_mode: &BlendMode) -> &'static str { - match blend_mode { - BlendMode::Normal => "Normal", - - BlendMode::Multiply => "Multiply", - BlendMode::Darken => "Darken", - BlendMode::ColorBurn => "ColorBurn", - BlendMode::LinearBurn => "LinearBurn", - BlendMode::DarkerColor => "DarkerColor", - - BlendMode::Screen => "Screen", - BlendMode::Lighten => "Lighten", - BlendMode::ColorDodge => "ColorDodge", - BlendMode::LinearDodge => "LinearDodge", - BlendMode::LighterColor => "LighterColor", - - BlendMode::Overlay => "Overlay", - BlendMode::SoftLight => "SoftLight", - BlendMode::HardLight => "HardLight", - BlendMode::VividLight => "VividLight", - BlendMode::LinearLight => "LinearLight", - BlendMode::PinLight => "PinLight", - BlendMode::HardMix => "HardMix", - - BlendMode::Difference => "Difference", - BlendMode::Exclusion => "Exclusion", - BlendMode::Subtract => "Subtract", - BlendMode::Divide => "Divide", - - BlendMode::Hue => "Hue", - BlendMode::Saturation => "Saturation", - BlendMode::Color => "Color", - BlendMode::Luminosity => "Luminosity", - - BlendMode::InsertRed => "InsertRed", - BlendMode::InsertGreen => "InsertGreen", - BlendMode::InsertBlue => "InsertBlue", - } -} - #[derive(Debug, Clone, Copy, Default)] pub struct LuminanceNode { luminance_calc: LuminanceCalculation, @@ -452,24 +404,27 @@ pub struct BlendNode { #[node_macro::node_fn(BlendNode)] fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color { - let opacity = opacity as f32 / 100.; - - let (foreground, background) = input; + blend_colors(input.0, input.1, blend_mode, opacity as f32 / 100.) +} +#[inline(always)] +pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color { let target_color = match blend_mode { + // Normal group BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal), - BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply), + // Darken group BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken), + BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply), BlendMode::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn), BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn), BlendMode::DarkerColor => background.blend_darker_color(foreground), - - BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen), + // Lighten group BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten), + BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen), BlendMode::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge), BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge), BlendMode::LighterColor => background.blend_lighter_color(foreground), - + // Contrast group BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight), BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight), BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight), @@ -477,20 +432,20 @@ fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Col BlendMode::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light), BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light), BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix), - - BlendMode::Difference => background.blend_rgb(foreground, Color::blend_exclusion), + // Inversion group + BlendMode::Difference => background.blend_rgb(foreground, Color::blend_difference), BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion), BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract), BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide), - + // Component group BlendMode::Hue => background.blend_hue(foreground), BlendMode::Saturation => background.blend_saturation(foreground), BlendMode::Color => background.blend_color(foreground), BlendMode::Luminosity => background.blend_luminosity(foreground), - - BlendMode::InsertRed => foreground.with_red(background.r()), - BlendMode::InsertGreen => foreground.with_green(background.g()), - BlendMode::InsertBlue => foreground.with_blue(background.b()), + // Other utility blend modes (hidden from the normal list) + BlendMode::Erase => return background.alpha_subtract(foreground), + BlendMode::Restore => return background.alpha_add(foreground), + BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground), }; background.alpha_blend(target_color.to_associated_alpha(opacity)) diff --git a/node-graph/gcore/src/raster/bbox.rs b/node-graph/gcore/src/raster/bbox.rs index 4a454853..c67c119a 100644 --- a/node-graph/gcore/src/raster/bbox.rs +++ b/node-graph/gcore/src/raster/bbox.rs @@ -65,6 +65,15 @@ impl Bbox { } } + pub fn from_transform(transform: DAffine2) -> Self { + Self { + top_left: transform.transform_point2(DVec2::new(0., 1.)), + top_right: transform.transform_point2(DVec2::new(1., 1.)), + bottom_left: transform.transform_point2(DVec2::new(0., 0.)), + bottom_right: transform.transform_point2(DVec2::new(1., 0.)), + } + } + pub fn affine_transform(self, transform: DAffine2) -> Self { Self { top_left: transform.transform_point2(self.top_left), diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 2750289e..55a2c5a7 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -938,6 +938,30 @@ impl Color { alpha: self.alpha * inv_alpha + other.alpha, } } + + #[inline(always)] + pub fn alpha_add(&self, other: Color) -> Self { + Self { + alpha: (self.alpha + other.alpha).clamp(0., 1.), + ..*self + } + } + + #[inline(always)] + pub fn alpha_subtract(&self, other: Color) -> Self { + Self { + alpha: (self.alpha - other.alpha).clamp(0., 1.), + ..*self + } + } + + #[inline(always)] + pub fn alpha_multiply(&self, other: Color) -> Self { + Self { + alpha: (self.alpha * other.alpha).clamp(0., 1.), + ..*self + } + } } #[test] diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index 2392d06a..bc35f4ec 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -67,12 +67,15 @@ where impl Raster for Image

{ type Pixel = P; + #[inline(always)] fn get_pixel(&self, x: u32, y: u32) -> Option

{ self.data.get((x + y * self.width) as usize).copied() } + #[inline(always)] fn width(&self) -> u32 { self.width } + #[inline(always)] fn height(&self) -> u32 { self.height } @@ -237,6 +240,16 @@ fn map_node(input: (u32, u32), data: Vec

) -> Image

{ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ImageFrame { pub image: Image

, + + // The transform that maps image space to layer space. + // + // Image space is unitless [0, 1] for both axes, with x axis positive + // going right and y axis positive going down, with the origin lying at + // the topleft of the image and (1, 1) lying at the bottom right of the image. + // + // Layer space has pixels as its units for both axes, with the x axis + // positive going right and y axis positive going down, with the origin + // being an unspecified quantity. pub transform: DAffine2, } @@ -244,6 +257,7 @@ impl Sample for ImageFrame

{ type Pixel = P; // TODO: Improve sampling logic + #[inline(always)] fn sample(&self, pos: DVec2, _area: DVec2) -> Option { let image_size = DVec2::new(self.image.width() as f64, self.image.height() as f64); let pos = (DAffine2::from_scale(image_size) * self.transform.inverse()).transform_point2(pos); diff --git a/node-graph/gcore/src/vector/brush_stroke.rs b/node-graph/gcore/src/vector/brush_stroke.rs index 4098873f..b2f4fb59 100644 --- a/node-graph/gcore/src/vector/brush_stroke.rs +++ b/node-graph/gcore/src/vector/brush_stroke.rs @@ -1,4 +1,5 @@ use crate::raster::bbox::AxisAlignedBbox; +use crate::raster::BlendMode; use crate::Color; use dyn_any::{DynAny, StaticType}; @@ -14,6 +15,7 @@ pub struct BrushStyle { pub hardness: f64, pub flow: f64, pub spacing: f64, // Spacing as a fraction of the diameter. + pub blend_mode: BlendMode, } impl Default for BrushStyle { @@ -24,6 +26,7 @@ impl Default for BrushStyle { hardness: 50., flow: 100., spacing: 50., // Percentage of diameter. + blend_mode: BlendMode::Normal, } } } @@ -41,6 +44,8 @@ impl Hash for BrushStyle { #[derive(Clone, Debug, PartialEq, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BrushInputSample { + // The position of the sample in layer space, in pixels. + // The origin of layer space is not specified. pub position: DVec2, // Future work: pressure, stylus angle, etc. } @@ -63,11 +68,11 @@ pub struct BrushStroke { impl BrushStroke { pub fn bounding_box(&self) -> AxisAlignedBbox { let radius = self.style.diameter / 2.; - self.trace + self.compute_blit_points() .iter() - .map(|sample| AxisAlignedBbox { - start: sample.position + DVec2::new(-radius, -radius), - end: sample.position + DVec2::new(radius, radius), + .map(|pos| AxisAlignedBbox { + start: *pos + DVec2::new(-radius, -radius), + end: *pos + DVec2::new(radius, radius), }) .reduce(|a, b| a.union(&b)) .unwrap_or(AxisAlignedBbox::ZERO) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index aea9ccdf..3b9f4131 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -3,7 +3,7 @@ use crate::executor::Any; pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus}; use crate::proto::{Any as DAny, FutureAny}; -use graphene_core::raster::{to_primtive_string, BlendMode, LuminanceCalculation}; +use graphene_core::raster::{BlendMode, LuminanceCalculation}; use graphene_core::{Color, Node, Type}; use dyn_any::DynAny; @@ -188,7 +188,7 @@ impl<'a> TaggedValue { TaggedValue::F32(x) => x.to_string() + "_f32", TaggedValue::F64(x) => x.to_string() + "_f64", TaggedValue::Bool(x) => x.to_string(), - TaggedValue::BlendMode(blend_mode) => "BlendMode::".to_string() + to_primtive_string(blend_mode), + TaggedValue::BlendMode(blend_mode) => "BlendMode::".to_string() + &blend_mode.to_string(), _ => panic!("Cannot convert to primitive string"), } } diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs index 10cf8873..ebd1f5ae 100644 --- a/node-graph/gstd/src/brush.rs +++ b/node-graph/gstd/src/brush.rs @@ -1,12 +1,18 @@ -use std::marker::PhantomData; +use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode}; -use glam::{DAffine2, DVec2}; -use graphene_core::raster::{Alpha, Color, ImageFrame, Pixel, Sample}; +use graphene_core::raster::adjustments::blend_colors; +use graphene_core::raster::{Alpha, Color, Image, ImageFrame, Pixel, Sample}; +use graphene_core::raster::{BlendMode, BlendNode}; use graphene_core::transform::{Transform, TransformMut}; +use graphene_core::value::{CopiedNode, ValueNode}; +use graphene_core::vector::brush_stroke::BrushStyle; use graphene_core::vector::VectorData; use graphene_core::Node; use node_macro::node_fn; +use glam::{DAffine2, DVec2}; +use std::marker::PhantomData; + #[derive(Clone, Debug, PartialEq)] pub struct ReduceNode { pub initial: Initial, @@ -159,15 +165,20 @@ pub struct BlitNode { } #[node_fn(BlitNode<_P>)] -fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: ImageFrame<_P>, positions: Vec, blend_mode: BlendFn) -> ImageFrame<_P> +fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: Image<_P>, positions: Vec, blend_mode: BlendFn) -> ImageFrame<_P> where BlendFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>, { + if positions.len() == 0 { + return target; + } + + let target_size = DVec2::new(target.image.width as f64, target.image.height as f64); + let texture_size = DVec2::new(texture.width as f64, texture.height as f64); + let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * target.transform.inverse(); + for position in positions { - let target_size = DVec2::new(target.image.width as f64, target.image.height as f64); - let texture_size = DVec2::new(texture.image.width as f64, texture.image.height as f64); - let document_to_target = target.transform.inverse(); - let start = document_to_target.transform_point2(position) * target_size - texture_size / 2.; + let start = document_to_target.transform_point2(position).round(); let stop = start + texture_size; // Half-open integer ranges [start, stop). @@ -178,17 +189,17 @@ where let blit_area_dimensions = (clamp_stop - clamp_start).min(texture_size.as_uvec2() - blit_area_offset); // Tight blitting loop. Eagerly assert bounds to hopefully eliminate bounds check inside loop. - let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.image.width as usize) + (x as usize) }; + let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.width as usize) + (x as usize) }; let target_index = |x: u32, y: u32| -> usize { (y as usize * target.image.width as usize) + (x as usize) }; let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1); let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1); - assert!(texture_index(max_x, max_y) < texture.image.data.len()); + assert!(texture_index(max_x, max_y) < texture.data.len()); assert!(target_index(max_x, max_y) < target.image.data.len()); for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y { for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x { - let src_pixel = texture.image.data[texture_index(x, y)]; + let src_pixel = texture.data[texture_index(x, y)]; let dst_pixel = &mut target.image.data[target_index(x + clamp_start.x, y + clamp_start.y)]; *dst_pixel = blend_mode.eval((src_pixel, *dst_pixel)); } @@ -198,6 +209,76 @@ where target } +pub fn create_brush_texture(brush_style: BrushStyle) -> Image { + let stamp = BrushStampGeneratorNode::new(CopiedNode::new(brush_style.color), CopiedNode::new(brush_style.hardness), CopiedNode::new(brush_style.flow)); + let stamp = stamp.eval(brush_style.diameter); + let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.)); + let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform); + let normal_blend = BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.)); + let blend_executor = BlendImageTupleNode::new(ValueNode::new(normal_blend)); + blend_executor.eval((blank_texture, stamp)).image +} + +macro_rules! inline_blend_funcs { + ($bg:ident, $fg:ident, $blend_mode:ident, $opacity:ident, [$($mode:path,)*]) => { + match std::hint::black_box($blend_mode) { + $( + $mode => { + blend_image_closure($fg, $bg, |a, b| blend_colors(a, b, $mode, $opacity)) + } + )* + } + }; +} + +pub fn blend_with_mode(background: ImageFrame, foreground: ImageFrame, blend_mode: BlendMode, opacity: f32) -> ImageFrame { + let opacity = opacity / 100.; + inline_blend_funcs!( + background, + foreground, + blend_mode, + opacity, + [ + // Normal group + BlendMode::Normal, + // Darken group + BlendMode::Darken, + BlendMode::Multiply, + BlendMode::ColorBurn, + BlendMode::LinearBurn, + BlendMode::DarkerColor, + // Lighten group + BlendMode::Lighten, + BlendMode::Screen, + BlendMode::ColorDodge, + BlendMode::LinearDodge, + BlendMode::LighterColor, + // Contrast group + BlendMode::Overlay, + BlendMode::SoftLight, + BlendMode::HardLight, + BlendMode::VividLight, + BlendMode::LinearLight, + BlendMode::PinLight, + BlendMode::HardMix, + // Inversion group + BlendMode::Difference, + BlendMode::Exclusion, + BlendMode::Subtract, + BlendMode::Divide, + // Component group + BlendMode::Hue, + BlendMode::Saturation, + BlendMode::Color, + BlendMode::Luminosity, + // Other utility blend modes (hidden from the normal list) + BlendMode::Erase, + BlendMode::Restore, + BlendMode::MultiplyAlpha, + ] + ) +} + #[cfg(test)] mod test { use super::*; diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index ac0a3997..b2df05fe 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -279,6 +279,17 @@ fn blend_image<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample + Tra ) -> Background where MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>, +{ + blend_image_closure(foreground, background, |a, b| map_fn.eval((a, b))) +} + +pub fn blend_image_closure<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample + Transform, Background: RasterMut + Transform + Sample>( + foreground: Frame, + mut background: Background, + map_fn: MapFn, +) -> Background +where + MapFn: Fn(_P, _P) -> _P, { let background_size = DVec2::new(background.width() as f64, background.height() as f64); // Transforms a point from the background image to the forground image @@ -299,7 +310,7 @@ where if let Some(src_pixel) = foreground.sample(fg_point, area) { if let Some(dst_pixel) = background.get_pixel_mut(x, y) { - *dst_pixel = map_fn.eval((src_pixel, *dst_pixel)); + *dst_pixel = map_fn(src_pixel, *dst_pixel); } } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 63bd4389..0e5444d2 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -1,7 +1,7 @@ use graph_craft::proto::{NodeConstructor, TypeErasedPinned}; use graphene_core::ops::IdNode; use graphene_core::quantization::QuantizationChannels; -use graphene_core::raster::bbox::AxisAlignedBbox; +use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox}; use graphene_core::raster::color::Color; use graphene_core::structural::Then; use graphene_core::value::{ClonedNode, CopiedNode, ValueNode}; @@ -14,6 +14,7 @@ use graphene_core::{fn_type, raster::*}; use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor}; use graphene_core::{Node, NodeIO, NodeIOTypes}; use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode}; +use graphene_std::brush; use graphene_std::raster::BlendImageTupleNode; use graphene_std::raster::*; @@ -311,8 +312,11 @@ fn node_registry() -> HashMap> = DowncastBothNode::new(args[1]); let strokes: DowncastBothNode<(), Vec> = DowncastBothNode::new(args[2]); - let strokes = strokes.eval(()).await; - let bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); + let image_val = image.eval(()).await; + let strokes_val = strokes.eval(()).await; + let stroke_bbox = strokes_val.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); + let image_bbox = Bbox::from_transform(image_val.transform).to_axis_aligned_bbox(); + let bbox = stroke_bbox.union(&image_bbox); let mut background_bounds = CopiedNode::new(bbox.to_transform()); let bounds_transform = bounds.eval(()).await.transform; @@ -320,30 +324,66 @@ fn node_registry() -> HashMap = stroke.compute_blit_points().into_iter().collect(); + let mut bbox = stroke.bounding_box(); + bbox.start = bbox.start.floor(); + bbox.end = bbox.end.floor(); + let stroke_size = bbox.size() + DVec2::splat(stroke.style.diameter); + // For numerical stability we want to place the first blit point at a stable, integer offset + // in layer space. + let snap_offset = positions[0].floor() - positions[0]; + let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.); + let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size); - let blend_params = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.)); - let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params)); - let texture = blend_executor.eval((blank_texture, stamp)); + match stroke.style.blend_mode { + BlendMode::Erase => { + if let Some(mask) = erase_restore_mask { + let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.)); + let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params)); + erase_restore_mask = Some(blit_node.eval(mask)); + } + } - let translations: Vec<_> = stroke.compute_blit_points().into_iter().collect(); - let blit_node = BlitNode::new(ClonedNode::new(texture), ClonedNode::new(translations), ClonedNode::new(blend_params)); - blits.push(blit_node); + // Yes, this is essentially the same as the above, but we duplicate to inline the blend mode. + BlendMode::Restore => { + if let Some(mask) = erase_restore_mask { + let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Restore), CopiedNode::new(100.)); + let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params)); + erase_restore_mask = Some(blit_node.eval(mask)); + } + } + + blend_mode => { + let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(normal_blend)); + let empty_stroke_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(stroke_to_layer); + let stroke_texture = blit_node.eval(empty_stroke_texture); + // TODO: Is this the correct way to do opacity in blending? + actual_image = brush::blend_with_mode(actual_image, stroke_texture, blend_mode, stroke.style.color.a() * 100.); + } + } } - let all_blits = ChainApplyNode::new(background); - let node = ClonedNode::new(blits.into_iter()).then(all_blits); + if let Some(mask) = erase_restore_mask { + let blend_params = BlendNode::new(CopiedNode::new(BlendMode::MultiplyAlpha), CopiedNode::new(100.)); + let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params)); + actual_image = blend_executor.eval((actual_image, mask)); + } - let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(node)); + // TODO: there *has* to be a better way to do this. + let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(FutureWrapperNode::new(ClonedNode::new(actual_image)))); Box::pin(any) as TypeErasedPinned }) },