From cbda8114805ac18db8f53cc5ab0f68e083f47d91 Mon Sep 17 00:00:00 2001 From: Henry Barreto <68656e72792e6261727265746f@gmail.com> Date: Sat, 9 Dec 2023 20:21:41 -0300 Subject: [PATCH] Add the Image Color Palette node (#1311) * Add image color palette node * Add max size of palette * Code review cleanup --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers --- .../node_graph/node_graph_message_handler.rs | 3 + .../document_node_types.rs | 12 +++ .../node_properties.rs | 6 ++ editor/src/node_graph_executor.rs | 1 + .../gcore/src/graphic_element/renderer.rs | 20 +++++ node-graph/gcore/src/raster/adjustments.rs | 8 ++ node-graph/graph-craft/src/document/value.rs | 5 ++ node-graph/gstd/src/image_color_palette.rs | 83 +++++++++++++++++++ node-graph/gstd/src/lib.rs | 2 + .../interpreted-executor/src/node_registry.rs | 8 ++ 10 files changed, 148 insertions(+) create mode 100644 node-graph/gstd/src/image_color_palette.rs diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 24dac0d1a..9edbd17cc 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -38,6 +38,8 @@ pub enum FrontendGraphDataType { GraphicGroup, #[serde(rename = "artboard")] Artboard, + #[serde(rename = "palette")] + Palette, } impl FrontendGraphDataType { pub const fn with_tagged_value(value: &TaggedValue) -> Self { @@ -52,6 +54,7 @@ impl FrontendGraphDataType { TaggedValue::RcSubpath(_) | TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::Subpath, TaggedValue::GraphicGroup(_) => Self::GraphicGroup, TaggedValue::Artboard(_) => Self::Artboard, + TaggedValue::Palette(_) => Self::Palette, _ => Self::General, } } 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 a1b2d082f..a91001fb8 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 @@ -2323,6 +2323,18 @@ fn static_nodes() -> Vec { properties: node_properties::color_overlay_properties, ..Default::default() }, + DocumentNodeDefinition { + name: "Image Color Palette", + category: "Image Adjustments", + implementation: NodeImplementation::proto("graphene_std::image_color_palette::ImageColorPaletteNode<_>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Max Size", TaggedValue::U32(8), true), + ], + outputs: vec![DocumentOutputType::new("Colors", FrontendGraphDataType::Color)], + properties: node_properties::image_color_palette, + ..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 4149e942b..cde38c9b2 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 @@ -1904,3 +1904,9 @@ pub fn color_overlay_properties(document_node: &DocumentNode, node_id: NodeId, _ vec![color, blend_mode, LayoutGroup::Row { widgets: opacity }] } + +pub fn image_color_palette(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let size = number_widget(document_node, node_id, 1, "Max Size", NumberInput::default().int().min(1.).max(28.), true); + + vec![LayoutGroup::Row { widgets: size }] +} diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index d0fd843bc..e3a7a3701 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -716,6 +716,7 @@ impl NodeGraphExecutor { TaggedValue::OptionalColor(render_object) => Self::render(render_object, transform, responses), TaggedValue::VectorData(render_object) => Self::render(render_object, transform, responses), TaggedValue::ImageFrame(render_object) => Self::render(render_object, transform, responses), + TaggedValue::Palette(render_object) => Self::render(render_object, transform, responses), _ => { return Err(format!("Invalid node graph output type: {node_graph_output:#?}")); } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index b0b6f0c18..8cac0f3fb 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -617,6 +617,26 @@ impl GraphicElementRendered for Option { fn add_click_targets(&self, _click_targets: &mut Vec) {} } +impl GraphicElementRendered for Vec { + fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + for (index, &color) in self.iter().enumerate() { + render.leaf_tag("rect", |attributes| { + attributes.push("width", "100"); + attributes.push("height", "100"); + attributes.push("x", (index * 120).to_string()); + attributes.push("y", "40"); + attributes.push("fill", format!("#{}", color.rgba_hex())); + }); + } + } + + fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { + None + } + + fn add_click_targets(&self, _click_targets: &mut Vec) {} +} + /// A segment of an svg string to allow for embedding blob urls #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 8dfe9a741..729a0aaed 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1128,4 +1128,12 @@ mod index_node { ImageFrame::empty() } } + + #[node_macro::node_impl(IndexNode)] + pub fn index_node(input: Vec, index: u32) -> Option { + if index as usize >= input.len() { + warn!("Index of colors is out of range: index is {index} and length is {}", input.len()); + } + input.into_iter().nth(index as usize) + } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 5ec8deb45..0d711f88a 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -67,6 +67,7 @@ pub enum TaggedValue { SurfaceFrame(graphene_core::SurfaceFrame), Footprint(graphene_core::transform::Footprint), RenderOutput(RenderOutput), + Palette(Vec), } #[allow(clippy::derived_hash_with_manual_eq)] @@ -138,6 +139,7 @@ impl Hash for TaggedValue { Self::SurfaceFrame(surface_id) => surface_id.hash(state), Self::Footprint(footprint) => footprint.hash(state), Self::RenderOutput(render_output) => render_output.hash(state), + Self::Palette(palette) => palette.hash(state), } } } @@ -196,6 +198,7 @@ impl<'a> TaggedValue { TaggedValue::SurfaceFrame(x) => Box::new(x), TaggedValue::Footprint(x) => Box::new(x), TaggedValue::RenderOutput(x) => Box::new(x), + TaggedValue::Palette(x) => Box::new(x), } } @@ -265,6 +268,7 @@ impl<'a> TaggedValue { TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame), TaggedValue::Footprint(_) => concrete!(graphene_core::transform::Footprint), TaggedValue::RenderOutput(_) => concrete!(RenderOutput), + TaggedValue::Palette(_) => concrete!(Vec), } } @@ -326,6 +330,7 @@ impl<'a> TaggedValue { Ok(TaggedValue::SurfaceFrame(frame.into())) } x if x == TypeId::of::() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())), + x if x == TypeId::of::>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())), _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), } } diff --git a/node-graph/gstd/src/image_color_palette.rs b/node-graph/gstd/src/image_color_palette.rs new file mode 100644 index 000000000..790a076a1 --- /dev/null +++ b/node-graph/gstd/src/image_color_palette.rs @@ -0,0 +1,83 @@ +use graphene_core::raster::ImageFrame; +use graphene_core::Color; +use graphene_core::Node; + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ImageColorPaletteNode { + max_size: MaxSize, +} + +#[node_macro::node_fn(ImageColorPaletteNode)] +fn image_color_palette(frame: ImageFrame, max_size: u32) -> Vec { + const GRID: f32 = 3.0; + + let bins = GRID * GRID * GRID; + + let mut histogram: Vec = vec![0; (bins + 1.0) as usize]; + let mut colors: Vec> = vec![vec![]; (bins + 1.0) as usize]; + + for pixel in frame.image.data.iter() { + let r = pixel.r() * GRID; + let g = pixel.g() * GRID; + let b = pixel.b() * GRID; + + let bin = (r * GRID + g * GRID + b * GRID) as usize; + + histogram[bin] += 1; + colors[bin].push(pixel.to_gamma_srgb()); + } + + let shorted = histogram.iter().enumerate().filter(|(_, &count)| count > 0).map(|(i, _)| i).collect::>(); + + let mut palette = vec![]; + + for i in shorted.iter().take(max_size as usize) { + let list = colors[*i].clone(); + + let mut r = 0.0; + let mut g = 0.0; + let mut b = 0.0; + let mut a = 0.0; + + for color in list.iter() { + r += color.r(); + g += color.g(); + b += color.b(); + a += color.a(); + } + + r /= list.len() as f32; + g /= list.len() as f32; + b /= list.len() as f32; + a /= list.len() as f32; + + let color = Color::from_rgbaf32(r, g, b, a).unwrap(); + + palette.push(color); + } + + return palette; +} + +#[cfg(test)] +mod test { + use graphene_core::{raster::Image, value::CopiedNode}; + + use super::*; + + #[test] + fn test_image_color_palette() { + assert_eq!( + ImageColorPaletteNode { max_size: CopiedNode(1u32) }.eval(ImageFrame { + image: Image { + width: 100, + height: 100, + data: vec![Color::from_rgbaf32(0.0, 0.0, 0.0, 1.0).unwrap(); 10000], + }, + ..Default::default() + }), + [Color::from_rgbaf32(0.0, 0.0, 0.0, 1.0).unwrap()] + ); + } +} diff --git a/node-graph/gstd/src/lib.rs b/node-graph/gstd/src/lib.rs index c20fab7d4..b54f4af74 100644 --- a/node-graph/gstd/src/lib.rs +++ b/node-graph/gstd/src/lib.rs @@ -21,6 +21,8 @@ pub use graphene_core::*; pub mod image_segmentation; +pub mod image_color_palette; + pub mod brush; #[cfg(feature = "wasm")] diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 841cec078..082a10459 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -442,9 +442,11 @@ fn node_registry() -> HashMap, params: []), raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f32, f32, f32, f32, f32]), register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame, params: [ImageFrame]), + register_node!(graphene_std::image_color_palette::ImageColorPaletteNode<_>, input: ImageFrame, params: [u32]), 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]), + register_node!(graphene_core::raster::IndexNode<_>, input: Vec, params: [u32]), vec![( ProtoNodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"), |args| { @@ -561,8 +563,11 @@ fn node_registry() -> HashMap, input: WasmEditorApi, output: RenderOutput, params: [bool]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [String]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Option]), + async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ImageFrame]), + async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option]), + async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec]), async_node!( graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, @@ -666,6 +671,9 @@ fn node_registry() -> HashMap, input: WasmEditorApi, output: RenderOutput, fn_params: [() => f64, () => Arc]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => String, () => Arc]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Option, () => Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option, () => Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Vec, () => Arc]), + async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec, () => Arc]), //register_node!(graphene_core::transform::TranformNode<_, _, _, _, _, _>, input: , output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc]), vec![ (