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 <keavon@keavon.com>
This commit is contained in:
Orson Peters 2023-06-02 21:59:55 +02:00 committed by Keavon Chambers
parent 4e1bfddcd8
commit 5558deba5e
13 changed files with 397 additions and 153 deletions

View File

@ -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. /// 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)] #[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize, specta::Type)]
pub enum BlendMode { pub enum BlendMode {
// Basic group // Normal group
Normal, Normal,
// Not supported by SVG, but we should someday support: Dissolve // Not supported by SVG, but we should someday support: Dissolve
// Darken group // Darken group
Multiply,
Darken, Darken,
Multiply,
ColorBurn, ColorBurn,
// Not supported by SVG, but we should someday support: Linear Burn, Darker Color // Not supported by SVG, but we should someday support: Linear Burn, Darker Color
// Lighten group // Lighten group
Screen,
Lighten, Lighten,
Screen,
ColorDodge, ColorDodge,
// Not supported by SVG, but we should someday support: Linear Dodge (Add), Lighter Color // 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 { impl fmt::Display for BlendMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"), BlendMode::Normal => write!(f, "Normal"),
BlendMode::Multiply => write!(f, "Multiply"), // Darken group
BlendMode::Darken => write!(f, "Darken"), BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"), BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::Screen => write!(f, "Screen"), // Lighten group
BlendMode::Lighten => write!(f, "Lighten"), BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"), BlendMode::ColorDodge => write!(f, "Color Dodge"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"), BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"), BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"), BlendMode::HardLight => write!(f, "Hard Light"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"), BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"), BlendMode::Exclusion => write!(f, "Exclusion"),
// Component group
BlendMode::Hue => write!(f, "Hue"), BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"), BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"), 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) /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> &str { pub fn to_svg_style_name(&self) -> &str {
match self { match self {
// Normal group
BlendMode::Normal => "normal", BlendMode::Normal => "normal",
BlendMode::Multiply => "multiply", // Darken group
BlendMode::Darken => "darken", BlendMode::Darken => "darken",
BlendMode::Multiply => "multiply",
BlendMode::ColorBurn => "color-burn", BlendMode::ColorBurn => "color-burn",
BlendMode::Screen => "screen", // Lighten group
BlendMode::Lighten => "lighten", BlendMode::Lighten => "lighten",
BlendMode::Screen => "screen",
BlendMode::ColorDodge => "color-dodge", BlendMode::ColorDodge => "color-dodge",
// Contrast group
BlendMode::Overlay => "overlay", BlendMode::Overlay => "overlay",
BlendMode::SoftLight => "soft-light", BlendMode::SoftLight => "soft-light",
BlendMode::HardLight => "hard-light", BlendMode::HardLight => "hard-light",
// Inversion group
BlendMode::Difference => "difference", BlendMode::Difference => "difference",
BlendMode::Exclusion => "exclusion", BlendMode::Exclusion => "exclusion",
// Component group
BlendMode::Hue => "hue", BlendMode::Hue => "hue",
BlendMode::Saturation => "saturation", BlendMode::Saturation => "saturation",
BlendMode::Color => "color", BlendMode::Color => "color",
@ -94,11 +106,17 @@ impl BlendMode {
/// List of all the blend modes in their conventional ordering and grouping. /// List of all the blend modes in their conventional ordering and grouping.
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] { pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
[ [
// Normal group
&[BlendMode::Normal], &[BlendMode::Normal],
&[BlendMode::Multiply, BlendMode::Darken, BlendMode::ColorBurn], // Darken group
&[BlendMode::Screen, BlendMode::Lighten, BlendMode::ColorDodge], &[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn],
// Lighten group
&[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge],
// Contrast group
&[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight], &[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight],
// Inversion group
&[BlendMode::Difference, BlendMode::Exclusion], &[BlendMode::Difference, BlendMode::Exclusion],
// Component group
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity], &[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
] ]
} }

View File

@ -309,12 +309,15 @@ fn blend_mode(document_node: &DocumentNode, node_id: u64, index: usize, name: &s
exposed: false, exposed: false,
} = &document_node.inputs[index] } = &document_node.inputs[index]
{ {
let calculation_modes = BlendMode::list(); let entries = BlendMode::list()
let mut entries = Vec::with_capacity(calculation_modes.len()); .iter()
for method in calculation_modes { .map(|category| {
entries.push(DropdownEntryData::new(method.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(method), node_id, index))); category
} .iter()
let entries = vec![entries]; .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()]); widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder()]);
} }

