From e7b8b5a3b625ccb25e5e8f8c0c2a8cd293ab6c79 Mon Sep 17 00:00:00 2001 From: Firestar99 Date: Thu, 24 Jul 2025 15:32:10 +0200 Subject: [PATCH] Shaders: `graster-nodes` no-std prep (#2924) * raster-nodes: remove commented out index node * raster-nodes: move `CubicSplines` to separate mod * raster-nodes: create `mod blending_nodes` and move assoc nodes * raster-nodes: move node `gradient_map` to its own mod --- .../messages/portfolio/document_migration.rs | 47 +-- node-graph/gbrush/src/brush.rs | 2 +- node-graph/graster-nodes/src/adjust.rs | 35 +++ node-graph/graster-nodes/src/adjustments.rs | 290 +----------------- .../graster-nodes/src/blending_nodes.rs | 201 ++++++++++++ node-graph/graster-nodes/src/cubic_spline.rs | 118 +++++++ node-graph/graster-nodes/src/curve.rs | 119 ------- node-graph/graster-nodes/src/gradient_map.rs | 30 ++ node-graph/graster-nodes/src/lib.rs | 5 + 9 files changed, 426 insertions(+), 421 deletions(-) create mode 100644 node-graph/graster-nodes/src/adjust.rs create mode 100644 node-graph/graster-nodes/src/blending_nodes.rs create mode 100644 node-graph/graster-nodes/src/cubic_spline.rs create mode 100644 node-graph/graster-nodes/src/gradient_map.rs diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 96213b4e..b56d764f 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -251,7 +251,28 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::vector::auto_tangents::IDENTIFIER, aliases: &["graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::RemoveHandlesNode"], }, - // raster::adjustments + // graphene_raster_nodes::blending_nodes + NodeReplacement { + node: graphene_std::raster_nodes::blending_nodes::blend::IDENTIFIER, + aliases: &[ + "graphene_raster_nodes::adjustments::BlendNode", + "graphene_core::raster::adjustments::BlendNode", + "graphene_core::raster::BlendNode", + ], + }, + NodeReplacement { + node: graphene_std::raster_nodes::blending_nodes::blend_color_pair::IDENTIFIER, + aliases: &["graphene_raster_nodes::adjustments::BlendColorPairNode", "graphene_core::raster::BlendColorPairNode"], + }, + NodeReplacement { + node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER, + aliases: &[ + "graphene_raster_nodes::adjustments::ColorOverlayNode", + "graphene_core::raster::adjustments::ColorOverlayNode", + "graphene_raster_nodes::generate_curves::ColorOverlayNode", + ], + }, + // graphene_raster_nodes::adjustments NodeReplacement { node: graphene_std::raster_nodes::adjustments::luminance::IDENTIFIER, aliases: &["graphene_core::raster::adjustments::LuminanceNode", "graphene_core::raster::LuminanceNode"], @@ -292,20 +313,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::raster_nodes::adjustments::threshold::IDENTIFIER, aliases: &["graphene_core::raster::adjustments::ThresholdNode", "graphene_core::raster::ThresholdNode"], }, - NodeReplacement { - node: graphene_std::raster_nodes::adjustments::blend::IDENTIFIER, - aliases: &["graphene_core::raster::adjustments::BlendNode", "graphene_core::raster::BlendNode"], - }, - NodeReplacement { - node: graphene_std::raster_nodes::adjustments::blend_color_pair::IDENTIFIER, - aliases: &["graphene_core::raster::BlendColorPairNode"], - }, - // this node doesn't seem to exist? - // (graphene_std::raster_nodes::adjustments::blend_color::IDENTIFIER, &["graphene_core::raster::adjustments::BlendColorsNode","graphene_core::raster::BlendColorsNode"]), - NodeReplacement { - node: graphene_std::raster_nodes::adjustments::gradient_map::IDENTIFIER, - aliases: &["graphene_core::raster::adjustments::GradientMapNode", "graphene_core::raster::GradientMapNode"], - }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::vibrance::IDENTIFIER, aliases: &["graphene_core::raster::adjustments::VibranceNode", "graphene_core::raster::VibranceNode"], @@ -326,11 +333,15 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::raster_nodes::adjustments::exposure::IDENTIFIER, aliases: &["graphene_core::raster::adjustments::ExposureNode", "graphene_core::raster::ExposureNode"], }, + // graphene_raster_nodes::* NodeReplacement { - node: graphene_std::raster_nodes::adjustments::color_overlay::IDENTIFIER, - aliases: &["graphene_core::raster::adjustments::ColorOverlayNode", "graphene_raster_nodes::generate_curves::ColorOverlayNode"], + node: graphene_std::raster_nodes::gradient_map::gradient_map::IDENTIFIER, + aliases: &[ + "graphene_raster_nodes::gradient_map::GradientMapNode", + "graphene_core::raster::adjustments::GradientMapNode", + "graphene_core::raster::GradientMapNode", + ], }, - // raster NodeReplacement { node: graphene_std::raster_nodes::generate_curves::generate_curves::IDENTIFIER, aliases: &["graphene_core::raster::adjustments::GenerateCurvesNode"], diff --git a/node-graph/gbrush/src/brush.rs b/node-graph/gbrush/src/brush.rs index 3a085c0a..01289fdb 100644 --- a/node-graph/gbrush/src/brush.rs +++ b/node-graph/gbrush/src/brush.rs @@ -14,7 +14,7 @@ use graphene_core::registry::FutureWrapperNode; use graphene_core::transform::Transform; use graphene_core::value::ClonedNode; use graphene_core::{Ctx, Node}; -use graphene_raster_nodes::adjustments::blend_colors; +use graphene_raster_nodes::blending_nodes::blend_colors; use graphene_raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/node-graph/graster-nodes/src/adjust.rs b/node-graph/graster-nodes/src/adjust.rs new file mode 100644 index 00000000..ffbbac18 --- /dev/null +++ b/node-graph/graster-nodes/src/adjust.rs @@ -0,0 +1,35 @@ +use graphene_core::Color; +use graphene_core::gradient::GradientStops; +use graphene_core::raster_types::{CPU, RasterDataTable}; + +pub trait Adjust

{ + fn adjust(&mut self, map_fn: impl Fn(&P) -> P); +} +impl Adjust for Color { + fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { + *self = map_fn(self); + } +} +impl Adjust for Option { + fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { + if let Some(v) = self { + *v = map_fn(v) + } + } +} +impl Adjust for GradientStops { + fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { + for (_pos, c) in self.iter_mut() { + *c = map_fn(c); + } + } +} +impl Adjust for RasterDataTable { + fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { + for instance in self.instance_mut_iter() { + for c in instance.instance.data_mut().data.iter_mut() { + *c = map_fn(c); + } + } + } +} diff --git a/node-graph/graster-nodes/src/adjustments.rs b/node-graph/graster-nodes/src/adjustments.rs index eb516a72..f517476b 100644 --- a/node-graph/graster-nodes/src/adjustments.rs +++ b/node-graph/graster-nodes/src/adjustments.rs @@ -1,17 +1,13 @@ #![allow(clippy::too_many_arguments)] -use crate::curve::CubicSplines; +use crate::adjust::Adjust; +use crate::cubic_spline::CubicSplines; use dyn_any::DynAny; -use graphene_core::Node; -use graphene_core::blending::BlendMode; use graphene_core::color::Color; -use graphene_core::color::Pixel; use graphene_core::context::Ctx; use graphene_core::gradient::GradientStops; -use graphene_core::raster::image::Image; -use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; +use graphene_core::raster_types::{CPU, RasterDataTable}; use graphene_core::registry::types::{Angle, Percentage, SignedPercentage}; -use std::cmp::Ordering; use std::fmt::Debug; // TODO: Implement the following: @@ -137,10 +133,10 @@ fn make_opaque>( fn brightness_contrast>( _: impl Ctx, #[implementations( - Color, - RasterDataTable, - GradientStops, -)] + Color, + RasterDataTable, + GradientStops, + )] mut input: T, brightness: SignedPercentage, contrast: SignedPercentage, @@ -447,202 +443,6 @@ async fn threshold>( image } -trait Blend { - fn blend(&self, under: &Self, blend_fn: impl Fn(P, P) -> P) -> Self; -} -impl Blend for Color { - fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { - blend_fn(*self, *under) - } -} -impl Blend for Option { - fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { - match (self, under) { - (Some(a), Some(b)) => Some(blend_fn(*a, *b)), - (a, None) => *a, - (None, b) => *b, - } - } -} -impl Blend for RasterDataTable { - fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { - let mut result_table = self.clone(); - - for (over, under) in result_table.instance_mut_iter().zip(under.instance_ref_iter()) { - let data = over.instance.data.iter().zip(under.instance.data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect(); - - *over.instance = Raster::new_cpu(Image { - data, - width: over.instance.width, - height: over.instance.height, - base64_string: None, - }); - } - - result_table - } -} -impl Blend for GradientStops { - fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { - let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::>(); - combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6); - combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - - let stops = combined_stops - .into_iter() - .map(|&position| { - let over_color = self.evaluate(position); - let under_color = under.evaluate(position); - let color = blend_fn(over_color, under_color); - (position, color) - }) - .collect::>(); - - GradientStops::new(stops) - } -} - -#[node_macro::node(category("Raster"))] -async fn blend + Send>( - _: impl Ctx, - #[implementations( - Color, - RasterDataTable, - GradientStops, - )] - over: T, - #[expose] - #[implementations( - Color, - RasterDataTable, - GradientStops, - )] - under: T, - blend_mode: BlendMode, - #[default(100.)] opacity: Percentage, -) -> T { - over.blend(&under, |a, b| blend_colors(a, b, blend_mode, opacity / 100.)) -} - -#[node_macro::node(category(""), skip_impl)] -fn blend_color_pair(input: (Color, Color), blend_mode: &'n BlendModeNode, opacity: &'n OpacityNode) -> Color -where - BlendModeNode: Node<'n, (), Output = BlendMode> + 'n, - OpacityNode: Node<'n, (), Output = Percentage> + 'n, -{ - let blend_mode = blend_mode.eval(()); - let opacity = opacity.eval(()); - blend_colors(input.0, input.1, blend_mode, opacity / 100.) -} - -pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color { - match blend_mode { - // Normal group - BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal), - // 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), - // 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), - BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light), - 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), - // 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), - // Other utility blend modes (hidden from the normal list) - do not have alpha blend - _ => panic!("Used blend mode without alpha blend"), - } -} - -trait Adjust

{ - fn adjust(&mut self, map_fn: impl Fn(&P) -> P); -} -impl Adjust for Color { - fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - *self = map_fn(self); - } -} -impl Adjust for Option { - fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - if let Some(v) = self { - *v = map_fn(v) - } - } -} -impl Adjust for GradientStops { - fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for (_pos, c) in self.iter_mut() { - *c = map_fn(c); - } - } -} -impl Adjust for RasterDataTable { - fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for instance in self.instance_mut_iter() { - for c in instance.instance.data_mut().data.iter_mut() { - *c = map_fn(c); - } - } - } -} - -#[inline(always)] -pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f64) -> Color { - let target_color = match blend_mode { - // Other utility blend modes (hidden from the normal list) - do not have alpha blend - BlendMode::Erase => return background.alpha_subtract(foreground), - BlendMode::Restore => return background.alpha_add(foreground), - BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground), - blend_mode => apply_blend_mode(foreground, background, blend_mode), - }; - - background.alpha_blend(target_color.to_associated_alpha(opacity as f32)) -} - -// Aims for interoperable compatibility with: -// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map -// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0) -#[node_macro::node(category("Raster: Adjustment"))] -async fn gradient_map>( - _: impl Ctx, - #[implementations( - Color, - RasterDataTable, - GradientStops, - )] - mut image: T, - gradient: GradientStops, - reverse: bool, -) -> T { - image.adjust(|color| { - let intensity = color.luminance_srgb(); - let intensity = if reverse { 1. - intensity } else { intensity }; - gradient.evaluate(intensity as f64).to_linear_srgb() - }); - - image -} - // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27-,vibA%27%20%3D%20Vibrance,-%27hue%20%27%20%3D%20Old // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Vibrance%20(Photoshop%20CS3) @@ -1146,79 +946,3 @@ async fn exposure>( }); input } - -#[node_macro::node(category("Raster: Adjustment"))] -fn color_overlay>( - _: impl Ctx, - #[implementations( - Color, - RasterDataTable, - GradientStops, - )] - mut image: T, - #[default(Color::BLACK)] color: Color, - blend_mode: BlendMode, - #[default(100.)] opacity: Percentage, -) -> T { - let opacity = (opacity as f32 / 100.).clamp(0., 1.); - - image.adjust(|pixel| { - let image = pixel.map_rgb(|channel| channel * (1. - opacity)); - - // The apply blend mode function divides rgb by the alpha channel for the background. This undoes that. - let associated_pixel = Color::from_rgbaf32_unchecked(pixel.r() * pixel.a(), pixel.g() * pixel.a(), pixel.b() * pixel.a(), pixel.a()); - let overlay = apply_blend_mode(color, associated_pixel, blend_mode).map_rgb(|channel| channel * opacity); - - Color::from_rgbaf32_unchecked(image.r() + overlay.r(), image.g() + overlay.g(), image.b() + overlay.b(), pixel.a()) - }); - image -} - -// pub use index_node::IndexNode; - -// mod index_node { -// use crate::raster::{Color, Image}; -// use crate::Ctx; - -// #[node_macro::node(category(""))] -// pub fn index( -// _: impl Ctx, -// #[implementations(Vec>, Vec)] -// #[widget(ParsedWidgetOverride::Hidden)] -// input: Vec, -// index: u32, -// ) -> T { -// if (index as usize) < input.len() { -// input[index as usize].clone() -// } else { -// warn!("The number of segments is {} but the requested segment is {}!", input.len(), index); -// Default::default() -// } -// } -// } - -#[cfg(test)] -mod test { - use graphene_core::blending::BlendMode; - use graphene_core::color::Color; - use graphene_core::raster::image::Image; - use graphene_core::raster_types::{Raster, RasterDataTable}; - - #[tokio::test] - async fn color_overlay_multiply() { - let image_color = Color::from_rgbaf32_unchecked(0.7, 0.6, 0.5, 0.4); - let image = Image::new(1, 1, image_color); - - // Color { red: 0., green: 1., blue: 0., alpha: 1. } - let overlay_color = Color::GREEN; - - // 100% of the output should come from the multiplied value - let opacity = 100_f64; - - let result = super::color_overlay((), RasterDataTable::new(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity); - let result = result.instance_ref_iter().next().unwrap().instance; - - // The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0) - assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a())); - } -} diff --git a/node-graph/graster-nodes/src/blending_nodes.rs b/node-graph/graster-nodes/src/blending_nodes.rs new file mode 100644 index 00000000..dbdf6e0a --- /dev/null +++ b/node-graph/graster-nodes/src/blending_nodes.rs @@ -0,0 +1,201 @@ +use crate::adjust::Adjust; +use graphene_core::color::Pixel; +use graphene_core::gradient::GradientStops; +use graphene_core::raster::Image; +use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; +use graphene_core::registry::types::Percentage; +use graphene_core::{BlendMode, Color, Ctx}; +use std::cmp::Ordering; + +pub trait Blend { + fn blend(&self, under: &Self, blend_fn: impl Fn(P, P) -> P) -> Self; +} +impl Blend for Color { + fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { + blend_fn(*self, *under) + } +} +impl Blend for Option { + fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { + match (self, under) { + (Some(a), Some(b)) => Some(blend_fn(*a, *b)), + (a, None) => *a, + (None, b) => *b, + } + } +} +impl Blend for RasterDataTable { + fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { + let mut result_table = self.clone(); + + for (over, under) in result_table.instance_mut_iter().zip(under.instance_ref_iter()) { + let data = over.instance.data.iter().zip(under.instance.data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect(); + + *over.instance = Raster::new_cpu(Image { + data, + width: over.instance.width, + height: over.instance.height, + base64_string: None, + }); + } + + result_table + } +} +impl Blend for GradientStops { + fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { + let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::>(); + combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6); + combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + let stops = combined_stops + .into_iter() + .map(|&position| { + let over_color = self.evaluate(position); + let under_color = under.evaluate(position); + let color = blend_fn(over_color, under_color); + (position, color) + }) + .collect::>(); + + GradientStops::new(stops) + } +} + +#[inline(always)] +pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f64) -> Color { + let target_color = match blend_mode { + // Other utility blend modes (hidden from the normal list) - do not have alpha blend + BlendMode::Erase => return background.alpha_subtract(foreground), + BlendMode::Restore => return background.alpha_add(foreground), + BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground), + blend_mode => apply_blend_mode(foreground, background, blend_mode), + }; + + background.alpha_blend(target_color.to_associated_alpha(opacity as f32)) +} + +pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color { + match blend_mode { + // Normal group + BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal), + // 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), + // 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), + BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light), + 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), + // 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), + // Other utility blend modes (hidden from the normal list) - do not have alpha blend + _ => panic!("Used blend mode without alpha blend"), + } +} + +#[node_macro::node(category("Raster"))] +async fn blend + Send>( + _: impl Ctx, + #[implementations( + Color, + RasterDataTable, + GradientStops, + )] + over: T, + #[expose] + #[implementations( + Color, + RasterDataTable, + GradientStops, + )] + under: T, + blend_mode: BlendMode, + #[default(100.)] opacity: Percentage, +) -> T { + over.blend(&under, |a, b| blend_colors(a, b, blend_mode, opacity / 100.)) +} + +#[node_macro::node(category("Raster: Adjustment"))] +fn color_overlay>( + _: impl Ctx, + #[implementations( + Color, + RasterDataTable, + GradientStops, + )] + mut image: T, + #[default(Color::BLACK)] color: Color, + blend_mode: BlendMode, + #[default(100.)] opacity: Percentage, +) -> T { + let opacity = (opacity as f32 / 100.).clamp(0., 1.); + + image.adjust(|pixel| { + let image = pixel.map_rgb(|channel| channel * (1. - opacity)); + + // The apply blend mode function divides rgb by the alpha channel for the background. This undoes that. + let associated_pixel = Color::from_rgbaf32_unchecked(pixel.r() * pixel.a(), pixel.g() * pixel.a(), pixel.b() * pixel.a(), pixel.a()); + let overlay = apply_blend_mode(color, associated_pixel, blend_mode).map_rgb(|channel| channel * opacity); + + Color::from_rgbaf32_unchecked(image.r() + overlay.r(), image.g() + overlay.g(), image.b() + overlay.b(), pixel.a()) + }); + image +} + +#[node_macro::node(category(""), skip_impl)] +fn blend_color_pair(input: (Color, Color), blend_mode: &'n BlendModeNode, opacity: &'n OpacityNode) -> Color +where + BlendModeNode: graphene_core::Node<'n, (), Output = BlendMode> + 'n, + OpacityNode: graphene_core::Node<'n, (), Output = Percentage> + 'n, +{ + let blend_mode = blend_mode.eval(()); + let opacity = opacity.eval(()); + blend_colors(input.0, input.1, blend_mode, opacity / 100.) +} + +#[cfg(test)] +mod test { + use graphene_core::blending::BlendMode; + use graphene_core::color::Color; + use graphene_core::raster::image::Image; + use graphene_core::raster_types::{Raster, RasterDataTable}; + + #[tokio::test] + async fn color_overlay_multiply() { + let image_color = Color::from_rgbaf32_unchecked(0.7, 0.6, 0.5, 0.4); + let image = Image::new(1, 1, image_color); + + // Color { red: 0., green: 1., blue: 0., alpha: 1. } + let overlay_color = Color::GREEN; + + // 100% of the output should come from the multiplied value + let opacity = 100_f64; + + let result = super::color_overlay((), RasterDataTable::new(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity); + let result = result.instance_ref_iter().next().unwrap().instance; + + // The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0) + assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a())); + } +} diff --git a/node-graph/graster-nodes/src/cubic_spline.rs b/node-graph/graster-nodes/src/cubic_spline.rs new file mode 100644 index 00000000..99675422 --- /dev/null +++ b/node-graph/graster-nodes/src/cubic_spline.rs @@ -0,0 +1,118 @@ +#[derive(Debug)] +pub struct CubicSplines { + pub x: [f32; 4], + pub y: [f32; 4], +} + +impl CubicSplines { + pub fn solve(&self) -> [f32; 4] { + let (x, y) = (&self.x, &self.y); + + // Build an augmented matrix to solve the system of equations using Gaussian elimination + let mut augmented_matrix = [ + [ + 2. / (x[1] - x[0]), + 1. / (x[1] - x[0]), + 0., + 0., + // | + 3. * (y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])), + ], + [ + 1. / (x[1] - x[0]), + 2. * (1. / (x[1] - x[0]) + 1. / (x[2] - x[1])), + 1. / (x[2] - x[1]), + 0., + // | + 3. * ((y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])) + (y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1]))), + ], + [ + 0., + 1. / (x[2] - x[1]), + 2. * (1. / (x[2] - x[1]) + 1. / (x[3] - x[2])), + 1. / (x[3] - x[2]), + // | + 3. * ((y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1])) + (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2]))), + ], + [ + 0., + 0., + 1. / (x[3] - x[2]), + 2. / (x[3] - x[2]), + // | + 3. * (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2])), + ], + ]; + + // Gaussian elimination: forward elimination + for row in 0..4 { + let pivot_row_index = (row..4) + .max_by(|&a_row, &b_row| augmented_matrix[a_row][row].abs().partial_cmp(&augmented_matrix[b_row][row].abs()).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap(); + + // Swap the current row with the row that has the largest pivot element + augmented_matrix.swap(row, pivot_row_index); + + // Eliminate the current column in all rows below the current one + for row_below_current in row + 1..4 { + assert!(augmented_matrix[row][row].abs() > f32::EPSILON); + + let scale_factor = augmented_matrix[row_below_current][row] / augmented_matrix[row][row]; + for col in row..5 { + augmented_matrix[row_below_current][col] -= augmented_matrix[row][col] * scale_factor + } + } + } + + // Gaussian elimination: back substitution + let mut solutions = [0.; 4]; + for col in (0..4).rev() { + assert!(augmented_matrix[col][col].abs() > f32::EPSILON); + + solutions[col] = augmented_matrix[col][4] / augmented_matrix[col][col]; + + for row in (0..col).rev() { + augmented_matrix[row][4] -= augmented_matrix[row][col] * solutions[col]; + augmented_matrix[row][col] = 0.; + } + } + + solutions + } + + pub fn interpolate(&self, input: f32, solutions: &[f32]) -> f32 { + if input <= self.x[0] { + return self.y[0]; + } + if input >= self.x[self.x.len() - 1] { + return self.y[self.x.len() - 1]; + } + + // Find the segment that the input falls between + let mut segment = 1; + while self.x[segment] < input { + segment += 1; + } + let segment_start = segment - 1; + let segment_end = segment; + + // Calculate the output value using quadratic interpolation + let input_value = self.x[segment_start]; + let input_value_prev = self.x[segment_end]; + let output_value = self.y[segment_start]; + let output_value_prev = self.y[segment_end]; + let solutions_value = solutions[segment_start]; + let solutions_value_prev = solutions[segment_end]; + + let output_delta = solutions_value_prev * (input_value - input_value_prev) - (output_value - output_value_prev); + let solution_delta = (output_value - output_value_prev) - solutions_value * (input_value - input_value_prev); + + let input_ratio = (input - input_value_prev) / (input_value - input_value_prev); + let prev_output_ratio = (1. - input_ratio) * output_value_prev; + let output_ratio = input_ratio * output_value; + let quadratic_ratio = input_ratio * (1. - input_ratio) * (output_delta * (1. - input_ratio) + solution_delta * input_ratio); + + let result = prev_output_ratio + output_ratio + quadratic_ratio; + result.clamp(0., 1.) + } +} diff --git a/node-graph/graster-nodes/src/curve.rs b/node-graph/graster-nodes/src/curve.rs index 188ef4a5..aa84d9ec 100644 --- a/node-graph/graster-nodes/src/curve.rs +++ b/node-graph/graster-nodes/src/curve.rs @@ -45,125 +45,6 @@ impl Hash for CurveManipulatorGroup { } } -#[derive(Debug)] -pub struct CubicSplines { - pub x: [f32; 4], - pub y: [f32; 4], -} - -impl CubicSplines { - pub fn solve(&self) -> [f32; 4] { - let (x, y) = (&self.x, &self.y); - - // Build an augmented matrix to solve the system of equations using Gaussian elimination - let mut augmented_matrix = [ - [ - 2. / (x[1] - x[0]), - 1. / (x[1] - x[0]), - 0., - 0., - // | - 3. * (y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])), - ], - [ - 1. / (x[1] - x[0]), - 2. * (1. / (x[1] - x[0]) + 1. / (x[2] - x[1])), - 1. / (x[2] - x[1]), - 0., - // | - 3. * ((y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])) + (y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1]))), - ], - [ - 0., - 1. / (x[2] - x[1]), - 2. * (1. / (x[2] - x[1]) + 1. / (x[3] - x[2])), - 1. / (x[3] - x[2]), - // | - 3. * ((y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1])) + (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2]))), - ], - [ - 0., - 0., - 1. / (x[3] - x[2]), - 2. / (x[3] - x[2]), - // | - 3. * (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2])), - ], - ]; - - // Gaussian elimination: forward elimination - for row in 0..4 { - let pivot_row_index = (row..4) - .max_by(|&a_row, &b_row| augmented_matrix[a_row][row].abs().partial_cmp(&augmented_matrix[b_row][row].abs()).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap(); - - // Swap the current row with the row that has the largest pivot element - augmented_matrix.swap(row, pivot_row_index); - - // Eliminate the current column in all rows below the current one - for row_below_current in row + 1..4 { - assert!(augmented_matrix[row][row].abs() > f32::EPSILON); - - let scale_factor = augmented_matrix[row_below_current][row] / augmented_matrix[row][row]; - for col in row..5 { - augmented_matrix[row_below_current][col] -= augmented_matrix[row][col] * scale_factor - } - } - } - - // Gaussian elimination: back substitution - let mut solutions = [0.; 4]; - for col in (0..4).rev() { - assert!(augmented_matrix[col][col].abs() > f32::EPSILON); - - solutions[col] = augmented_matrix[col][4] / augmented_matrix[col][col]; - - for row in (0..col).rev() { - augmented_matrix[row][4] -= augmented_matrix[row][col] * solutions[col]; - augmented_matrix[row][col] = 0.; - } - } - - solutions - } - - pub fn interpolate(&self, input: f32, solutions: &[f32]) -> f32 { - if input <= self.x[0] { - return self.y[0]; - } - if input >= self.x[self.x.len() - 1] { - return self.y[self.x.len() - 1]; - } - - // Find the segment that the input falls between - let mut segment = 1; - while self.x[segment] < input { - segment += 1; - } - let segment_start = segment - 1; - let segment_end = segment; - - // Calculate the output value using quadratic interpolation - let input_value = self.x[segment_start]; - let input_value_prev = self.x[segment_end]; - let output_value = self.y[segment_start]; - let output_value_prev = self.y[segment_end]; - let solutions_value = solutions[segment_start]; - let solutions_value_prev = solutions[segment_end]; - - let output_delta = solutions_value_prev * (input_value - input_value_prev) - (output_value - output_value_prev); - let solution_delta = (output_value - output_value_prev) - solutions_value * (input_value - input_value_prev); - - let input_ratio = (input - input_value_prev) / (input_value - input_value_prev); - let prev_output_ratio = (1. - input_ratio) * output_value_prev; - let output_ratio = input_ratio * output_value; - let quadratic_ratio = input_ratio * (1. - input_ratio) * (output_delta * (1. - input_ratio) + solution_delta * input_ratio); - - let result = prev_output_ratio + output_ratio + quadratic_ratio; - result.clamp(0., 1.) - } -} - pub struct ValueMapperNode { lut: Vec, } diff --git a/node-graph/graster-nodes/src/gradient_map.rs b/node-graph/graster-nodes/src/gradient_map.rs new file mode 100644 index 00000000..5457aeb4 --- /dev/null +++ b/node-graph/graster-nodes/src/gradient_map.rs @@ -0,0 +1,30 @@ +//! Not immediately shader compatible due to needing [`GradientStops`] as a param, which needs [`Vec`] + +use crate::adjust::Adjust; +use graphene_core::gradient::GradientStops; +use graphene_core::raster_types::{CPU, RasterDataTable}; +use graphene_core::{Color, Ctx}; + +// Aims for interoperable compatibility with: +// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map +// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0) +#[node_macro::node(category("Raster: Adjustment"))] +async fn gradient_map>( + _: impl Ctx, + #[implementations( + Color, + RasterDataTable, + GradientStops, + )] + mut image: T, + gradient: GradientStops, + reverse: bool, +) -> T { + image.adjust(|color| { + let intensity = color.luminance_srgb(); + let intensity = if reverse { 1. - intensity } else { intensity }; + gradient.evaluate(intensity as f64).to_linear_srgb() + }); + + image +} diff --git a/node-graph/graster-nodes/src/lib.rs b/node-graph/graster-nodes/src/lib.rs index 938923db..78106fa9 100644 --- a/node-graph/graster-nodes/src/lib.rs +++ b/node-graph/graster-nodes/src/lib.rs @@ -1,7 +1,12 @@ +pub mod adjust; pub mod adjustments; +pub mod blending_nodes; +pub mod cubic_spline; + pub mod curve; pub mod dehaze; pub mod filter; pub mod generate_curves; +pub mod gradient_map; pub mod image_color_palette; pub mod std_nodes;