mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
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
6 changed files with 198 additions and 13 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue