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:
parent
a1b63811ba
commit
9db5ad43bf
|
|
@ -667,6 +667,40 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
||||||
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||||
properties: node_properties::adjust_vibrance_properties,
|
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 {
|
DocumentNodeType {
|
||||||
name: "Opacity",
|
name: "Opacity",
|
||||||
category: "Image Adjustments",
|
category: "Image Adjustments",
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
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![
|
let mut widgets = vec![
|
||||||
expose_widget(node_id, index, data_type, input.is_exposed()),
|
expose_widget(node_id, index, data_type, input.is_exposed()),
|
||||||
WidgetHolder::unrelated_separator(),
|
WidgetHolder::unrelated_separator(),
|
||||||
|
|
@ -68,6 +68,7 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na
|
||||||
if blank_assist {
|
if blank_assist {
|
||||||
add_blank_assist(&mut widgets);
|
add_blank_assist(&mut widgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
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 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 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);
|
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 }]
|
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")]
|
#[cfg(feature = "gpu")]
|
||||||
pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
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);
|
let map = text_widget(document_node, node_id, 1, "Map", true);
|
||||||
|
|
|
||||||
|
|
@ -336,12 +336,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function doubleClick(e: MouseEvent) {
|
function doubleClick(e: MouseEvent) {
|
||||||
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
// const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||||
const nodeId = node?.getAttribute("data-node") || undefined;
|
// const nodeId = node?.getAttribute("data-node") || undefined;
|
||||||
if (nodeId) {
|
// if (nodeId) {
|
||||||
const id = BigInt(nodeId);
|
// const id = BigInt(nodeId);
|
||||||
editor.instance.doubleClickNode(id);
|
// editor.instance.doubleClickNode(id);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointerMove(e: PointerEvent) {
|
function pointerMove(e: PointerEvent) {
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,9 @@
|
||||||
function onCancelTextChange() {
|
function onCancelTextChange() {
|
||||||
updateValue(undefined, min, max, displayDecimalPlaces, unit);
|
updateValue(undefined, min, max, displayDecimalPlaces, unit);
|
||||||
|
|
||||||
|
rangeSliderValue = value;
|
||||||
|
rangeSliderValueAsRendered = value;
|
||||||
|
|
||||||
editing = false;
|
editing = false;
|
||||||
|
|
||||||
self?.unFocus();
|
self?.unFocus();
|
||||||
|
|
@ -203,11 +206,16 @@
|
||||||
|
|
||||||
function updateValue(newValue: number | undefined, min: number | undefined, max: number | undefined, displayDecimalPlaces: number, unit: string) {
|
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)
|
// 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;
|
const oldValue = value !== undefined && isInteger ? Math.round(value) : value;
|
||||||
let cleaned = newValue !== undefined ? newValue : nowValid;
|
let cleaned = newValue !== undefined ? newValue : oldValue;
|
||||||
|
|
||||||
if (typeof min === "number" && !Number.isNaN(min) && cleaned !== undefined) cleaned = Math.max(cleaned, min);
|
if (cleaned !== undefined) {
|
||||||
if (typeof max === "number" && !Number.isNaN(max) && cleaned !== undefined) cleaned = Math.min(cleaned, max);
|
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);
|
text = displayText(cleaned, displayDecimalPlaces, unit);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct OpacityNode<O> {
|
pub struct OpacityNode<O> {
|
||||||
opacity_multiplier: O,
|
opacity_multiplier: O,
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,10 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||||
raster_node!(graphene_core::raster::InvertRGBNode, params: []),
|
raster_node!(graphene_core::raster::InvertRGBNode, params: []),
|
||||||
raster_node!(graphene_core::raster::ThresholdNode<_, _, _>, params: [f64, f64, LuminanceCalculation]),
|
raster_node!(graphene_core::raster::ThresholdNode<_, _, _>, params: [f64, f64, LuminanceCalculation]),
|
||||||
raster_node!(graphene_core::raster::VibranceNode<_>, params: [f64]),
|
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![(
|
vec![(
|
||||||
NodeIdentifier::new("graphene_core::raster::BrightnessContrastNode<_, _, _>"),
|
NodeIdentifier::new("graphene_core::raster::BrightnessContrastNode<_, _, _>"),
|
||||||
|args| {
|
|args| {
|
||||||
|
|
@ -483,7 +487,12 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||||
];
|
];
|
||||||
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
||||||
for (id, c, types) in node_types.into_iter().flatten() {
|
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
|
map
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue