Use the SpectrumInput widget for the adjustment node Properties panel layouts (#4105)

This commit is contained in:
Keavon Chambers 2026-05-05 03:42:44 -07:00 committed by GitHub
parent e59612c4ce
commit 9db91a1ac4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 526 additions and 94 deletions

View File

@ -354,6 +354,22 @@ impl ColorPickerMessageHandler {
SpectrumInputUpdate::ResetMidpoint { index } => {
gradient.reset_midpoint(index as usize);
}
SpectrumInputUpdate::ResetMarker { index } => {
let i = index as usize;
let count = gradient.position.len();
if i >= count {
return;
}
// Each stop's "natural" position is its evenly-spaced fraction along 0..1, e.g., for 5 stops: 0, 0.25, 0.5, 0.75, 1. Falls back to the midpoint between neighbors when the natural position would push the stop past another.
let left = if i == 0 { 0. } else { gradient.position[i - 1] };
let right = gradient.position.get(i + 1).copied().unwrap_or(1.);
let natural = if count <= 1 { 0. } else { i as f64 / (count - 1) as f64 };
let new_position = if (left..=right).contains(&natural) { natural } else { (left + right) / 2. };
let new_index = gradient.move_stop(i, new_position);
if Some(index) == self.active_marker_index {
self.active_marker_index = Some(new_index as u32);
}
}
SpectrumInputUpdate::ActiveMarker { .. } => unreachable!("handled above"),
}
@ -393,7 +409,7 @@ impl ColorPickerMessageHandler {
.show_midpoints(true)
.allow_insert(!self.disabled)
.allow_delete(!self.disabled)
.allow_swap(true)
.allow_reorder(true)
.disabled(self.disabled)
.on_update(|update: &SpectrumInputUpdate| ColorPickerMessage::GradientUpdate { update: update.clone() }.into())
.widget_instance(),

View File

@ -584,8 +584,10 @@ pub struct SpectrumInput {
#[serde(rename = "allowDelete")]
pub allow_delete: bool,
/// Whether dragging a marker past another reorders them. If false, the dragged marker is clamped between its neighbors.
#[serde(rename = "allowSwap")]
pub allow_swap: bool,
#[serde(rename = "allowReorder")]
pub allow_reorder: bool,
/// Compact mode: 8px track height with 8px top padding, for use in rows alongside other widgets.
pub narrow: bool,
/// Whether the input is disabled (dimmed and read-only).
pub disabled: bool,
@ -634,6 +636,10 @@ pub enum SpectrumInputUpdate {
DeleteMarker {
index: u32,
},
/// Emitted when the user double-clicks a marker. The consumer decides what (if anything) to reset the marker to.
ResetMarker {
index: u32,
},
ResetMidpoint {
index: u32,
},

View File

@ -1535,6 +1535,11 @@ fn static_node_properties() -> NodeProperties {
let mut map: NodeProperties = HashMap::new();
map.insert("brightness_contrast_properties".to_string(), Box::new(node_properties::brightness_contrast_properties));
map.insert("channel_mixer_properties".to_string(), Box::new(node_properties::channel_mixer_properties));
map.insert("levels_properties".to_string(), Box::new(node_properties::levels_properties));
map.insert("hue_saturation_properties".to_string(), Box::new(node_properties::hue_saturation_properties));
map.insert("black_and_white_properties".to_string(), Box::new(node_properties::black_and_white_properties));
map.insert("threshold_properties".to_string(), Box::new(node_properties::threshold_properties));
map.insert("vibrance_properties".to_string(), Box::new(node_properties::vibrance_properties));
map.insert("fill_properties".to_string(), Box::new(node_properties::fill_properties));
map.insert("stroke_properties".to_string(), Box::new(node_properties::stroke_properties));
map.insert("offset_path_properties".to_string(), Box::new(node_properties::offset_path_properties));

View File

@ -1276,57 +1276,434 @@ pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesCo
})
}
/// 2-stop black-to-white gradient track for spectrum sliders that map a value to a grayscale axis.
fn bw_track() -> GradientStops {
GradientStops {
position: vec![0., 1.],
midpoint: vec![0.5, 0.5],
color: vec![Color::BLACK, Color::WHITE],
}
}
/// 3-stop black-to-color-to-white gradient track for spectrum sliders that map a value to a hue's full luminance range.
fn color_track(color: Color) -> GradientStops {
GradientStops {
position: vec![0., 0.5, 1.],
midpoint: vec![0.5; 3],
color: vec![Color::BLACK, color, Color::WHITE],
}
}
pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::brightness_contrast::*;
// Use Classic
let use_classic = bool_widget(ParameterWidgetsInfo::new(node_id, UseClassicInput::INDEX, true, context), CheckboxInput::default());
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
log::error!("Could not get document node in brightness_contrast_properties: {err}");
return Vec::new();
}
};
let use_classic_value = document_node.inputs.get(UseClassicInput::INDEX);
// Use Classic toggle changes the brightness range
let use_classic_value = get_document_node(node_id, context)
.ok()
.and_then(|document_node| document_node.inputs.get(UseClassicInput::INDEX).and_then(|input| input.as_value()))
.and_then(|tagged| if let TaggedValue::Bool(value) = tagged { Some(*value) } else { None });
let includes_use_classic = use_classic_value.is_some();
let use_classic_value = match use_classic_value.and_then(|input| input.as_value()) {
Some(TaggedValue::Bool(use_classic_choice)) => *use_classic_choice,
_ => false,
let use_classic_value = use_classic_value.unwrap_or(false);
let brightness_min = if use_classic_value { -100. } else { -150. };
let brightness_max = if use_classic_value { 100. } else { 150. };
let brightness = spectrum_slider_row(
node_id,
context,
BrightnessInput::INDEX,
bw_track(),
Color::WHITE,
brightness_min,
brightness_max,
0.,
NumberInput::default().mode_increment().unit("%").min(brightness_min).max(brightness_max),
);
let contrast_min = if use_classic_value { -100. } else { -50. };
let zero_position = -contrast_min / (100. - contrast_min);
let contrast_track = GradientStops {
position: vec![0., zero_position, 1.],
midpoint: vec![0.5; 3],
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::BLACK, Color::from_rgbf32_unchecked(0.5, 0.5, 0.5)],
};
// Brightness
let brightness = number_widget(
ParameterWidgetsInfo::new(node_id, BrightnessInput::INDEX, true, context),
NumberInput::default()
.unit("%")
.mode_range()
.display_decimal_places(2)
.range_min(Some(if use_classic_value { -100. } else { -150. }))
.range_max(Some(if use_classic_value { 100. } else { 150. })),
let contrast = spectrum_slider_row(
node_id,
context,
ContrastInput::INDEX,
contrast_track,
Color::WHITE,
contrast_min,
100.,
0.,
NumberInput::default().mode_increment().unit("%").min(contrast_min).max(100.),
);
// Contrast
let contrast = number_widget(
ParameterWidgetsInfo::new(node_id, ContrastInput::INDEX, true, context),
NumberInput::default()
.unit("%")
.mode_range()
.display_decimal_places(2)
.range_min(Some(if use_classic_value { -100. } else { -50. }))
.range_max(Some(100.)),
);
let mut layout = vec![LayoutGroup::row(brightness), LayoutGroup::row(contrast)];
let mut layout = vec![brightness, contrast];
if includes_use_classic {
// TODO: When we no longer use this function in the temporary "Brightness/Contrast Classic" node, remove this conditional pushing and just always include this
let use_classic = bool_widget(ParameterWidgetsInfo::new(node_id, UseClassicInput::INDEX, true, context), CheckboxInput::default());
layout.push(LayoutGroup::row(use_classic));
}
layout
}
pub(crate) fn levels_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::levels::*;
// (input index, marker handle color, default percentage for double-click reset)
let input_range_params = [
(ShadowsInput::INDEX, Color::BLACK, 0.),
(MidtonesInput::INDEX, Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), 50.),
(HighlightsInput::INDEX, Color::WHITE, 100.),
];
let output_range_params = [(OutputMinimumsInput::INDEX, Color::BLACK, 0.), (OutputMaximumsInput::INDEX, Color::WHITE, 100.)];
let mut layout = Vec::with_capacity(5);
build_shared_spectrum_section(node_id, context, &input_range_params, &mut layout);
build_shared_spectrum_section(node_id, context, &output_range_params, &mut layout);
layout
}
/// Append a section of related percentage parameters as rows: a shared black-to-white spectrum (with one marker per non-exposed parameter) sits on the first non-exposed row
/// alongside its 60px number input, and the remaining non-exposed rows show only their 60px number input. Exposed parameters render as the standard exposed-row display.
/// Marker positions are clamped to non-decreasing display order so they never visually cross even if the underlying values do.
fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesContext, params: &[(usize, Color, f64)], layout: &mut Vec<LayoutGroup>) {
// Snapshot exposure and values before the mutable-borrow loop
let exposure_and_value: Vec<(bool, f64)> = match get_document_node(node_id, context) {
Ok(document_node) => params
.iter()
.map(|&(input_index, _, _)| {
let input = document_node.inputs.get(input_index);
let exposed = input.is_some_and(|input| input.is_exposed());
let percent = input
.and_then(|input| input.as_value())
.and_then(|tagged| if let TaggedValue::F32(value) = tagged { Some(*value as f64) } else { None })
.unwrap_or(0.);
(exposed, percent)
})
.collect(),
Err(err) => {
log::error!("Could not get document node in build_shared_spectrum_section: {err}");
return;
}
};
// Build markers for all non-exposed params
let mut marker_input_indices = Vec::new();
let mut marker_default_percents = Vec::new();
let mut marker_positions = Vec::new();
let mut handle_colors = Vec::new();
for (i, &(input_index, handle_color, default_percent)) in params.iter().enumerate() {
let (exposed, percent) = exposure_and_value[i];
if exposed {
continue;
}
marker_positions.push((percent / 100.).clamp(0., 1.));
marker_input_indices.push(input_index);
marker_default_percents.push(default_percent);
handle_colors.push(handle_color);
}
// Enforce non-decreasing order so markers never visually cross, matching the node's algorithm where shadows takes precedence
for i in 1..marker_positions.len() {
marker_positions[i] = marker_positions[i].max(marker_positions[i - 1]);
}
let spectrum_markers: Vec<SpectrumMarker> = marker_positions
.iter()
.zip(&handle_colors)
.map(|(&position, &handle_color)| SpectrumMarker::new(position, 0.5, handle_color))
.collect();
// Build the shared spectrum widget (placed on the first non-exposed row)
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
SpectrumInput::new(bw_track())
.markers(spectrum_markers)
.show_midpoints(false)
.allow_insert(false)
.allow_delete(false)
.allow_reorder(false)
.narrow(true)
.on_update({
let marker_input_indices = marker_input_indices.clone();
let marker_default_percents = marker_default_percents.clone();
let marker_positions = marker_positions.clone();
move |update: &SpectrumInputUpdate| {
let (input_index, percent) = match update {
SpectrumInputUpdate::MoveMarker { index, position } => match marker_input_indices.get(*index as usize) {
Some(&input_index) => (input_index, *position * 100.),
None => return Message::NoOp,
},
SpectrumInputUpdate::ResetMarker { index } => {
let i = *index as usize;
let Some(&input_index) = marker_input_indices.get(i) else { return Message::NoOp };
let Some(&default_percent) = marker_default_percents.get(i) else { return Message::NoOp };
// Falls back to midpoint between neighbors if the default would cross one
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);
let default_position = default_percent / 100.;
let new_position = if (left..=right).contains(&default_position) { default_position } else { (left + right) / 2. };
(input_index, new_position * 100.)
}
_ => return Message::NoOp,
};
NodeGraphMessage::SetInputValue {
node_id,
input_index,
value: TaggedValue::F32(percent.clamp(0., 100.) as f32),
}
.into()
}
})
.on_commit(commit_value)
.widget_instance()
});
let spectrum_owner = marker_input_indices.first().copied();
let number_input = NumberInput::default().mode_increment().unit("%").min(0.).max(100.);
// One row per parameter: first non-exposed carries the shared spectrum, others get just a number input
for (i, &(input_index, _, _)) in params.iter().enumerate() {
let (exposed, current) = exposure_and_value[i];
if exposed {
let row = number_widget(ParameterWidgetsInfo::new(node_id, input_index, true, context), number_input.clone());
layout.push(LayoutGroup::row(row));
} else {
let mut row = start_widgets(ParameterWidgetsInfo::new(node_id, input_index, true, context));
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
if Some(input_index) == spectrum_owner
&& let Some(spectrum) = &spectrum_widget
{
row.push(spectrum.clone());
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
}
row.push(
number_input
.clone()
.value(Some(current))
.min_width(60)
.max_width(60)
.display_decimal_places(0)
.on_update(update_value(move |widget: &NumberInput| TaggedValue::F32(widget.value.unwrap_or(0.) as f32), node_id, input_index))
.on_commit(commit_value)
.widget_instance(),
);
layout.push(LayoutGroup::row(row));
}
}
}
pub(crate) fn hue_saturation_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::hue_saturation::*;
// Current hue position on the rainbow track, used for the saturation track's right-end color
let current_hue_shift = get_document_node(node_id, context)
.ok()
.and_then(|document_node| document_node.inputs.get(HueShiftInput::INDEX).and_then(|input| input.as_value()))
.and_then(|tagged| if let TaggedValue::F32(value) = tagged { Some(*value) } else { None })
.unwrap_or(0.);
// The rainbow has cyan at position 0.5 (hue_shift=0), so offset by +180 to align
let marker_hue = ((current_hue_shift + 180.) / 360.).rem_euclid(1.);
let saturated_current_hue = Color::from_hsva(marker_hue, 1., 1., 1.);
// Hue: cyclic rainbow
let hue_track = GradientStops {
position: vec![0., 1. / 6., 2. / 6., 3. / 6., 4. / 6., 5. / 6., 1.],
midpoint: vec![0.5; 7],
color: vec![Color::RED, Color::YELLOW, Color::GREEN, Color::CYAN, Color::BLUE, Color::MAGENTA, Color::RED],
};
// Saturation: gray to the fully saturated current hue
let saturation_track = GradientStops {
position: vec![0., 1.],
midpoint: vec![0.5, 0.5],
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), saturated_current_hue],
};
// Lightness: black to white
let lightness_track = bw_track();
vec![
spectrum_slider_row(
node_id,
context,
HueShiftInput::INDEX,
hue_track,
Color::WHITE,
-180.,
180.,
0.,
NumberInput::default().mode_increment().unit("°").min(-180.).max(180.),
),
spectrum_slider_row(
node_id,
context,
SaturationShiftInput::INDEX,
saturation_track,
Color::WHITE,
-100.,
100.,
0.,
NumberInput::default().mode_increment().unit("%").min(-100.).max(100.),
),
spectrum_slider_row(
node_id,
context,
LightnessShiftInput::INDEX,
lightness_track,
Color::WHITE,
-100.,
100.,
0.,
NumberInput::default().mode_increment().unit("%").min(-100.).max(100.),
),
]
}
/// Build a row with a single-marker `SpectrumInput` and a 60px `NumberInput`. The marker maps `value_min..value_max` to position 0..1, and double-click resets to `default_value`.
fn spectrum_slider_row(
node_id: NodeId,
context: &mut NodePropertiesContext,
input_index: usize,
track: GradientStops,
handle_color: Color,
value_min: f64,
value_max: f64,
default_value: f64,
number_input: NumberInput,
) -> LayoutGroup {
let mut row = start_widgets(ParameterWidgetsInfo::new(node_id, input_index, true, context));
let current = get_document_node(node_id, context)
.ok()
.and_then(|document_node| document_node.inputs.get(input_index))
.and_then(|input| input.as_non_exposed_value())
.and_then(|tagged| if let TaggedValue::F32(value) = tagged { Some(*value as f64) } else { None });
// Only add the spectrum and number widgets when the input is not exposed
if let Some(current) = current {
let value_range = value_max - value_min;
let position = ((current - value_min) / value_range).clamp(0., 1.);
let default_position = ((default_value - value_min) / value_range).clamp(0., 1.);
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
let position_to_value = move |position: f64| value_min + position * value_range;
row.push(
SpectrumInput::new(track)
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
.show_midpoints(false)
.allow_insert(false)
.allow_delete(false)
.allow_reorder(false)
.narrow(true)
.on_update(move |update: &SpectrumInputUpdate| {
let new_position = match update {
SpectrumInputUpdate::MoveMarker { index: 0, position } => *position,
SpectrumInputUpdate::ResetMarker { index: 0 } => default_position,
_ => return Message::NoOp,
};
NodeGraphMessage::SetInputValue {
node_id,
input_index,
value: TaggedValue::F32(position_to_value(new_position).clamp(value_min, value_max) as f32),
}
.into()
})
.on_commit(commit_value)
.widget_instance(),
);
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
row.push(
number_input
.value(Some(current))
.min_width(60)
.max_width(60)
.display_decimal_places(0)
.on_update(update_value(move |widget: &NumberInput| TaggedValue::F32(widget.value.unwrap_or(0.) as f32), node_id, input_index))
.on_commit(commit_value)
.widget_instance(),
);
}
LayoutGroup::row(row)
}
pub(crate) fn threshold_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::threshold::*;
let params: &[(usize, Color, f64)] = &[(MinLuminanceInput::INDEX, Color::BLACK, 50.), (MaxLuminanceInput::INDEX, Color::WHITE, 100.)];
let mut layout = Vec::with_capacity(3);
build_shared_spectrum_section(node_id, context, params, &mut layout);
let luminance_calc = {
let mut info = ParameterWidgetsInfo::new(node_id, LuminanceCalcInput::INDEX, true, context);
info.exposable = false;
enum_choice::<LuminanceCalculation>().for_socket(info).property_row()
};
layout.push(luminance_calc);
layout
}
pub(crate) fn vibrance_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::vibrance::*;
let track = GradientStops {
position: vec![0., 1.],
midpoint: vec![0.5, 0.5],
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::RED],
};
vec![spectrum_slider_row(
node_id,
context,
VibranceInput::INDEX,
track,
Color::WHITE,
-100.,
100.,
0.,
NumberInput::default().mode_increment().unit("%").min(-100.).max(100.),
)]
}
pub(crate) fn black_and_white_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::black_and_white::*;
let number_input = NumberInput::default().mode_increment().unit("%").min(-200.).max(300.);
let tint = color_widget(ParameterWidgetsInfo::new(node_id, TintInput::INDEX, true, context), ColorInput::default());
let mut layout = vec![tint];
let params: &[(usize, Color, f64)] = &[
(RedsInput::INDEX, Color::RED, 40.),
(YellowsInput::INDEX, Color::YELLOW, 60.),
(GreensInput::INDEX, Color::GREEN, 40.),
(CyansInput::INDEX, Color::CYAN, 60.),
(BluesInput::INDEX, Color::BLUE, 20.),
(MagentasInput::INDEX, Color::MAGENTA, 80.),
];
for &(input_index, color, default) in params {
layout.push(spectrum_slider_row(
node_id,
context,
input_index,
color_track(color),
Color::WHITE,
-200.,
300.,
default,
number_input.clone(),
));
}
layout
}
pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::channel_mixer::*;
@ -1356,27 +1733,38 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
}
};
// Output Channel modes
let (red_output_index, green_output_index, blue_output_index, constant_output_index) = match (is_monochrome_value, output_channel_value) {
(true, _) => (MonochromeRInput::INDEX, MonochromeGInput::INDEX, MonochromeBInput::INDEX, MonochromeCInput::INDEX),
(false, RedGreenBlue::Red) => (RedRInput::INDEX, RedGInput::INDEX, RedBInput::INDEX, RedCInput::INDEX),
(false, RedGreenBlue::Green) => (GreenRInput::INDEX, GreenGInput::INDEX, GreenBInput::INDEX, GreenCInput::INDEX),
(false, RedGreenBlue::Blue) => (BlueRInput::INDEX, BlueGInput::INDEX, BlueBInput::INDEX, BlueCInput::INDEX),
// Input indices and defaults depend on monochrome toggle and output channel selection
let (indices, defaults) = match (is_monochrome_value, output_channel_value) {
(true, _) => (
[MonochromeRInput::INDEX, MonochromeGInput::INDEX, MonochromeBInput::INDEX, MonochromeCInput::INDEX],
[40., 40., 20., 0.],
),
(false, RedGreenBlue::Red) => ([RedRInput::INDEX, RedGInput::INDEX, RedBInput::INDEX, RedCInput::INDEX], [100., 0., 0., 0.]),
(false, RedGreenBlue::Green) => ([GreenRInput::INDEX, GreenGInput::INDEX, GreenBInput::INDEX, GreenCInput::INDEX], [0., 100., 0., 0.]),
(false, RedGreenBlue::Blue) => ([BlueRInput::INDEX, BlueGInput::INDEX, BlueBInput::INDEX, BlueCInput::INDEX], [0., 0., 100., 0.]),
};
let number_input = NumberInput::default().mode_range().min(-200.).max(200.).unit("%");
let red = number_widget(ParameterWidgetsInfo::new(node_id, red_output_index, true, context), number_input.clone());
let green = number_widget(ParameterWidgetsInfo::new(node_id, green_output_index, true, context), number_input.clone());
let blue = number_widget(ParameterWidgetsInfo::new(node_id, blue_output_index, true, context), number_input.clone());
let constant = number_widget(ParameterWidgetsInfo::new(node_id, constant_output_index, true, context), number_input);
// Monochrome
let number_input = NumberInput::default().mode_increment().unit("%").min(-200.).max(200.);
let tracks = [color_track(Color::RED), color_track(Color::GREEN), color_track(Color::BLUE), bw_track()];
let mut layout = vec![LayoutGroup::row(is_monochrome)];
// Output channel choice
if !is_monochrome_value {
layout.push(output_channel);
}
// Channel values
layout.extend([LayoutGroup::row(red), LayoutGroup::row(green), LayoutGroup::row(blue), LayoutGroup::row(constant)]);
for (i, (&input_index, &default)) in indices.iter().zip(defaults.iter()).enumerate() {
layout.push(spectrum_slider_row(
node_id,
context,
input_index,
tracks[i].clone(),
Color::WHITE,
-200.,
200.,
default,
number_input.clone(),
));
}
layout
}
@ -1403,39 +1791,43 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
}
};
// CMYK
let (c_index, m_index, y_index, k_index) = match colors_choice {
SelectiveColorChoice::Reds => (RCInput::INDEX, RMInput::INDEX, RYInput::INDEX, RKInput::INDEX),
SelectiveColorChoice::Yellows => (YCInput::INDEX, YMInput::INDEX, YYInput::INDEX, YKInput::INDEX),
SelectiveColorChoice::Greens => (GCInput::INDEX, GMInput::INDEX, GYInput::INDEX, GKInput::INDEX),
SelectiveColorChoice::Cyans => (CCInput::INDEX, CMInput::INDEX, CYInput::INDEX, CKInput::INDEX),
SelectiveColorChoice::Blues => (BCInput::INDEX, BMInput::INDEX, BYInput::INDEX, BKInput::INDEX),
SelectiveColorChoice::Magentas => (MCInput::INDEX, MMInput::INDEX, MYInput::INDEX, MKInput::INDEX),
SelectiveColorChoice::Whites => (WCInput::INDEX, WMInput::INDEX, WYInput::INDEX, WKInput::INDEX),
SelectiveColorChoice::Neutrals => (NCInput::INDEX, NMInput::INDEX, NYInput::INDEX, NKInput::INDEX),
SelectiveColorChoice::Blacks => (KCInput::INDEX, KMInput::INDEX, KYInput::INDEX, KKInput::INDEX),
let indices = match colors_choice {
SelectiveColorChoice::Reds => [RCInput::INDEX, RMInput::INDEX, RYInput::INDEX, RKInput::INDEX],
SelectiveColorChoice::Yellows => [YCInput::INDEX, YMInput::INDEX, YYInput::INDEX, YKInput::INDEX],
SelectiveColorChoice::Greens => [GCInput::INDEX, GMInput::INDEX, GYInput::INDEX, GKInput::INDEX],
SelectiveColorChoice::Cyans => [CCInput::INDEX, CMInput::INDEX, CYInput::INDEX, CKInput::INDEX],
SelectiveColorChoice::Blues => [BCInput::INDEX, BMInput::INDEX, BYInput::INDEX, BKInput::INDEX],
SelectiveColorChoice::Magentas => [MCInput::INDEX, MMInput::INDEX, MYInput::INDEX, MKInput::INDEX],
SelectiveColorChoice::Whites => [WCInput::INDEX, WMInput::INDEX, WYInput::INDEX, WKInput::INDEX],
SelectiveColorChoice::Neutrals => [NCInput::INDEX, NMInput::INDEX, NYInput::INDEX, NKInput::INDEX],
SelectiveColorChoice::Blacks => [KCInput::INDEX, KMInput::INDEX, KYInput::INDEX, KKInput::INDEX],
};
let number_input = NumberInput::default().mode_range().min(-100.).max(100.).unit("%");
let cyan = number_widget(ParameterWidgetsInfo::new(node_id, c_index, true, context), number_input.clone());
let magenta = number_widget(ParameterWidgetsInfo::new(node_id, m_index, true, context), number_input.clone());
let yellow = number_widget(ParameterWidgetsInfo::new(node_id, y_index, true, context), number_input.clone());
let black = number_widget(ParameterWidgetsInfo::new(node_id, k_index, true, context), number_input);
let tracks = [color_track(Color::CYAN), color_track(Color::MAGENTA), color_track(Color::YELLOW), bw_track()];
let number_input = NumberInput::default().mode_increment().unit("%").min(-100.).max(100.);
// Mode
let mode = enum_choice::<RelativeAbsolute>()
.for_socket(ParameterWidgetsInfo::new(node_id, ModeInput::INDEX, true, context))
.property_row();
vec![
// Colors choice
colors,
// CMYK
LayoutGroup::row(cyan),
LayoutGroup::row(magenta),
LayoutGroup::row(yellow),
LayoutGroup::row(black),
// Mode
mode,
]
let mut layout = vec![colors];
for (i, &input_index) in indices.iter().enumerate() {
layout.push(spectrum_slider_row(
node_id,
context,
input_index,
tracks[i].clone(),
Color::WHITE,
-100.,
100.,
0.,
number_input.clone(),
));
}
layout.push(mode);
layout
}
pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {

View File

@ -179,6 +179,10 @@
> .radio-input button {
flex: 1 1 100%;
}
> .parameter-expose-button + .text-label ~ .number-input:last-child {
margin-left: auto;
}
}
}
}

