mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-23 15:45:05 +00:00
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 <keavon@keavon.com>
This commit is contained in:
parent
fe4b9ef8bb
commit
cbda811480
10 changed files with 148 additions and 0 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2323,6 +2323,18 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
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()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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<LayoutGroup> {
|
||||
let size = number_widget(document_node, node_id, 1, "Max Size", NumberInput::default().int().min(1.).max(28.), true);
|
||||
|
||||
vec![LayoutGroup::Row { widgets: size }]
|
||||
}
|
||||
|
|
|
@ -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:#?}"));
|
||||
}
|
||||
|
|
|
@ -617,6 +617,26 @@ impl GraphicElementRendered for Option<Color> {
|
|||
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for Vec<Color> {
|
||||
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<ClickTarget>) {}
|
||||
}
|
||||
|
||||
/// A segment of an svg string to allow for embedding blob urls
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SvgSegment {
|
||||
|
|
|
@ -1128,4 +1128,12 @@ mod index_node {
|
|||
ImageFrame::empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node_impl(IndexNode)]
|
||||
pub fn index_node(input: Vec<Color>, index: u32) -> Option<Color> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ pub enum TaggedValue {
|
|||
SurfaceFrame(graphene_core::SurfaceFrame),
|
||||
Footprint(graphene_core::transform::Footprint),
|
||||
RenderOutput(RenderOutput),
|
||||
Palette(Vec<Color>),
|
||||
}
|
||||
|
||||
#[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<Color>),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,6 +330,7 @@ impl<'a> TaggedValue {
|
|||
Ok(TaggedValue::SurfaceFrame(frame.into()))
|
||||
}
|
||||
x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
||||
}
|
||||
}
|
||||
|
|
83
node-graph/gstd/src/image_color_palette.rs
Normal file
83
node-graph/gstd/src/image_color_palette.rs
Normal file
|
@ -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<MaxSize> {
|
||||
max_size: MaxSize,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(ImageColorPaletteNode)]
|
||||
fn image_color_palette(frame: ImageFrame<Color>, max_size: u32) -> Vec<Color> {
|
||||
const GRID: f32 = 3.0;
|
||||
|
||||
let bins = GRID * GRID * GRID;
|
||||
|
||||
let mut histogram: Vec<usize> = vec![0; (bins + 1.0) as usize];
|
||||
let mut colors: Vec<Vec<Color>> = 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::<Vec<usize>>();
|
||||
|
||||
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()]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ pub use graphene_core::*;
|
|||
|
||||
pub mod image_segmentation;
|
||||
|
||||
pub mod image_color_palette;
|
||||
|
||||
pub mod brush;
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
|
|
|
@ -442,9 +442,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
raster_node!(graphene_core::raster::ExtractOpaqueNode<>, params: []),
|
||||
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_std::image_color_palette::ImageColorPaletteNode<_>, input: ImageFrame<Color>, params: [u32]),
|
||||
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]),
|
||||
register_node!(graphene_core::raster::IndexNode<_>, input: Vec<Color>, params: [u32]),
|
||||
vec![(
|
||||
ProtoNodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"),
|
||||
|args| {
|
||||
|
@ -561,8 +563,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::EndLetNode<_, _>, 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<Color>]),
|
||||
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec<Color>]),
|
||||
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<Color>]),
|
||||
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>]),
|
||||
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>]),
|
||||
async_node!(
|
||||
graphene_core::memo::EndLetNode<_, _>,
|
||||
input: WasmEditorApi,
|
||||
|
@ -666,6 +671,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => f64, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => String, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Option<Color>, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Vec<Color>, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>, () => Arc<WasmSurfaceHandle>]),
|
||||
//register_node!(graphene_core::transform::TranformNode<_, _, _, _, _, _>, input: , output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<WasmSurfaceHandle>]),
|
||||
vec![
|
||||
(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue