New node: Color Overlay (#1391)

* [wip]feat: implement Color Overlay node's base logic

* [wip]feat: attempt implementing node

* feat: implement naive node logic
- this needs to accommodate coloring the image

* [wip]feat: attempt implementing ColorFillNode

* fix: color fill node implementation path

* Simplify document node definition

* [wip]feat: implement ColorFillNode

- correct the node implementation logic
-[wip]use node in Color Overlay node network

* Remove secondary image input

* chore: perform cleanup and minor optimization

- rename ColorFillNode node to Color Fill
- remove unneeded clone method call

* refactor: optimize node logic and hide optional params

* fix: color fill implementation
- fix broken color fill implementation logic
- add alpha multiplication data to the image

* Fix colour overlay node

* Blend mode

* chore: remove unused import

* Debug logging for overlay

* Use premultiplied alpha

* Comment out logging

* Code review nits

* Remove color debug logging

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dhruv 2023-09-02 14:52:43 +05:30 committed by Keavon Chambers
parent 81a7c2dfcf
commit a2db95d18b
7 changed files with 131 additions and 9 deletions

View file

@ -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 }

View file

@ -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<C> {
color: C,
}
#[node_macro::node_fn(ColorFillNode)]
pub fn color_fill_node(mut image_frame: ImageFrame<Color>, color: Color) -> ImageFrame<Color> {
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, BlendMode, Opacity> {
color: Color,
blend_mode: BlendMode,
opacity: Opacity,
}
#[node_macro::node_fn(ColorOverlayNode)]
pub fn color_overlay_node(mut image: ImageFrame<Color>, color: Color, blend_mode: BlendMode, opacity: f32) -> ImageFrame<Color> {
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;

View file

@ -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)

View file

@ -428,6 +428,8 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f32, f32, f32, f32, f32]),
register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame<Color>, params: [ImageFrame<Color>]),
register_node!(graphene_core::raster::IndexNode<_>, input: Vec<ImageFrame<Color>>, params: [u32]),
register_node!(graphene_core::raster::adjustments::ColorFillNode<_>, input: ImageFrame<Color>, params: [Color]),
register_node!(graphene_core::raster::adjustments::ColorOverlayNode<_, _, _>, input: ImageFrame<Color>, params: [Color, BlendMode, f32]),
vec![(
NodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"),
|args| {