View File

@ -19,7 +19,8 @@
export let showMidpoints = true;
export let allowInsert = true;
export let allowDelete = true;
export let allowSwap = true;
export let allowReorder = true;
export let narrow = false;
export let disabled = false;
// Reference to the marker track DOM element so we can convert pointer coordinates to a 0..1 position along the track.
@ -94,6 +95,11 @@
emit({ ResetMidpoint: { index } });
}
function markerDoubleClick(index: number) {
if (disabled) return;
emit({ ResetMarker: { index } });
}
function trackPointerDown(e: PointerEvent) {
if (disabled) return;
if (e.button !== BUTTON_LEFT) return;
@ -137,7 +143,7 @@
let position = pointerPosition(e);
if (position === undefined) return;
if (!allowSwap) position = clampToNeighbors(activeMarkerIndex, position);
if (!allowReorder) position = clampToNeighbors(activeMarkerIndex, position);
if (!dragInsertedMarker) dispatch("dragging", true);
emit({ MoveMarker: { index: activeMarkerIndex, position } });
@ -239,7 +245,7 @@
<LayoutCol
class="spectrum-input"
classes={{ disabled }}
classes={{ narrow, disabled }}
styles={{
"--gradient-start": trackStartCSS,
"--gradient-end": trackEndCSS,
@ -271,6 +277,7 @@
style:--marker-position={marker.position}
style:--marker-color={marker.handleColorCSS}
on:pointerdown={(e) => markerPointerDown(e, index)}
on:dblclick={() => markerDoubleClick(index)}
data-gradient-marker
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 12"
@ -317,6 +324,11 @@
border-radius: 2px;
}
&.narrow .gradient-strip {
margin-top: 8px;
height: 8px;
}
&.disabled .gradient-strip {
transition: opacity 0.1s;

View File

@ -140,12 +140,8 @@ fn make_opaque<T: Adjust<Color>>(
}
// TODO: Remove this once GPU shader nodes are able to support the non-classic algorithm
#[node_macro::node(
name("Brightness/Contrast Classic"),
category("Raster: Adjustment"),
properties("brightness_contrast_properties"),
shader_node(PerPixelAdjust)
)]
// TODO: Maybe re-add the "Raster: Adjustment" category to make this user-facing if we care to make this not just for testing
#[node_macro::node(name("Brightness/Contrast Classic"), category(""), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))]
fn brightness_contrast_classic<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@ -257,7 +253,7 @@ fn brightness_contrast<T: Adjust<Color>>(
//
// Some further analysis available at:
// https://geraldbakker.nl/psnumbers/levels.html
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
#[node_macro::node(category("Raster: Adjustment"), properties("levels_properties"), shader_node(PerPixelAdjust))]
fn levels<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@ -325,7 +321,8 @@ fn levels<T: Adjust<Color>>(
// Algorithm from:
// https://stackoverflow.com/a/55233732/775283
// Works the same for gamma and linear color
#[node_macro::node(name("Black & White"), category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
// TODO: Currently the un-Table-wrapped `tint` Color is causing a type error. Put this back in the "Raster: Adjustment" category once that's fixed.
#[node_macro::node(name("Black & White"), category(""), properties("black_and_white_properties"), shader_node(PerPixelAdjust))]
fn black_and_white<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@ -398,7 +395,7 @@ fn black_and_white<T: Adjust<Color>>(
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27hue%20%27%20%3D%20Old,saturation%2C%20Photoshop%205.0
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=0%20%3D%20Use%20other.-,Hue/Saturation,-Hue/Saturation%20settings
#[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
#[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"), properties("hue_saturation_properties"), shader_node(PerPixelAdjust))]
fn hue_saturation<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@ -456,7 +453,7 @@ fn invert<T: Adjust<Color>>(
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=post%27%20%3D%20Posterize-,%27thrs%27%20%3D%20Threshold,-%27grdm%27%20%3D%20Gradient
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
#[node_macro::node(category("Raster: Adjustment"), properties("threshold_properties"), shader_node(PerPixelAdjust))]
fn threshold<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@ -502,7 +499,7 @@ fn threshold<T: Adjust<Color>>(
// It's not the same as the saturation component of Hue/Saturation/Value. Vibrance and Saturation are both separable.
// When both parameters are set, it is equivalent to running this adjustment twice, with only vibrance set and then only saturation set.
// (Except for some noise probably due to rounding error.)
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
#[node_macro::node(category("Raster: Adjustment"), properties("vibrance_properties"), shader_node(PerPixelAdjust))]
fn vibrance<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(