View File

@ -15,13 +15,51 @@ use document_legacy::layers::layer_layer::CachedOutputData;
use document_legacy::LayerId; use document_legacy::LayerId;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork}; 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::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
use graphene_core::Color; use graphene_core::Color;
use glam::DAffine2; use glam::DAffine2;
use serde::{Deserialize, Serialize}; 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<u32> {
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)] #[derive(Default)]
pub struct BrushTool { pub struct BrushTool {
fsm_state: BrushToolFsmState, fsm_state: BrushToolFsmState,
@ -35,6 +73,8 @@ pub struct BrushOptions {
flow: f64, flow: f64,
spacing: f64, spacing: f64,
color: ToolColorOptions, color: ToolColorOptions,
blend_mode: BlendMode,
draw_mode: DrawMode,
} }
impl Default for BrushOptions { impl Default for BrushOptions {
@ -45,6 +85,8 @@ impl Default for BrushOptions {
flow: 100., flow: 100.,
spacing: 20., spacing: 20.,
color: ToolColorOptions::default(), color: ToolColorOptions::default(),
blend_mode: BlendMode::Normal,
draw_mode: DrawMode::Draw,
} }
} }
} }
@ -69,10 +111,12 @@ pub enum BrushToolMessage {
#[remain::sorted] #[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
pub enum BrushToolMessageOptionsUpdate { pub enum BrushToolMessageOptionsUpdate {
BlendMode(BlendMode),
ChangeDiameter(f64), ChangeDiameter(f64),
Color(Option<Color>), Color(Option<Color>),
ColorType(ToolColorType), ColorType(ToolColorType),
Diameter(f64), Diameter(f64),
DrawMode(DrawMode),
Flow(f64), Flow(f64),
Hardness(f64), Hardness(f64),
Spacing(f64), Spacing(f64),
@ -135,6 +179,14 @@ impl PropertyHolder for BrushTool {
widgets.push(WidgetHolder::section_separator()); 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( widgets.append(&mut self.options.color.create_widgets(
"Color", "Color",
false, false,
@ -143,6 +195,29 @@ impl PropertyHolder for BrushTool {
WidgetCallback::new(|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into()), WidgetCallback::new(|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into()),
)); ));
widgets.push(WidgetHolder::related_separator());
let blend_mode_entries: Vec<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 }])) Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
} }
} }
@ -151,6 +226,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message { if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message {
match action { match action {
BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode,
BrushToolMessageOptionsUpdate::ChangeDiameter(change) => { BrushToolMessageOptionsUpdate::ChangeDiameter(change) => {
let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5; let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5;
if needs_rounding && change > 0. { if needs_rounding && change > 0. {
@ -164,6 +240,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
self.register_properties(responses, LayoutTarget::ToolOptions); self.register_properties(responses, LayoutTarget::ToolOptions);
} }
BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter, 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::Hardness(hardness) => self.options.hardness = hardness,
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow, BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing, BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing,
@ -297,6 +374,11 @@ impl Fsm for BrushToolFsmState {
.max((tool_data.transform.matrix2 * glam::DVec2::Y).length()); .max((tool_data.transform.matrix2 * glam::DVec2::Y).length());
// Start a new stroke with a single sample // 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 { tool_data.strokes.push(BrushStroke {
trace: vec![BrushInputSample { position: layer_position }], trace: vec![BrushInputSample { position: layer_position }],
style: BrushStyle { style: BrushStyle {
@ -305,6 +387,7 @@ impl Fsm for BrushToolFsmState {
hardness: tool_options.hardness, hardness: tool_options.hardness,
flow: tool_options.flow, flow: tool_options.flow,
spacing: tool_options.spacing, spacing: tool_options.spacing,
blend_mode,
}, },
}); });

View File

@ -199,6 +199,7 @@ pub trait Sample {
impl<'i, T: Sample> Sample for &'i T { impl<'i, T: Sample> Sample for &'i T {
type Pixel = T::Pixel; type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> { fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area) (**self).sample(pos, area)
} }

View File

@ -46,19 +46,16 @@ impl core::fmt::Display for LuminanceCalculation {
} }
impl BlendMode { impl BlendMode {
pub fn list() -> [BlendMode; 29] { pub fn list() -> [&'static [BlendMode]; 6] {
[ [
BlendMode::Normal, // Normal group
BlendMode::Multiply, &[BlendMode::Normal],
BlendMode::Darken, // Darken group
BlendMode::ColorBurn, &[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor],
BlendMode::LinearBurn, // Lighten group
BlendMode::DarkerColor, &[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge, BlendMode::LinearDodge, BlendMode::LighterColor],
BlendMode::Screen, // Contrast group
BlendMode::Lighten, &[
BlendMode::ColorDodge,
BlendMode::LinearDodge,
BlendMode::LighterColor,
BlendMode::Overlay, BlendMode::Overlay,
BlendMode::SoftLight, BlendMode::SoftLight,
BlendMode::HardLight, BlendMode::HardLight,
@ -66,17 +63,11 @@ impl BlendMode {
BlendMode::LinearLight, BlendMode::LinearLight,
BlendMode::PinLight, BlendMode::PinLight,
BlendMode::HardMix, BlendMode::HardMix,
BlendMode::Difference, ],
BlendMode::Exclusion, // Inversion group
BlendMode::Subtract, &[BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract, BlendMode::Divide],
BlendMode::Divide, // Component group
BlendMode::Hue, &[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
BlendMode::InsertRed,
BlendMode::InsertGreen,
BlendMode::InsertBlue,
] ]
} }
} }
@ -84,7 +75,7 @@ impl BlendMode {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] #[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 { pub enum BlendMode {
#[default] #[default]
// Basic group // Basic group
@ -126,29 +117,30 @@ pub enum BlendMode {
Color, Color,
Luminosity, Luminosity,
// Other Stuff // Other stuff
InsertRed, Erase,
InsertGreen, Restore,
InsertBlue, MultiplyAlpha,
} }
impl core::fmt::Display for BlendMode { impl core::fmt::Display for BlendMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self { match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"), BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::Darken => write!(f, "Darken"), BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"), BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"), BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"), BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Screen => write!(f, "Screen"),
BlendMode::Lighten => write!(f, "Lighten"), BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"), BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"), BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"), BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"), BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"), BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"), BlendMode::HardLight => write!(f, "Hard Light"),
@ -156,64 +148,24 @@ impl core::fmt::Display for BlendMode {
BlendMode::LinearLight => write!(f, "Linear Light"), BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"), BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"), BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"), BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"), BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"), BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"), BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"), BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"), BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"), BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"), BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::InsertRed => write!(f, "Insert Red"), BlendMode::Erase => write!(f, "Erase"),
BlendMode::InsertGreen => write!(f, "Insert Green"), BlendMode::Restore => write!(f, "Restore"),
BlendMode::InsertBlue => write!(f, "Insert Blue"), 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)] #[derive(Debug, Clone, Copy, Default)]
pub struct LuminanceNode<LuminanceCalculation> { pub struct LuminanceNode<LuminanceCalculation> {
luminance_calc: LuminanceCalculation, luminance_calc: LuminanceCalculation,
@ -452,24 +404,27 @@ pub struct BlendNode<BlendMode, Opacity> {
#[node_macro::node_fn(BlendNode)] #[node_macro::node_fn(BlendNode)]
fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color { fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color {
let opacity = opacity as f32 / 100.; blend_colors(input.0, input.1, blend_mode, opacity as f32 / 100.)
}
let (foreground, background) = input;
#[inline(always)]
pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color {
let target_color = match blend_mode { let target_color = match blend_mode {
// Normal group
BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal), 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::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::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn),
BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn), BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn),
BlendMode::DarkerColor => background.blend_darker_color(foreground), BlendMode::DarkerColor => background.blend_darker_color(foreground),
// Lighten group
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten), 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::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge),
BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge), BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge),
BlendMode::LighterColor => background.blend_lighter_color(foreground), BlendMode::LighterColor => background.blend_lighter_color(foreground),
// Contrast group
BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight), BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight),
BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight), BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight),
BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight), 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::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light),
BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light), BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light),
BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix), BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix),
// Inversion group
BlendMode::Difference => background.blend_rgb(foreground, Color::blend_exclusion), BlendMode::Difference => background.blend_rgb(foreground, Color::blend_difference),
BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion), BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion),
BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract), BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract),
BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide), BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide),
// Component group
BlendMode::Hue => background.blend_hue(foreground), BlendMode::Hue => background.blend_hue(foreground),
BlendMode::Saturation => background.blend_saturation(foreground), BlendMode::Saturation => background.blend_saturation(foreground),
BlendMode::Color => background.blend_color(foreground), BlendMode::Color => background.blend_color(foreground),
BlendMode::Luminosity => background.blend_luminosity(foreground), BlendMode::Luminosity => background.blend_luminosity(foreground),
// Other utility blend modes (hidden from the normal list)
BlendMode::InsertRed => foreground.with_red(background.r()), BlendMode::Erase => return background.alpha_subtract(foreground),
BlendMode::InsertGreen => foreground.with_green(background.g()), BlendMode::Restore => return background.alpha_add(foreground),
BlendMode::InsertBlue => foreground.with_blue(background.b()), BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground),
}; };
background.alpha_blend(target_color.to_associated_alpha(opacity)) background.alpha_blend(target_color.to_associated_alpha(opacity))

