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 <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers 2023-04-17 12:47:24 -07:00
parent a1b63811ba
commit 9db5ad43bf
6 changed files with 198 additions and 13 deletions

View file

@ -667,6 +667,40 @@ fn static_nodes() -> Vec<DocumentNodeType> {
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",

View file

@ -59,7 +59,7 @@ fn add_blank_assist(widgets: &mut Vec<WidgetHolder>) {
}
fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec<WidgetHolder> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
let map = text_widget(document_node, node_id, 1, "Map", true);

View file

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

View file

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

View file

@ -460,6 +460,73 @@ fn vibrance_node(color: Color, vibrance: f64) -> Color {
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChannelMixerNode<Monochrome, MonochromeR, MonochromeG, MonochromeB, MonochromeC, RedR, RedG, RedB, RedC, GreenR, GreenG, GreenB, GreenC, BlueR, BlueG, BlueB, BlueC> {
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<O> {
opacity_multiplier: O,

View file

@ -258,6 +258,10 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
raster_node!(graphene_core::raster::InvertRGBNode, params: []),
raster_node!(graphene_core::raster::ThresholdNode<_, _, _>, 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<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
];
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = 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
}