diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index cbb97587c..38d21345c 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -589,6 +589,8 @@ impl MessageHandler { + // All the image's pixels have been converted to 0..=1, linear, and premultiplied by `Color::from_rgba8_srgb` + let image_size = DVec2::new(image.width as f64, image.height as f64); let Some(image_node_type) = crate::messages::portfolio::document::node_graph::resolve_document_node_type("Image") else { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 664874253..58df47a10 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -2211,7 +2211,34 @@ fn static_nodes() -> Vec { DocumentInputType::value("Index", TaggedValue::U32(0), false), ], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], - properties: node_properties::index_node_properties, + properties: node_properties::index_properties, + ..Default::default() + }, + // Applies the given color to each pixel of an image but maintains the alpha value + DocumentNodeType { + name: "Color Fill", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_core::raster::adjustments::ColorFillNode<_>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Color", TaggedValue::Color(Color::BLACK), false), + ], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::color_fill_properties, + ..Default::default() + }, + DocumentNodeType { + name: "Color Overlay", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_core::raster::adjustments::ColorOverlayNode<_, _, _>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Color", TaggedValue::Color(Color::BLACK), false), + DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false), + DocumentInputType::value("Opacity", TaggedValue::F32(100.), false), + ], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::color_overlay_properties, ..Default::default() }, ] diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index ed30ca2d3..0b094942c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1810,7 +1810,7 @@ pub fn no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: string_properties("Node has no properties") } -pub fn index_node_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub fn index_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let index = number_widget(document_node, node_id, 1, "Index", NumberInput::default().min(0.), true); vec![LayoutGroup::Row { widgets: index }] @@ -1938,3 +1938,16 @@ pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _conte }; vec![location, dimensions, background, clip] } + +pub fn color_fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let color = color_widget(document_node, node_id, 1, "Color", ColorInput::default(), true); + vec![color] +} + +pub fn color_overlay_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let color = color_widget(document_node, node_id, 1, "Color", ColorInput::default(), true); + let blend_mode = blend_mode(document_node, node_id, 2, "Blend Mode", true); + let opacity = number_widget(document_node, node_id, 3, "Opacity", NumberInput::default().percentage(), true); + + vec![color, blend_mode, LayoutGroup::Row { widgets: opacity }] +} diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index 020c12483..f08c8eae9 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -31,6 +31,7 @@ bytemuck = { version = "1.8", features = ["derive"] } async-trait = { version = "0.1", optional = true } serde = { version = "1.0", features = [ "derive", + "rc" ], optional = true, default-features = false } log = { version = "0.4", optional = true } diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 5f424536d..54f9cbb79 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use super::curve::{Curve, CurveManipulatorGroup, ValueMapperNode}; -use super::{Channel, Color, Node}; +use super::{Channel, Color, ImageFrame, Node, RGBMut}; use bezier_rs::{Bezier, TValue}; use dyn_any::{DynAny, StaticType}; @@ -415,9 +415,8 @@ fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f32) -> Col blend_colors(input.0, input.1, blend_mode, opacity / 100.) } -#[inline(always)] -pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color { - let target_color = match blend_mode { +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 @@ -450,10 +449,19 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, 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) + // Other utility blend modes (hidden from the normal list) - do not have alpha blend + _ => panic!("Used blend mode without alpha blend"), + } +} + +#[inline(always)] +pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> 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)) @@ -914,7 +922,7 @@ fn generate_curves<_Channel: Channel + super::Linear>(_primary: (), curve: Curve bezier.find_tvalues_for_x(x) .next() .map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y) - // a very bad approximation if bezier_rs failes + // Fall back to a very bad approximation if Bezier-rs fails .unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0) }; lut[index] = _Channel::from_f64(y); @@ -926,6 +934,73 @@ fn generate_curves<_Channel: Channel + super::Linear>(_primary: (), curve: Curve ValueMapperNode::new(lut) } +#[derive(Debug, Clone)] +pub struct ColorFillNode { + color: C, +} + +#[node_macro::node_fn(ColorFillNode)] +pub fn color_fill_node(mut image_frame: ImageFrame, color: Color) -> ImageFrame { + for pixel in &mut image_frame.image.data { + pixel.set_red(color.r()); + pixel.set_blue(color.b()); + pixel.set_green(color.g()); + pixel.alpha_multiply(color); + } + + image_frame +} + +pub struct ColorOverlayNode { + color: Color, + blend_mode: BlendMode, + opacity: Opacity, +} + +#[node_macro::node_fn(ColorOverlayNode)] +pub fn color_overlay_node(mut image: ImageFrame, color: Color, blend_mode: BlendMode, opacity: f32) -> ImageFrame { + let opacity = (opacity / 100.).clamp(0., 1.); + for pixel in &mut image.image.data { + 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); + + *pixel = Color::from_rgbaf32(image.r() + overlay.r(), image.g() + overlay.g(), image.b() + overlay.b(), pixel.a()).unwrap(); + } + + image +} + +#[test] +fn color_overlay_multiply() { + use crate::raster::Image; + use crate::value::ClonedNode; + + let image_color = Color::from_rgbaf32_unchecked(0.7, 0.6, 0.5, 0.4); + let image = ImageFrame { + image: Image::new(1, 1, image_color), + ..Default::default() + }; + + // 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_f32; + + let result = ColorOverlayNode { + color: ClonedNode(overlay_color), + blend_mode: ClonedNode(BlendMode::Multiply), + opacity: ClonedNode(opacity), + } + .eval(image); + + // 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.image.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a())); +} + #[cfg(feature = "alloc")] pub use index_node::IndexNode; diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 4abc15ce8..f4df08d11 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -336,14 +336,16 @@ impl Color { /// ``` #[inline(always)] pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color { + let alpha = alpha as f32 / 255.; let map_range = |int_color| int_color as f32 / 255.0; Color { red: map_range(red), green: map_range(green), blue: map_range(blue), - alpha: map_range(alpha), + alpha, } .to_linear_srgb() + .map_rgb(|channel| channel * alpha) } /// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 484568c6c..2b98b0270 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -428,6 +428,8 @@ fn node_registry() -> HashMap, params: [f32, f32, f32, f32, f32]), register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_core::raster::IndexNode<_>, input: Vec>, params: [u32]), + register_node!(graphene_core::raster::adjustments::ColorFillNode<_>, input: ImageFrame, params: [Color]), + register_node!(graphene_core::raster::adjustments::ColorOverlayNode<_, _, _>, input: ImageFrame, params: [Color, BlendMode, f32]), vec![( NodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"), |args| {