From 9db5ad43bf07ce9997b5d4c8a17d9233e1a5a349 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 17 Apr 2023 12:47:24 -0700 Subject: [PATCH] Add the Channel Mixer node (#1142) * Add the Channel Mixer node * Fix NodeIdentifier not found in Registry * Add radio toggle for red/green/blue --------- Co-authored-by: Dennis Kobert --- .../document_node_types.rs | 34 +++++++++ .../node_properties.rs | 71 ++++++++++++++++++- .../src/components/panels/NodeGraph.svelte | 12 ++-- .../widgets/inputs/NumberInput.svelte | 16 +++-- node-graph/gcore/src/raster/adjustments.rs | 67 +++++++++++++++++ .../interpreted-executor/src/node_registry.rs | 11 ++- 6 files changed, 198 insertions(+), 13 deletions(-) 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 84e2f2ad..5dce9e8f 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 @@ -667,6 +667,40 @@ fn static_nodes() -> Vec { outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::adjust_vibrance_properties, }, + DocumentNodeType { + name: "Channel Mixer", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_core::raster::ChannelMixerNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), + // Monochrome toggle + DocumentInputType::value("Monochrome", TaggedValue::Bool(false), false), + // Monochrome + DocumentInputType::value("Red", TaggedValue::F64(40.), false), + DocumentInputType::value("Green", TaggedValue::F64(40.), false), + DocumentInputType::value("Blue", TaggedValue::F64(20.), false), + DocumentInputType::value("Constant", TaggedValue::F64(0.), false), + // Red output channel + DocumentInputType::value("(Red) Red", TaggedValue::F64(100.), false), + DocumentInputType::value("(Red) Green", TaggedValue::F64(0.), false), + DocumentInputType::value("(Red) Blue", TaggedValue::F64(0.), false), + DocumentInputType::value("(Red) Constant", TaggedValue::F64(0.), false), + // Green output channel + DocumentInputType::value("(Green) Red", TaggedValue::F64(0.), false), + DocumentInputType::value("(Green) Green", TaggedValue::F64(100.), false), + DocumentInputType::value("(Green) Blue", TaggedValue::F64(0.), false), + DocumentInputType::value("(Green) Constant", TaggedValue::F64(0.), false), + // Blue output channel + DocumentInputType::value("(Blue) Red", TaggedValue::F64(0.), false), + DocumentInputType::value("(Blue) Green", TaggedValue::F64(0.), false), + DocumentInputType::value("(Blue) Blue", TaggedValue::F64(100.), false), + DocumentInputType::value("(Blue) Constant", TaggedValue::F64(0.), false), + // Display-only properties (not used within the node) + DocumentInputType::value("Output Channel", TaggedValue::U32(0), false), + ], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::adjust_channel_mixer_properties, + }, DocumentNodeType { name: "Opacity", category: "Image Adjustments", 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 1e485d0e..e7aec70d 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 @@ -59,7 +59,7 @@ fn add_blank_assist(widgets: &mut Vec) { } fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec { - let input = document_node.inputs.get(index).unwrap(); + let input = document_node.inputs.get(index).expect("A widget failed to be built because its node's input index is invalid."); let mut widgets = vec![ expose_widget(node_id, index, data_type, input.is_exposed()), WidgetHolder::unrelated_separator(), @@ -68,6 +68,7 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na if blank_assist { add_blank_assist(&mut widgets); } + widgets } @@ -523,7 +524,7 @@ pub fn blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _con pub fn brush_node_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let color = color_widget(document_node, node_id, 5, "Color", ColorInput::default(), true); - let size = number_widget(document_node, node_id, 2, "Diameter", NumberInput::default().min(0.).max(100.).unit(" px"), true); + let size = number_widget(document_node, node_id, 2, "Diameter", NumberInput::default().min(1.).max(100.).unit(" px"), true); let hardness = number_widget(document_node, node_id, 3, "Hardness", NumberInput::default().min(0.).max(100.).unit("%"), true); let flow = number_widget(document_node, node_id, 4, "Flow", NumberInput::default().min(1.).max(100.).unit("%"), true); @@ -544,6 +545,72 @@ pub fn adjust_vibrance_properties(document_node: &DocumentNode, node_id: NodeId, vec![LayoutGroup::Row { widgets: vibrance }] } +pub fn adjust_channel_mixer_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let monochrome_index = 1; + let monochrome = bool_widget(document_node, node_id, monochrome_index, "Monochrome", true); + let is_monochrome = if let &NodeInput::Value { + tagged_value: TaggedValue::Bool(monochrome_choice), + .. + } = &document_node.inputs[monochrome_index] + { + monochrome_choice + } else { + false + }; + + let output_channel_index = 18; + let mut output_channel = vec![WidgetHolder::text_widget("Output Channel"), WidgetHolder::unrelated_separator()]; + add_blank_assist(&mut output_channel); + if let &NodeInput::Value { + tagged_value: TaggedValue::U32(red_green_blue_index), + exposed: false, + } = &document_node.inputs[output_channel_index] + { + let entries = [("Red", 0), ("Green", 1), ("Blue", 2)] + .into_iter() + .map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::U32(val), node_id, output_channel_index))) + .collect(); + output_channel.extend([RadioInput::new(entries).selected_index(red_green_blue_index).widget_holder()]); + }; + + let is_output_channel = if let &NodeInput::Value { + tagged_value: TaggedValue::U32(red_green_blue_index), + .. + } = &document_node.inputs[output_channel_index] + { + red_green_blue_index + } else { + warn!("Channel Mixer node properties panel could not be displayed."); + return vec![]; + }; + + let (r, g, b, c) = match (is_monochrome, is_output_channel) { + (true, _) => ((2, "Red", 40.), (3, "Green", 40.), (4, "Blue", 20.), (5, "Constant", 0.)), + (false, 0) => ((6, "(Red) Red", 100.), (7, "(Red) Green", 0.), (8, "(Red) Blue", 0.), (9, "(Red) Constant", 0.)), + (false, 1) => ((10, "(Green) Red", 0.), (11, "(Green) Green", 100.), (12, "(Green) Blue", 0.), (13, "(Green) Constant", 0.)), + (false, 2) => ((14, "(Blue) Red", 0.), (15, "(Blue) Green", 0.), (16, "(Blue) Blue", 100.), (17, "(Blue) Constant", 0.)), + _ => unreachable!(), + }; + + let red = number_widget(document_node, node_id, r.0, r.1, NumberInput::default().min(-200.).max(200.).value(Some(r.2)).unit("%"), true); + let green = number_widget(document_node, node_id, g.0, g.1, NumberInput::default().min(-200.).max(200.).value(Some(g.2)).unit("%"), true); + let blue = number_widget(document_node, node_id, b.0, b.1, NumberInput::default().min(-200.).max(200.).value(Some(b.2)).unit("%"), true); + let constant = number_widget(document_node, node_id, c.0, c.1, NumberInput::default().min(-200.).max(200.).value(Some(c.2)).unit("%"), true); + + let mut layout = vec![LayoutGroup::Row { widgets: monochrome }]; + if !is_monochrome { + layout.push(LayoutGroup::Row { widgets: output_channel }); + }; + layout.extend([ + // Gray output + LayoutGroup::Row { widgets: red }, + LayoutGroup::Row { widgets: green }, + LayoutGroup::Row { widgets: blue }, + LayoutGroup::Row { widgets: constant }, + ]); + layout +} + #[cfg(feature = "gpu")] pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let map = text_widget(document_node, node_id, 1, "Map", true); diff --git a/frontend/src/components/panels/NodeGraph.svelte b/frontend/src/components/panels/NodeGraph.svelte index df2a2305..0b2dddbc 100644 --- a/frontend/src/components/panels/NodeGraph.svelte +++ b/frontend/src/components/panels/NodeGraph.svelte @@ -336,12 +336,12 @@ } function doubleClick(e: MouseEvent) { - const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined; - const nodeId = node?.getAttribute("data-node") || undefined; - if (nodeId) { - const id = BigInt(nodeId); - editor.instance.doubleClickNode(id); - } + // const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined; + // const nodeId = node?.getAttribute("data-node") || undefined; + // if (nodeId) { + // const id = BigInt(nodeId); + // editor.instance.doubleClickNode(id); + // } } function pointerMove(e: PointerEvent) { diff --git a/frontend/src/components/widgets/inputs/NumberInput.svelte b/frontend/src/components/widgets/inputs/NumberInput.svelte index 2b840720..e5a2ab5a 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.svelte +++ b/frontend/src/components/widgets/inputs/NumberInput.svelte @@ -174,6 +174,9 @@ function onCancelTextChange() { updateValue(undefined, min, max, displayDecimalPlaces, unit); + rangeSliderValue = value; + rangeSliderValueAsRendered = value; + editing = false; self?.unFocus(); @@ -203,11 +206,16 @@ function updateValue(newValue: number | undefined, min: number | undefined, max: number | undefined, displayDecimalPlaces: number, unit: string) { // Check if the new value is valid, otherwise we use the old value (rounded if it's an integer) - const nowValid = value !== undefined && isInteger ? Math.round(value) : value; - let cleaned = newValue !== undefined ? newValue : nowValid; + const oldValue = value !== undefined && isInteger ? Math.round(value) : value; + let cleaned = newValue !== undefined ? newValue : oldValue; - if (typeof min === "number" && !Number.isNaN(min) && cleaned !== undefined) cleaned = Math.max(cleaned, min); - if (typeof max === "number" && !Number.isNaN(max) && cleaned !== undefined) cleaned = Math.min(cleaned, max); + if (cleaned !== undefined) { + if (typeof min === "number" && !Number.isNaN(min)) cleaned = Math.max(cleaned, min); + if (typeof max === "number" && !Number.isNaN(max)) cleaned = Math.min(cleaned, max); + + rangeSliderValue = cleaned; + rangeSliderValueAsRendered = cleaned; + } text = displayText(cleaned, displayDecimalPlaces, unit); diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index b33c03f0..f7773fc0 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -460,6 +460,73 @@ fn vibrance_node(color: Color, vibrance: f64) -> Color { } } +#[derive(Debug, Clone, Copy)] +pub struct ChannelMixerNode { + monochrome: Monochrome, + monochrome_r: MonochromeR, + monochrome_g: MonochromeG, + monochrome_b: MonochromeB, + monochrome_c: MonochromeC, + red_r: RedR, + red_g: RedG, + red_b: RedB, + red_c: RedC, + green_r: GreenR, + green_g: GreenG, + green_b: GreenB, + green_c: GreenC, + blue_r: BlueR, + blue_g: BlueG, + blue_b: BlueB, + blue_c: BlueC, +} + +#[node_macro::node_fn(ChannelMixerNode)] +fn channel_mixer_node( + color: Color, + monochrome: bool, + monochrome_r: f64, + monochrome_g: f64, + monochrome_b: f64, + monochrome_c: f64, + red_r: f64, + red_g: f64, + red_b: f64, + red_c: f64, + green_r: f64, + green_g: f64, + green_b: f64, + green_c: f64, + blue_r: f64, + blue_g: f64, + blue_b: f64, + blue_c: f64, +) -> Color { + let color = color.to_gamma_srgb(); + + let (r, g, b, a) = color.components(); + + let color = if monochrome { + let (monochrome_r, monochrome_g, monochrome_b, monochrome_c) = (monochrome_r as f32 / 100., monochrome_g as f32 / 100., monochrome_b as f32 / 100., monochrome_c as f32 / 100.); + + let gray = (r * monochrome_r + g * monochrome_g + b * monochrome_b + monochrome_c).clamp(0., 1.); + + Color::from_rgbaf32_unchecked(gray, gray, gray, a) + } else { + let (red_r, red_g, red_b, red_c) = (red_r as f32 / 100., red_g as f32 / 100., red_b as f32 / 100., red_c as f32 / 100.); + let (green_r, green_g, green_b, green_c) = (green_r as f32 / 100., green_g as f32 / 100., green_b as f32 / 100., green_c as f32 / 100.); + let (blue_r, blue_g, blue_b, blue_c) = (blue_r as f32 / 100., blue_g as f32 / 100., blue_b as f32 / 100., blue_c as f32 / 100.); + + let red = (r * red_r + g * red_g + b * red_b + red_c).clamp(0., 1.); + let green = (r * green_r + g * green_g + b * green_b + green_c).clamp(0., 1.); + let blue = (r * blue_r + g * blue_g + b * blue_b + blue_c).clamp(0., 1.); + + Color::from_rgbaf32_unchecked(red, green, blue, a) + }; + + color.to_linear_srgb() +} + #[derive(Debug, Clone, Copy)] pub struct OpacityNode { opacity_multiplier: O, diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 4fc7fe73..1cf093d2 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -258,6 +258,10 @@ fn node_registry() -> HashMap, params: [f64, f64, LuminanceCalculation]), raster_node!(graphene_core::raster::VibranceNode<_>, params: [f64]), + raster_node!( + graphene_core::raster::ChannelMixerNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _>, + params: [bool, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64] + ), vec![( NodeIdentifier::new("graphene_core::raster::BrightnessContrastNode<_, _, _>"), |args| { @@ -483,7 +487,12 @@ fn node_registry() -> HashMap> = HashMap::new(); for (id, c, types) in node_types.into_iter().flatten() { - map.entry(id).or_default().insert(types.clone(), c); + // TODO: this is a hack to remove the newline from the node new_name + // This occurs for the ChannelMixerNode presumably because of the long name. + // This might be caused by the stringify! macro + let new_name = id.name.replace('\n', " "); + let nid = NodeIdentifier { name: Cow::Owned(new_name) }; + map.entry(nid).or_default().insert(types.clone(), c); } map }