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;