View File

@ -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 { pub fn affine_transform(self, transform: DAffine2) -> Self {
Self { Self {
top_left: transform.transform_point2(self.top_left), top_left: transform.transform_point2(self.top_left),

View File

@ -938,6 +938,30 @@ impl Color {
alpha: self.alpha * inv_alpha + other.alpha, 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] #[test]

View File

@ -67,12 +67,15 @@ where
impl<P: Copy + Pixel> Raster for Image<P> { impl<P: Copy + Pixel> Raster for Image<P> {
type Pixel = P; type Pixel = P;
#[inline(always)]
fn get_pixel(&self, x: u32, y: u32) -> Option<P> { fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
self.data.get((x + y * self.width) as usize).copied() self.data.get((x + y * self.width) as usize).copied()
} }
#[inline(always)]
fn width(&self) -> u32 { fn width(&self) -> u32 {
self.width self.width
} }
#[inline(always)]
fn height(&self) -> u32 { fn height(&self) -> u32 {
self.height self.height
} }
@ -237,6 +240,16 @@ fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> { pub struct ImageFrame<P: Pixel> {
pub image: Image<P>, pub image: Image<P>,
// 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, pub transform: DAffine2,
} }
@ -244,6 +257,7 @@ impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
type Pixel = P; type Pixel = P;
// TODO: Improve sampling logic // TODO: Improve sampling logic
#[inline(always)]
fn sample(&self, pos: DVec2, _area: DVec2) -> Option<Self::Pixel> { fn sample(&self, pos: DVec2, _area: DVec2) -> Option<Self::Pixel> {
let image_size = DVec2::new(self.image.width() as f64, self.image.height() as f64); 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); let pos = (DAffine2::from_scale(image_size) * self.transform.inverse()).transform_point2(pos);

View File

@ -1,4 +1,5 @@
use crate::raster::bbox::AxisAlignedBbox; use crate::raster::bbox::AxisAlignedBbox;
use crate::raster::BlendMode;
use crate::Color; use crate::Color;
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
@ -14,6 +15,7 @@ pub struct BrushStyle {
pub hardness: f64, pub hardness: f64,
pub flow: f64, pub flow: f64,
pub spacing: f64, // Spacing as a fraction of the diameter. pub spacing: f64, // Spacing as a fraction of the diameter.
pub blend_mode: BlendMode,
} }
impl Default for BrushStyle { impl Default for BrushStyle {
@ -24,6 +26,7 @@ impl Default for BrushStyle {
hardness: 50., hardness: 50.,
flow: 100., flow: 100.,
spacing: 50., // Percentage of diameter. spacing: 50., // Percentage of diameter.
blend_mode: BlendMode::Normal,
} }
} }
} }
@ -41,6 +44,8 @@ impl Hash for BrushStyle {
#[derive(Clone, Debug, PartialEq, DynAny)] #[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BrushInputSample { pub struct BrushInputSample {
// The position of the sample in layer space, in pixels.
// The origin of layer space is not specified.
pub position: DVec2, pub position: DVec2,
// Future work: pressure, stylus angle, etc. // Future work: pressure, stylus angle, etc.
} }
@ -63,11 +68,11 @@ pub struct BrushStroke {
impl BrushStroke { impl BrushStroke {
pub fn bounding_box(&self) -> AxisAlignedBbox { pub fn bounding_box(&self) -> AxisAlignedBbox {
let radius = self.style.diameter / 2.; let radius = self.style.diameter / 2.;
self.trace self.compute_blit_points()
.iter() .iter()
.map(|sample| AxisAlignedBbox { .map(|pos| AxisAlignedBbox {
start: sample.position + DVec2::new(-radius, -radius), start: *pos + DVec2::new(-radius, -radius),
end: sample.position + DVec2::new(radius, radius), end: *pos + DVec2::new(radius, radius),
}) })
.reduce(|a, b| a.union(&b)) .reduce(|a, b| a.union(&b))
.unwrap_or(AxisAlignedBbox::ZERO) .unwrap_or(AxisAlignedBbox::ZERO)

View File

@ -3,7 +3,7 @@ use crate::executor::Any;
pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus}; pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus};
use crate::proto::{Any as DAny, FutureAny}; 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 graphene_core::{Color, Node, Type};
use dyn_any::DynAny; use dyn_any::DynAny;
@ -188,7 +188,7 @@ impl<'a> TaggedValue {
TaggedValue::F32(x) => x.to_string() + "_f32", TaggedValue::F32(x) => x.to_string() + "_f32",
TaggedValue::F64(x) => x.to_string() + "_f64", TaggedValue::F64(x) => x.to_string() + "_f64",
TaggedValue::Bool(x) => x.to_string(), 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"), _ => panic!("Cannot convert to primitive string"),
} }
} }

