mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 13:38:13 +08:00
Add dashed links between spectrum markers and typed marker scales to the shared spectrum sections (#4534)
This commit is contained in:
@@ -705,12 +705,25 @@ pub struct SpectrumMarker {
|
|||||||
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
|
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
|
||||||
#[serde(rename = "handleColorCSS")]
|
#[serde(rename = "handleColorCSS")]
|
||||||
handle_color_css: String,
|
handle_color_css: String,
|
||||||
|
/// 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpectrumMarker {
|
impl SpectrumMarker {
|
||||||
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
||||||
let handle_color_css = format!("#{}", SRGBA8::from(handle_color).to_rgb_hex());
|
let handle_color_css = format!("#{}", SRGBA8::from(handle_color).to_rgb_hex());
|
||||||
Self { position, midpoint, handle_color_css }
|
Self {
|
||||||
|
position,
|
||||||
|
midpoint,
|
||||||
|
handle_color_css,
|
||||||
|
dashed_to_next: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dash_to_next(mut self) -> Self {
|
||||||
|
self.dashed_to_next = true;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1399,36 +1399,101 @@ 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> {
|
pub(crate) fn levels_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::raster::levels::*;
|
use graphene_std::raster::levels::*;
|
||||||
|
|
||||||
// (parameter, marker handle color, default percentage for double-click reset)
|
|
||||||
let input_range_params = [
|
let input_range_params = [
|
||||||
(ShadowsInput.into(), Color::BLACK, 0.),
|
SpectrumSectionParam::new(ShadowsInput, Color::BLACK, 0., MarkerScale::Percent),
|
||||||
(MidtonesInput.into(), Color::MIDDLE_GRAY, 50.),
|
SpectrumSectionParam::new(MidtonesInput, Color::MIDDLE_GRAY, 50., MarkerScale::Percent),
|
||||||
(HighlightsInput.into(), Color::WHITE, 100.),
|
SpectrumSectionParam::new(HighlightsInput, 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),
|
||||||
];
|
];
|
||||||
let output_range_params = [(OutputMinimumsInput.into(), Color::BLACK, 0.), (OutputMaximumsInput.into(), Color::WHITE, 100.)];
|
|
||||||
|
|
||||||
let mut layout = Vec::with_capacity(5);
|
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, &bw_track(), &input_range_params, &mut layout);
|
||||||
build_shared_spectrum_section(node_id, context, &output_range_params, &mut layout);
|
build_shared_spectrum_section(node_id, context, &bw_track(), &output_range_params, &mut layout);
|
||||||
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
|
/// How a shared spectrum marker's value maps onto its track.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum MarkerScale {
|
||||||
|
/// A 0..100 percentage, placed linearly.
|
||||||
|
Percent,
|
||||||
|
/// A gamma of 0.01..9.99 running from 9.99 at the left to 0.01 at the right, logarithmic on each side of the 1 at its center.
|
||||||
|
Gamma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkerScale {
|
||||||
|
fn position(self, value: f64) -> f64 {
|
||||||
|
match self {
|
||||||
|
Self::Percent => value / 100.,
|
||||||
|
Self::Gamma if value >= 1. => 0.5 - 0.5 * value.log10() / 9.99_f64.log10(),
|
||||||
|
Self::Gamma => 0.5 + 0.5 * value.log10() / 0.01_f64.log10(),
|
||||||
|
}
|
||||||
|
.clamp(0., 1.)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(self, position: f64) -> f64 {
|
||||||
|
match self {
|
||||||
|
Self::Percent => (position * 100.).clamp(0., 100.),
|
||||||
|
Self::Gamma if position <= 0.5 => 9.99_f64.powf(1. - 2. * position).clamp(1., 9.99),
|
||||||
|
Self::Gamma => 0.01_f64.powf(2. * position - 1.).clamp(0.01, 1.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number_input(self) -> NumberInput {
|
||||||
|
match self {
|
||||||
|
Self::Percent => NumberInput::default().mode_increment().unit("%").min(0.).max(100.).display_decimal_places(0),
|
||||||
|
Self::Gamma => NumberInput::default().mode_increment().min(0.01).max(9.99).display_decimal_places(2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One parameter of a shared spectrum section and how its marker sits on the track.
|
||||||
|
struct SpectrumSectionParam {
|
||||||
|
parameter: ParameterRef,
|
||||||
|
handle_color: Color,
|
||||||
|
/// The value a double-click resets to.
|
||||||
|
default_value: f64,
|
||||||
|
scale: MarkerScale,
|
||||||
|
/// Whether a dashed line joins the marker to the next parameter's marker.
|
||||||
|
dash_to_next: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpectrumSectionParam {
|
||||||
|
fn new(parameter: impl Into<ParameterRef>, handle_color: Color, default_value: f64, scale: MarkerScale) -> Self {
|
||||||
|
Self {
|
||||||
|
parameter: parameter.into(),
|
||||||
|
handle_color,
|
||||||
|
default_value,
|
||||||
|
scale,
|
||||||
|
dash_to_next: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dash_to_next(mut self) -> Self {
|
||||||
|
self.dash_to_next = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a section of related parameters as rows: a shared spectrum over `track` (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.
|
/// 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.
|
/// 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: &[(ParameterRef, Color, f64)], layout: &mut Vec<LayoutGroup>) {
|
fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesContext, track: &Gradient, params: &[SpectrumSectionParam], layout: &mut Vec<LayoutGroup>) {
|
||||||
// Snapshot exposure and values before the mutable-borrow loop
|
// Snapshot exposure and values before the mutable-borrow loop
|
||||||
let exposure_and_value: Vec<(bool, f64)> = match get_document_node(node_id, context) {
|
let exposure_and_value: Vec<(bool, f64)> = match get_document_node(node_id, context) {
|
||||||
Ok(document_node) => params
|
Ok(document_node) => params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(parameter, _, _)| {
|
.map(|param| {
|
||||||
let input = document_node.inputs.get(parameter.input_index);
|
let input = document_node.inputs.get(param.parameter.input_index);
|
||||||
let exposed = input.is_some_and(|input| input.is_exposed());
|
let exposed = input.is_some_and(|input| input.is_exposed());
|
||||||
let percent = input
|
let value = input
|
||||||
.and_then(|input| input.as_value())
|
.and_then(|input| input.as_value())
|
||||||
.and_then(|tagged| if let TaggedValue::F32(value) = tagged { Some(*value as f64) } else { None })
|
.and_then(|tagged| if let TaggedValue::F32(value) = tagged { Some(*value as f64) } else { None })
|
||||||
.unwrap_or(0.);
|
.unwrap_or(0.);
|
||||||
(exposed, percent)
|
(exposed, value)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -1437,20 +1502,23 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build markers for all non-exposed params
|
// Build markers for all non-exposed params, linking one to the next only when both have markers
|
||||||
let mut marker_input_indices = Vec::new();
|
let mut marker_input_indices = Vec::new();
|
||||||
let mut marker_default_percents = Vec::new();
|
let mut marker_default_positions = Vec::new();
|
||||||
|
let mut marker_scales = Vec::new();
|
||||||
let mut marker_positions = Vec::new();
|
let mut marker_positions = Vec::new();
|
||||||
let mut handle_colors = Vec::new();
|
let mut marker_colors_and_links = Vec::new();
|
||||||
for (i, &(ref parameter, handle_color, default_percent)) in params.iter().enumerate() {
|
for (i, param) in params.iter().enumerate() {
|
||||||
let (exposed, percent) = exposure_and_value[i];
|
let (exposed, value) = exposure_and_value[i];
|
||||||
if exposed {
|
if exposed {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
marker_positions.push((percent / 100.).clamp(0., 1.));
|
let next_has_marker = exposure_and_value.get(i + 1).is_some_and(|&(next_exposed, _)| !next_exposed);
|
||||||
marker_input_indices.push(parameter.input_index);
|
marker_positions.push(param.scale.position(value));
|
||||||
marker_default_percents.push(default_percent);
|
marker_input_indices.push(param.parameter.input_index);
|
||||||
handle_colors.push(handle_color);
|
marker_default_positions.push(param.scale.position(param.default_value));
|
||||||
|
marker_scales.push(param.scale);
|
||||||
|
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
|
// Enforce non-decreasing order so markers never visually cross, matching the node's algorithm where shadows takes precedence
|
||||||
@@ -1460,13 +1528,16 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
|
|
||||||
let spectrum_markers: Vec<SpectrumMarker> = marker_positions
|
let spectrum_markers: Vec<SpectrumMarker> = marker_positions
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&handle_colors)
|
.zip(&marker_colors_and_links)
|
||||||
.map(|(&position, &handle_color)| SpectrumMarker::new(position, 0.5, handle_color))
|
.map(|(&position, &(handle_color, dashed))| {
|
||||||
|
let marker = SpectrumMarker::new(position, 0.5, handle_color);
|
||||||
|
if dashed { marker.dash_to_next() } else { marker }
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Build the shared spectrum widget (placed on the first non-exposed row)
|
// Build the shared spectrum widget (placed on the first non-exposed row)
|
||||||
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
||||||
SpectrumInput::new(GradientStops::from(&bw_track()))
|
SpectrumInput::new(GradientStops::from(track))
|
||||||
.track_space(GradientSpace::RgbGamma)
|
.track_space(GradientSpace::RgbGamma)
|
||||||
.markers(spectrum_markers)
|
.markers(spectrum_markers)
|
||||||
.show_midpoints(false)
|
.show_midpoints(false)
|
||||||
@@ -1476,31 +1547,31 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
.narrow(true)
|
.narrow(true)
|
||||||
.on_update({
|
.on_update({
|
||||||
let marker_input_indices = marker_input_indices.clone();
|
let marker_input_indices = marker_input_indices.clone();
|
||||||
let marker_default_percents = marker_default_percents.clone();
|
let marker_default_positions = marker_default_positions.clone();
|
||||||
|
let marker_scales = marker_scales.clone();
|
||||||
let marker_positions = marker_positions.clone();
|
let marker_positions = marker_positions.clone();
|
||||||
move |update: &SpectrumInputUpdate| {
|
move |update: &SpectrumInputUpdate| {
|
||||||
let (input_index, percent) = match update {
|
let i = match update {
|
||||||
SpectrumInputUpdate::MoveMarker { index, position } => match marker_input_indices.get(*index as usize) {
|
SpectrumInputUpdate::MoveMarker { index, .. } | SpectrumInputUpdate::ResetMarker { index } => *index as usize,
|
||||||
Some(&input_index) => (input_index, *position * 100.),
|
_ => return Message::NoOp,
|
||||||
None => 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 {
|
||||||
SpectrumInputUpdate::ResetMarker { index } => {
|
return Message::NoOp;
|
||||||
let i = *index as usize;
|
};
|
||||||
let Some(&input_index) = marker_input_indices.get(i) else { return Message::NoOp };
|
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
|
||||||
let Some(&default_percent) = marker_default_percents.get(i) else { return Message::NoOp };
|
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);
|
||||||
// Falls back to midpoint between neighbors if the default would cross one
|
|
||||||
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
|
let scale_position = match update {
|
||||||
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);
|
SpectrumInputUpdate::MoveMarker { position, .. } => *position,
|
||||||
let default_position = default_percent / 100.;
|
// A default that would cross a neighbor falls back to the midpoint between them
|
||||||
let new_position = if (left..=right).contains(&default_position) { default_position } else { (left + right) / 2. };
|
SpectrumInputUpdate::ResetMarker { .. } if (left..=right).contains(&default_position) => default_position,
|
||||||
(input_index, new_position * 100.)
|
SpectrumInputUpdate::ResetMarker { .. } => (left + right) / 2.,
|
||||||
}
|
|
||||||
_ => return Message::NoOp,
|
_ => return Message::NoOp,
|
||||||
};
|
};
|
||||||
NodeGraphMessage::SetInputValue {
|
NodeGraphMessage::SetInputValue {
|
||||||
node_id,
|
node_id,
|
||||||
input_index,
|
input_index,
|
||||||
value: TaggedValue::F32(percent.clamp(0., 100.) as f32).into(),
|
value: TaggedValue::F32(scale.value(scale_position) as f32).into(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -1510,12 +1581,11 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
});
|
});
|
||||||
let spectrum_owner = marker_input_indices.first().copied();
|
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
|
// One row per parameter: first non-exposed carries the shared spectrum, others get just a number input
|
||||||
for (i, (parameter, _, _)) in params.iter().enumerate() {
|
for (i, param) in params.iter().enumerate() {
|
||||||
let (exposed, current) = exposure_and_value[i];
|
let (exposed, current) = exposure_and_value[i];
|
||||||
let input_index = parameter.input_index;
|
let input_index = param.parameter.input_index;
|
||||||
|
let number_input = param.scale.number_input();
|
||||||
|
|
||||||
if exposed {
|
if exposed {
|
||||||
let row = number_widget(ParameterWidgetsInfo::at_index(node_id, input_index, true, context), number_input.clone());
|
let row = number_widget(ParameterWidgetsInfo::at_index(node_id, input_index, true, context), number_input.clone());
|
||||||
@@ -1537,7 +1607,6 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
.value(Some(current))
|
.value(Some(current))
|
||||||
.min_width(60)
|
.min_width(60)
|
||||||
.max_width(60)
|
.max_width(60)
|
||||||
.display_decimal_places(0)
|
|
||||||
.on_update(update_value_at_index(
|
.on_update(update_value_at_index(
|
||||||
move |widget: &NumberInput| TaggedValue::F32(widget.value.unwrap_or(0.) as f32),
|
move |widget: &NumberInput| TaggedValue::F32(widget.value.unwrap_or(0.) as f32),
|
||||||
node_id,
|
node_id,
|
||||||
@@ -1772,10 +1841,13 @@ fn spectrum_slider_row(
|
|||||||
pub(crate) fn threshold_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn threshold_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::raster::threshold::*;
|
use graphene_std::raster::threshold::*;
|
||||||
|
|
||||||
let params: &[(ParameterRef, Color, f64)] = &[(MinLuminanceInput.into(), Color::BLACK, 50.), (MaxLuminanceInput.into(), Color::WHITE, 100.)];
|
let params = [
|
||||||
|
SpectrumSectionParam::new(MinLuminanceInput, Color::WHITE, 50., MarkerScale::Percent).dash_to_next(),
|
||||||
|
SpectrumSectionParam::new(MaxLuminanceInput, Color::WHITE, 100., MarkerScale::Percent),
|
||||||
|
];
|
||||||
|
|
||||||
let mut layout = Vec::with_capacity(2);
|
let mut layout = Vec::with_capacity(2);
|
||||||
build_shared_spectrum_section(node_id, context, params, &mut layout);
|
build_shared_spectrum_section(node_id, context, &bw_track(), ¶ms, &mut layout);
|
||||||
|
|
||||||
layout
|
layout
|
||||||
}
|
}
|
||||||
@@ -2143,11 +2215,31 @@ pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodeProp
|
|||||||
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::raster::exposure::*;
|
use graphene_std::raster::exposure::*;
|
||||||
|
|
||||||
let exposure = number_widget(ParameterWidgetsInfo::new(node_id, ExposureInput, true, context), NumberInput::default().min(-20.).max(20.));
|
let exposure = range_slider_widget(
|
||||||
let offset = number_widget(ParameterWidgetsInfo::new(node_id, OffsetInput, true, context), NumberInput::default().min(-0.5).max(0.5));
|
ParameterWidgetsInfo::new(node_id, ExposureInput, true, context),
|
||||||
let gamma_correction = number_widget(
|
NumberInput::default().min(-20.).max(20.),
|
||||||
|
SliderRange {
|
||||||
|
min: -20.,
|
||||||
|
max: 20.,
|
||||||
|
default: Some(0.),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let offset = range_slider_widget(
|
||||||
|
ParameterWidgetsInfo::new(node_id, OffsetInput, true, context),
|
||||||
|
NumberInput::default().min(-0.5).max(0.5),
|
||||||
|
SliderRange {
|
||||||
|
min: -0.5,
|
||||||
|
max: 0.5,
|
||||||
|
default: Some(0.),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let gamma_correction = slider_row(
|
||||||
ParameterWidgetsInfo::new(node_id, GammaCorrectionInput, true, context),
|
ParameterWidgetsInfo::new(node_id, GammaCorrectionInput, true, context),
|
||||||
NumberInput::default().min(0.01).max(9.99).increment_step(0.1),
|
MarkerScale::Gamma.number_input().increment_step(0.1),
|
||||||
|
Some(1.),
|
||||||
|
|gamma| MarkerScale::Gamma.position(gamma),
|
||||||
|
|position| MarkerScale::Gamma.value(position),
|
||||||
);
|
);
|
||||||
|
|
||||||
vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)]
|
vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)]
|
||||||
|
|||||||
@@ -50,7 +50,9 @@
|
|||||||
// Set when a key-triggered reconcile inserts/removes the frozen copy, so the next pointer move skips emitting a `MoveMarker`
|
// Set when a key-triggered reconcile inserts/removes the frozen copy, so the next pointer move skips emitting a `MoveMarker`
|
||||||
// that would otherwise race the structural change before Rust has reported the dragged marker's new index.
|
// that would otherwise race the structural change before Rust has reported the dragged marker's new index.
|
||||||
let skipNextMove = false;
|
let skipNextMove = false;
|
||||||
// The hovered marker, highlighted ahead of the drag.
|
// Set while a run of markers drags together: its bounds, the first's offset from the pointer, each member's gap from the first, and their start positions for cancelling.
|
||||||
|
let dragRun: { first: number; last: number; offset: number; spacings: number[]; restore: number[] } | undefined = undefined;
|
||||||
|
// The run a hovered marker or dashed link would carry, highlighted ahead of the drag.
|
||||||
let hoverRun: [number, number] | undefined = undefined;
|
let hoverRun: [number, number] | undefined = undefined;
|
||||||
// The marker being dragged, or the left marker of the interval when a midpoint is dragged, and which of the two it is.
|
// The marker being dragged, or the left marker of the interval when a midpoint is dragged, and which of the two it is.
|
||||||
// Where selecting is allowed, these follow the selection, which Rust renumbers across structural changes.
|
// Where selecting is allowed, these follow the selection, which Rust renumbers across structural changes.
|
||||||
@@ -60,9 +62,10 @@
|
|||||||
dragIndex = activeMarkerIndex;
|
dragIndex = activeMarkerIndex;
|
||||||
dragIsMidpoint = activeMarkerIsMidpoint;
|
dragIsMidpoint = activeMarkerIsMidpoint;
|
||||||
}
|
}
|
||||||
// The marker highlighted: the one hovered, else the one being dragged (or selected, where selecting is allowed).
|
// The markers highlighted: the run being dragged or hovered, else the marker dragged alone (or selected, where selecting is allowed).
|
||||||
let highlightedRun: [number, number] | undefined;
|
let highlightedRun: [number, number] | undefined;
|
||||||
$: highlightedRun = hoverRun !== undefined ? hoverRun : typeof dragIndex === "number" && !dragIsMidpoint ? [dragIndex, dragIndex] : undefined;
|
$: highlightedRun =
|
||||||
|
dragRun !== undefined ? [dragRun.first, dragRun.last] : hoverRun !== undefined ? hoverRun : typeof dragIndex === "number" && !dragIsMidpoint ? [dragIndex, dragIndex] : undefined;
|
||||||
|
|
||||||
function emit(intent: SpectrumInputUpdate) {
|
function emit(intent: SpectrumInputUpdate) {
|
||||||
dispatch("update", intent);
|
dispatch("update", intent);
|
||||||
@@ -91,12 +94,30 @@
|
|||||||
return clamp ? Math.max(0, Math.min(1, ratio)) : ratio;
|
return clamp ? Math.max(0, Math.min(1, ratio)) : ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampToNeighbors(index: number, position: number): number {
|
// Holds markers `first..=last` (spanning `spacing`) between their neighbors as they move to `position`
|
||||||
const lower = markers[index - 1]?.position ?? 0;
|
function holdBetweenNeighbors(first: number, last: number, spacing: number, position: number): number {
|
||||||
const upper = markers[index + 1]?.position ?? 1;
|
// 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;
|
||||||
return Math.max(lower, Math.min(upper, position));
|
return Math.max(lower, Math.min(upper, position));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 }[] = [];
|
||||||
|
|
||||||
|
markers.forEach((marker, index) => {
|
||||||
|
const next = markers[index + 1];
|
||||||
|
if (!linked(marker) || next === undefined || next.position === marker.position) return;
|
||||||
|
|
||||||
|
const [left, right] = next.position > marker.position ? [marker.position, next.position] : [next.position, marker.position];
|
||||||
|
spans.push({ index, left, width: right - left });
|
||||||
|
});
|
||||||
|
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
// The nearest of the markers drawn on the track, skipping any outside 0..1 as the template does
|
// The nearest of the markers drawn on the track, skipping any outside 0..1 as the template does
|
||||||
function nearestMarkerIndex(position: number): number | undefined {
|
function nearestMarkerIndex(position: number): number | undefined {
|
||||||
let nearest: number | undefined = undefined;
|
let nearest: number | undefined = undefined;
|
||||||
@@ -140,6 +161,43 @@
|
|||||||
addEvents();
|
addEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drags markers `first..=last` as one, keeping the pointer's offset from the first when `grabbed` and otherwise carrying the run to the pointer
|
||||||
|
function beginRunDrag(e: PointerEvent, first: number, last: number, grabbed: boolean) {
|
||||||
|
const pointer = pointerPosition(e);
|
||||||
|
if (pointer === undefined) return;
|
||||||
|
const start = markers[first].position;
|
||||||
|
|
||||||
|
const spacings: number[] = [];
|
||||||
|
const restore: number[] = [];
|
||||||
|
for (let index = first; index <= last; index += 1) {
|
||||||
|
const position = markers[index].position;
|
||||||
|
spacings.push(position - start);
|
||||||
|
restore.push(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeMarkerIndexRestore = activeMarkerIndex;
|
||||||
|
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||||
|
dragRestorePosition = start;
|
||||||
|
dragInsertedMarker = false;
|
||||||
|
dragMoved = false;
|
||||||
|
duplicateRequested = false;
|
||||||
|
duplicateActive = false;
|
||||||
|
dragRun = { first, last, offset: grabbed ? start - pointer : 0, spacings, restore };
|
||||||
|
setActive(first, false);
|
||||||
|
addEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The run a dashed link from `index` carries: the two markers it joins
|
||||||
|
function dashedRun(index: number): [number, number] {
|
||||||
|
return [index, index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashPointerDown(e: PointerEvent, index: number) {
|
||||||
|
if (disabled || e.button !== BUTTON_LEFT) return;
|
||||||
|
const [first, last] = dashedRun(index);
|
||||||
|
beginRunDrag(e, first, last, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Picks up the marker at `index` and carries it to the pointer
|
// Picks up the marker at `index` and carries it to the pointer
|
||||||
function pickUpMarker(e: PointerEvent, index: number) {
|
function pickUpMarker(e: PointerEvent, index: number) {
|
||||||
beginMarkerDrag(e, index);
|
beginMarkerDrag(e, index);
|
||||||
@@ -168,6 +226,11 @@
|
|||||||
emit({ ResetMarker: { index } });
|
emit({ ResetMarker: { index } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetRun(first: number, last: number) {
|
||||||
|
if (disabled || dragMoved) return;
|
||||||
|
for (let index = first; index <= last; index += 1) emit({ ResetMarker: { index } });
|
||||||
|
}
|
||||||
|
|
||||||
function trackPointerDown(e: PointerEvent) {
|
function trackPointerDown(e: PointerEvent) {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
if (e.button !== BUTTON_LEFT) return;
|
if (e.button !== BUTTON_LEFT) return;
|
||||||
@@ -297,14 +360,32 @@
|
|||||||
|
|
||||||
let position = pointerPosition(e);
|
let position = pointerPosition(e);
|
||||||
if (position === undefined) return;
|
if (position === undefined) return;
|
||||||
// Without selection nothing reports the dragged marker's new index after a reorder, so it stays between its neighbors
|
position = holdBetweenNeighbors(dragIndex, dragIndex, 0, position);
|
||||||
if (!allowReorder || !allowSelect) position = clampToNeighbors(dragIndex, position);
|
|
||||||
|
|
||||||
dragMoved = true;
|
dragMoved = true;
|
||||||
if (!dragInsertedMarker) dispatch("dragging", true);
|
if (!dragInsertedMarker) dispatch("dragging", true);
|
||||||
emit({ MoveMarker: { index: dragIndex, position } });
|
emit({ MoveMarker: { index: dragIndex, position } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveRun(e: PointerEvent) {
|
||||||
|
if (disabled || dragRun === undefined) return;
|
||||||
|
if (e.buttons === 0) {
|
||||||
|
endDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { first, last, offset, spacings } = dragRun;
|
||||||
|
const pointer = pointerPosition(e);
|
||||||
|
if (pointer === undefined) return;
|
||||||
|
|
||||||
|
const span = spacings[spacings.length - 1];
|
||||||
|
const start = holdBetweenNeighbors(first, last, span, pointer + offset);
|
||||||
|
|
||||||
|
dragMoved = true;
|
||||||
|
dispatch("dragging", true);
|
||||||
|
spacings.forEach((spacing, i) => emit({ MoveMarker: { index: first + i, position: start + spacing } }));
|
||||||
|
}
|
||||||
|
|
||||||
function moveActiveMidpoint(e: PointerEvent) {
|
function moveActiveMidpoint(e: PointerEvent) {
|
||||||
if (disabled || dragIndex === undefined) return;
|
if (disabled || dragIndex === undefined) return;
|
||||||
if (e.buttons === 0) {
|
if (e.buttons === 0) {
|
||||||
@@ -352,6 +433,10 @@
|
|||||||
} else if (anchor !== undefined) {
|
} else if (anchor !== undefined) {
|
||||||
// A duplicated pre-existing marker: the frozen copy already sits at the start position, so deleting the dragged copy restores the original.
|
// A duplicated pre-existing marker: the frozen copy already sits at the start position, so deleting the dragged copy restores the original.
|
||||||
emit({ DeleteMarker: { index: dragged } });
|
emit({ DeleteMarker: { index: dragged } });
|
||||||
|
} else if (dragRun !== undefined) {
|
||||||
|
// A run drag: return every member to where it began.
|
||||||
|
const { first, restore } = dragRun;
|
||||||
|
restore.forEach((position, i) => emit({ MoveMarker: { index: first + i, position } }));
|
||||||
} else if (dragRestorePosition !== undefined) {
|
} else if (dragRestorePosition !== undefined) {
|
||||||
// Plain drag: return the marker (or midpoint) to where it began.
|
// Plain drag: return the marker (or midpoint) to where it began.
|
||||||
if (dragIsMidpoint) emit({ MoveMidpoint: { index: dragged, position: dragRestorePosition } });
|
if (dragIsMidpoint) emit({ MoveMidpoint: { index: dragged, position: dragRestorePosition } });
|
||||||
@@ -376,6 +461,7 @@
|
|||||||
duplicateRequested = false;
|
duplicateRequested = false;
|
||||||
duplicateActive = false;
|
duplicateActive = false;
|
||||||
skipNextMove = false;
|
skipNextMove = false;
|
||||||
|
dragRun = undefined;
|
||||||
// Without selection nothing stays active once the drag ends
|
// Without selection nothing stays active once the drag ends
|
||||||
if (!allowSelect) {
|
if (!allowSelect) {
|
||||||
dragIndex = undefined;
|
dragIndex = undefined;
|
||||||
@@ -386,6 +472,7 @@
|
|||||||
|
|
||||||
function onPointerMove(e: PointerEvent) {
|
function onPointerMove(e: PointerEvent) {
|
||||||
if (dragIsMidpoint) moveActiveMidpoint(e);
|
if (dragIsMidpoint) moveActiveMidpoint(e);
|
||||||
|
else if (dragRun !== undefined) moveRun(e);
|
||||||
else moveActiveMarker(e);
|
else moveActiveMarker(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +544,7 @@
|
|||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
$: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic, trackInterpolation);
|
$: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic, trackInterpolation);
|
||||||
|
$: dashes = markerSpans(markers, (marker) => marker.dashedToNext);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener("keydown", deleteShortcut);
|
document.addEventListener("keydown", deleteShortcut);
|
||||||
@@ -507,6 +595,18 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="marker-track" classes={{ interactive: !allowInsert }} bind:this={markerTrackElement} on:pointerdown={markerTrackPointerDown}>
|
<LayoutRow class="marker-track" classes={{ interactive: !allowInsert }} bind:this={markerTrackElement} on:pointerdown={markerTrackPointerDown}>
|
||||||
|
{#each dashes as dash}
|
||||||
|
<div
|
||||||
|
class="dashed-link"
|
||||||
|
class:active={highlightedRun !== undefined && dash.index >= highlightedRun[0] && dash.index < highlightedRun[1]}
|
||||||
|
style:--span-left={dash.left}
|
||||||
|
style:--span-width={dash.width}
|
||||||
|
on:pointerenter={() => (hoverRun = dashedRun(dash.index))}
|
||||||
|
on:pointerleave={() => (hoverRun = undefined)}
|
||||||
|
on:pointerdown={(e) => dashPointerDown(e, dash.index)}
|
||||||
|
on:dblclick={() => resetRun(...dashedRun(dash.index))}
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
{#each markers as marker, index}
|
{#each markers as marker, index}
|
||||||
{#if marker.position >= 0 && marker.position <= 1}
|
{#if marker.position >= 0 && marker.position <= 1}
|
||||||
<svg
|
<svg
|
||||||
@@ -646,6 +746,20 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Full width and clipped to its span, so the dashes keep the lane's phase rather than walking with the left handle.
|
||||||
|
// The whole band is the grab area, with the 1px line drawn through its middle in the same neutral as the strip's edges.
|
||||||
|
.dashed-link {
|
||||||
|
--link-color: var(--color-5-dullgray);
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: repeating-linear-gradient(to right, var(--link-color) 0 2px, transparent 2px 4px) 0 4px / auto 1px repeat-x;
|
||||||
|
clip-path: inset(0 calc((1 - var(--span-left) - var(--span-width)) * 100%) 0 calc(var(--span-left) * 100%));
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.marker {
|
.marker {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -677,6 +791,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled .marker-track .dashed-link {
|
||||||
|
--link-color: var(--color-4-dimgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.disabled) .marker-track .dashed-link.active {
|
||||||
|
--link-color: var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.disabled) .marker-track .marker {
|
&:not(.disabled) .marker-track .marker {
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
.inner-fill:hover + .outer-border,
|
.inner-fill:hover + .outer-border,
|
||||||
|
|||||||
Reference in New Issue
Block a user