Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions editor/src/messages/layout/utility_types/widgets/input_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,11 +695,9 @@ pub struct SpectrumInput {
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SpectrumMarker {
/// Position of the marker along the spectrum track, normally from 0 to 1. A shifted or stretched non-cyclic ramp can
/// place it outside that range, where the track draws only the markers falling within its visible span.
/// Position along the track, normally 0..1. A shifted or stretched non-cyclic ramp can push it outside, where it is not drawn.
position: f64,
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true.
/// The last marker's value controls the wrapped interval when `track_cyclic` is set, and is otherwise ignored.
/// Midpoint (0..1) of the interval to the next marker, used only with `show_midpoints`. The last marker's midpoint spans the wrap of a cyclic track, or is otherwise ignored.
midpoint: f64,
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`],
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
Expand All @@ -708,6 +706,9 @@ pub struct SpectrumMarker {
/// Whether a dashed line runs from this marker to the next through the lane below the track. Dragging it carries both markers.
#[serde(rename = "dashedToNext")]
dashed_to_next: bool,
/// Whether this marker follows its neighbors instead of bounding them, so they may drag past its drawn position.
#[serde(rename = "betweenNeighbors")]
between_neighbors: bool,
}

impl SpectrumMarker {
Expand All @@ -718,9 +719,15 @@ impl SpectrumMarker {
midpoint,
handle_color_css,
dashed_to_next: false,
between_neighbors: false,
}
}

pub fn between_neighbors(mut self) -> Self {
self.between_neighbors = true;
self
}

pub fn dash_to_next(mut self) -> Self {
self.dashed_to_next = true;
self
Expand Down
122 changes: 106 additions & 16 deletions editor/src/messages/portfolio/document/node_graph/node_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1399,17 +1399,63 @@ pub(crate) fn transfer_curves_properties(node_id: NodeId, context: &mut NodeProp
pub(crate) fn levels_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::levels::*;

let mut channel_info = ParameterWidgetsInfo::new(node_id, ChannelInput, true, context);
channel_info.exposable = false;
let channel = enum_choice::<AdjustmentChannel>().for_socket(channel_info).property_row();

let channel_value = match get_document_node(node_id, context).ok().and_then(|document_node| document_node.input_value(ChannelInput).cloned()) {
Some(TaggedValue::AdjustmentChannel(channel)) => channel,
_ => AdjustmentChannel::Rgb,
};
let [shadows, midtones, highlights, output_minimums, output_maximums]: [ParameterRef; 5] = match channel_value {
AdjustmentChannel::Rgb => [
ShadowsInput.into(),
MidtonesInput.into(),
HighlightsInput.into(),
OutputMinimumsInput.into(),
OutputMaximumsInput.into(),
],
AdjustmentChannel::Red => [
RedShadowsInput.into(),
RedMidtonesInput.into(),
RedHighlightsInput.into(),
RedOutputMinimumsInput.into(),
RedOutputMaximumsInput.into(),
],
AdjustmentChannel::Green => [
GreenShadowsInput.into(),
GreenMidtonesInput.into(),
GreenHighlightsInput.into(),
GreenOutputMinimumsInput.into(),
GreenOutputMaximumsInput.into(),
],
AdjustmentChannel::Blue => [
BlueShadowsInput.into(),
BlueMidtonesInput.into(),
BlueHighlightsInput.into(),
BlueOutputMinimumsInput.into(),
BlueOutputMaximumsInput.into(),
],
AdjustmentChannel::Alpha => [
AlphaShadowsInput.into(),
AlphaMidtonesInput.into(),
AlphaHighlightsInput.into(),
AlphaOutputMinimumsInput.into(),
AlphaOutputMaximumsInput.into(),
],
};

let input_range_params = [
SpectrumSectionParam::new(ShadowsInput, Color::BLACK, 0., MarkerScale::Percent),
SpectrumSectionParam::new(MidtonesInput, Color::MIDDLE_GRAY, 50., MarkerScale::Percent),
SpectrumSectionParam::new(HighlightsInput, Color::WHITE, 100., MarkerScale::Percent),
SpectrumSectionParam::new(shadows, Color::BLACK, 0., MarkerScale::Percent),
SpectrumSectionParam::new(midtones, Color::MIDDLE_GRAY, 1., MarkerScale::Gamma).between_neighbors(),
Comment thread
Keavon marked this conversation as resolved.
SpectrumSectionParam::new(highlights, Color::WHITE, 100., MarkerScale::Percent),
];
let output_range_params = [
SpectrumSectionParam::new(OutputMinimumsInput, Color::BLACK, 0., MarkerScale::Percent),
SpectrumSectionParam::new(OutputMaximumsInput, Color::WHITE, 100., MarkerScale::Percent),
SpectrumSectionParam::new(output_minimums, Color::BLACK, 0., MarkerScale::Percent),
SpectrumSectionParam::new(output_maximums, Color::WHITE, 100., MarkerScale::Percent),
];

let mut layout = Vec::with_capacity(5);
let mut layout = vec![channel];
build_shared_spectrum_section(node_id, context, &bw_track(), &input_range_params, &mut layout);
build_shared_spectrum_section(node_id, context, &bw_track(), &output_range_params, &mut layout);
layout
Expand Down Expand Up @@ -1459,6 +1505,8 @@ struct SpectrumSectionParam {
scale: MarkerScale,
/// Whether a dashed line joins the marker to the next parameter's marker.
dash_to_next: bool,
/// Whether the marker takes its scale position within the span between its neighbors rather than the whole track, following them as they move.
between_neighbors: bool,
}

impl SpectrumSectionParam {
Expand All @@ -1469,9 +1517,15 @@ impl SpectrumSectionParam {
default_value,
scale,
dash_to_next: false,
between_neighbors: false,
}
}

fn between_neighbors(mut self) -> Self {
self.between_neighbors = true;
self
}

fn dash_to_next(mut self) -> Self {
self.dash_to_next = true;
self
Expand Down Expand Up @@ -1507,6 +1561,7 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
let mut marker_default_positions = Vec::new();
let mut marker_scales = Vec::new();
let mut marker_positions = Vec::new();
let mut marker_between = Vec::new();
let mut marker_colors_and_links = Vec::new();
for (i, param) in params.iter().enumerate() {
let (exposed, value) = exposure_and_value[i];
Expand All @@ -1518,20 +1573,41 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
marker_input_indices.push(param.parameter.input_index);
marker_default_positions.push(param.scale.position(param.default_value));
marker_scales.push(param.scale);
marker_between.push(param.between_neighbors);
marker_colors_and_links.push((param.handle_color, param.dash_to_next && next_has_marker));
}

// 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]);
// Enforce non-decreasing order so markers never visually cross, matching the node's algorithm where shadows takes precedence.
// A marker placed between its neighbors bounds nothing here and instead takes its scale position within their settled span.
let mut floor = 0.;
for (position, &between) in marker_positions.iter_mut().zip(&marker_between) {
if between {
continue;
}
*position = position.max(floor);
floor = *position;
}
for i in 0..marker_positions.len() {
if marker_between[i] {
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);
marker_positions[i] = left + marker_positions[i] * (right - left);
}
}

let spectrum_markers: Vec<SpectrumMarker> = marker_positions
.iter()
.zip(&marker_colors_and_links)
.map(|(&position, &(handle_color, dashed))| {
let marker = SpectrumMarker::new(position, 0.5, handle_color);
if dashed { marker.dash_to_next() } else { marker }
.zip(&marker_between)
.map(|((&position, &(handle_color, dashed)), &between)| {
let mut marker = SpectrumMarker::new(position, 0.5, handle_color);
if dashed {
marker = marker.dash_to_next();
}
if between {
marker = marker.between_neighbors();
}
marker
})
.collect();

Expand All @@ -1550,21 +1626,35 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
let marker_default_positions = marker_default_positions.clone();
let marker_scales = marker_scales.clone();
let marker_positions = marker_positions.clone();
let marker_between = marker_between.clone();
move |update: &SpectrumInputUpdate| {
let i = match update {
SpectrumInputUpdate::MoveMarker { index, .. } | SpectrumInputUpdate::ResetMarker { index } => *index as usize,
_ => return Message::NoOp,
};
let (Some(&input_index), Some(&scale), Some(&default_position)) = (marker_input_indices.get(i), marker_scales.get(i), marker_default_positions.get(i)) else {
let (Some(&input_index), Some(&scale), Some(&between), Some(&default_position)) =
(marker_input_indices.get(i), marker_scales.get(i), marker_between.get(i), marker_default_positions.get(i))
else {
return Message::NoOp;
};
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);

// The span the marker's scale maps onto: its neighbors' positions when placed between them, otherwise the track between the
// nearest markers that bound it, which a marker placed between its neighbors never does
let bounding = |j: usize| between || !marker_between[j];
let left = (0..i).rev().find(|&j| bounding(j)).map_or(0., |j| marker_positions[j]);
let right = (i + 1..marker_positions.len()).find(|&j| bounding(j)).map_or(1., |j| marker_positions[j]);

let scale_position = match update {
SpectrumInputUpdate::MoveMarker { position, .. } if between => {
let span = right - left;
if span <= f64::EPSILON {
return Message::NoOp;
}
((position - left) / span).clamp(0., 1.)
}
SpectrumInputUpdate::MoveMarker { position, .. } => *position,
// A default that would cross a neighbor falls back to the midpoint between them
SpectrumInputUpdate::ResetMarker { .. } if (left..=right).contains(&default_position) => default_position,
SpectrumInputUpdate::ResetMarker { .. } if between || (left..=right).contains(&default_position) => default_position,
SpectrumInputUpdate::ResetMarker { .. } => (left + right) / 2.,
_ => return Message::NoOp,
};
Expand Down
25 changes: 25 additions & 0 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,31 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
inputs_count = 3;
}

// Levels' Midtones became the gamma value it encoded, and each channel gained its own record after the composite one
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster::levels::IDENTIFIER) && inputs_count == 6 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
let output_level = |index: usize, default: f32| match old_inputs.get(index).and_then(|input| input.as_value()) {
Some(TaggedValue::F32(percent)) => percent / 100.,
_ => default,
};
let (output_minimums, output_maximums) = (output_level(4, 0.), output_level(5, 1.));
for (index, input) in old_inputs.iter().take(6).enumerate() {
let input = match (index, input.as_value()) {
Comment thread
Keavon marked this conversation as resolved.
(2, Some(TaggedValue::F32(percent))) => {
// The old node's midtones-to-gamma mapping, from https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels
let midtones = output_minimums + (output_maximums - output_minimums) * percent / 100.;
let gamma = if midtones < 0.5 { 1. + 9. * (1. - midtones * 2.) } else { ((1. - midtones) * 2.).max(0.01) };
NodeInput::value(TaggedValue::F32(gamma.clamp(0.01, 9.99)), input.is_exposed())
}
_ => input.clone(),
};
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input, network_path);
}
inputs_count = 27;
}

if reference == DefinitionIdentifier::ProtoNode(graphene_std::repeat::repeat_on_points::IDENTIFIER) && inputs_count == 2 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/components/widgets/inputs/SpectrumInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,19 @@
function holdBetweenNeighbors(first: number, last: number, spacing: number, position: number): number {
// Without selection nothing reports the dragged marker's new index after a reorder, so it stays between its neighbors
if (allowReorder && allowSelect) return position;
const lower = markers[first - 1]?.position ?? 0;
const upper = (markers[last + 1]?.position ?? 1) - spacing;
const lower = neighborBound(first, -1) ?? 0;
const upper = (neighborBound(last, 1) ?? 1) - spacing;
return Math.max(lower, Math.min(upper, position));
}

// The position of the nearest marker past `index` in the direction of `step` that bounds others, skipping any placed between its neighbors since those follow them instead
function neighborBound(index: number, step: -1 | 1): number | undefined {
for (let i = index + step; i >= 0 && i < markers.length; i += step) {
if (!markers[i].betweenNeighbors) return markers[i].position;
}
return undefined;
}

// The spans from each marker passing `linked` to its successor
function markerSpans(markers: SpectrumMarker[], linked: (marker: SpectrumMarker) => boolean): { index: number; left: number; width: number }[] {
const spans: { index: number; left: number; width: number }[] = [];
Expand Down
Loading
Loading