View File

@ -1,12 +1,18 @@
use std::marker::PhantomData; use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode};
use glam::{DAffine2, DVec2}; use graphene_core::raster::adjustments::blend_colors;
use graphene_core::raster::{Alpha, Color, ImageFrame, Pixel, Sample}; 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::transform::{Transform, TransformMut};
use graphene_core::value::{CopiedNode, ValueNode};
use graphene_core::vector::brush_stroke::BrushStyle;
use graphene_core::vector::VectorData; use graphene_core::vector::VectorData;
use graphene_core::Node; use graphene_core::Node;
use node_macro::node_fn; use node_macro::node_fn;
use glam::{DAffine2, DVec2};
use std::marker::PhantomData;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ReduceNode<Initial, Lambda> { pub struct ReduceNode<Initial, Lambda> {
pub initial: Initial, pub initial: Initial,
@ -159,15 +165,20 @@ pub struct BlitNode<P, Texture, Positions, BlendFn> {
} }
#[node_fn(BlitNode<_P>)] #[node_fn(BlitNode<_P>)]
fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: ImageFrame<_P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrame<_P> fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: Image<_P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrame<_P>
where where
BlendFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>, BlendFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
{ {
for position in positions { if positions.len() == 0 {
return target;
}
let target_size = DVec2::new(target.image.width as f64, target.image.height as f64); 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 texture_size = DVec2::new(texture.width as f64, texture.height as f64);
let document_to_target = target.transform.inverse(); let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * target.transform.inverse();
let start = document_to_target.transform_point2(position) * target_size - texture_size / 2.;
for position in positions {
let start = document_to_target.transform_point2(position).round();
let stop = start + texture_size; let stop = start + texture_size;
// Half-open integer ranges [start, stop). // 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); 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. // 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 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_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); 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()); 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 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 { 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)]; 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)); *dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
} }
@ -198,6 +209,76 @@ where
target target
} }
pub fn create_brush_texture(brush_style: BrushStyle) -> Image<Color> {
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<Color>, foreground: ImageFrame<Color>, blend_mode: BlendMode, opacity: f32) -> ImageFrame<Color> {
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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -279,6 +279,17 @@ fn blend_image<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> + Tra
) -> Background ) -> Background
where where
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>, 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<Pixel = _P> + Transform, Background: RasterMut<Pixel = _P> + Transform + Sample<Pixel = _P>>(
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); let background_size = DVec2::new(background.width() as f64, background.height() as f64);
// Transforms a point from the background image to the forground image // 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(src_pixel) = foreground.sample(fg_point, area) {
if let Some(dst_pixel) = background.get_pixel_mut(x, y) { 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);
} }
} }
} }

View File

@ -1,7 +1,7 @@
use graph_craft::proto::{NodeConstructor, TypeErasedPinned}; use graph_craft::proto::{NodeConstructor, TypeErasedPinned};
use graphene_core::ops::IdNode; use graphene_core::ops::IdNode;
use graphene_core::quantization::QuantizationChannels; 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::raster::color::Color;
use graphene_core::structural::Then; use graphene_core::structural::Then;
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode}; 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::{Cow, NodeIdentifier, Type, TypeDescriptor};
use graphene_core::{Node, NodeIO, NodeIOTypes}; use graphene_core::{Node, NodeIO, NodeIOTypes};
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode}; use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode};
use graphene_std::brush;
use graphene_std::raster::BlendImageTupleNode; use graphene_std::raster::BlendImageTupleNode;
use graphene_std::raster::*; use graphene_std::raster::*;
@ -311,8 +312,11 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
let bounds: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[1]); let bounds: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[1]);
let strokes: DowncastBothNode<(), Vec<BrushStroke>> = DowncastBothNode::new(args[2]); let strokes: DowncastBothNode<(), Vec<BrushStroke>> = DowncastBothNode::new(args[2]);
let strokes = strokes.eval(()).await; let image_val = image.eval(()).await;
let bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); 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 mut background_bounds = CopiedNode::new(bbox.to_transform());
let bounds_transform = bounds.eval(()).await.transform; let bounds_transform = bounds.eval(()).await.transform;
@ -320,30 +324,66 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
background_bounds = CopiedNode::new(bounds_transform); background_bounds = CopiedNode::new(bounds_transform);
} }
let has_erase_strokes = strokes_val.iter().any(|s| s.style.blend_mode == BlendMode::Erase);
let blank_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT))); let blank_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)));
let background = image.and_then(ExtendImageNode::new(blank_image)); let opaque_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::WHITE)));
let mut erase_restore_mask = has_erase_strokes.then(|| opaque_image.eval(()));
let mut actual_image = ExtendImageNode::new(blank_image).eval(image_val);
for stroke in strokes_val {
let normal_blend = BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
let mut blits = Vec::new(); // Create brush texture.
for stroke in strokes { // TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
let stamp = BrushStampGeneratorNode::new(CopiedNode::new(stroke.style.color), CopiedNode::new(stroke.style.hardness), CopiedNode::new(stroke.style.flow)); let brush_texture = brush::create_brush_texture(stroke.style.clone());
let stamp = stamp.eval(stroke.style.diameter);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(stroke.style.diameter), 0., -DVec2::splat(stroke.style.diameter / 2.0)); // Compute transformation from stroke texture space into layer space, and create the stroke texture.
let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform); let positions: Vec<_> = 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.)); match stroke.style.blend_mode {
let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params)); BlendMode::Erase => {
let texture = blend_executor.eval((blank_texture, stamp)); if let Some(mask) = erase_restore_mask {
let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.));
let translations: Vec<_> = stroke.compute_blit_points().into_iter().collect(); let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
let blit_node = BlitNode::new(ClonedNode::new(texture), ClonedNode::new(translations), ClonedNode::new(blend_params)); erase_restore_mask = Some(blit_node.eval(mask));
blits.push(blit_node); }
} }
let all_blits = ChainApplyNode::new(background); // Yes, this is essentially the same as the above, but we duplicate to inline the blend mode.
let node = ClonedNode::new(blits.into_iter()).then(all_blits); 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));
}
}
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(node)); 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.);
}
}
}
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));
}
// 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 Box::pin(any) as TypeErasedPinned
}) })
}, },