mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add a "Cyclic" gradient option that closes the ramp into a seamless loop (#4416)
* Add a Cyclic gradient attribute that wraps the stop list through the 1|0 boundary back to the first stop * Show the wrap segment's midpoint diamond in the color picker spectrum strip when cyclic * Keep the wrap midpoint diamond tracking the pointer by a wrapped strip width when dragged past the ends * Give the Gradient tool's viewport overlay the same cyclic wrap midpoint diamond and drag behavior * Correct the docs claiming the final stop's midpoint is always ignored now that cyclic uses it * Move the gradient cyclic toggle onto the Ends row as a Link icon checkbox * Update labels * Register GradientHueDirection with the Data panel so its attribute column renders * Reword wrap segment to wrapped interval and stray segment usages to interval * Clean up comments * Fix the color picker write-back losing default position elision and the blend path guessing the cyclic flag
This commit is contained in:
@@ -49,6 +49,8 @@ pub enum ColorPickerMessage {
|
|||||||
GradientUpdate { update: SpectrumInputUpdate },
|
GradientUpdate { update: SpectrumInputUpdate },
|
||||||
/// Gradient spread choice from the gradient "Ends" selection.
|
/// Gradient spread choice from the gradient "Ends" selection.
|
||||||
SetGradientSpread { gradient_spread: GradientSpread },
|
SetGradientSpread { gradient_spread: GradientSpread },
|
||||||
|
/// Gradient cyclic choice: whether the stops wrap as a cycle, from the "Cyclic" checkbox.
|
||||||
|
SetGradientCyclic { gradient_cyclic: bool },
|
||||||
/// Gradient space choice: the color space the stops interpolate in, from the "Space" dropdown.
|
/// Gradient space choice: the color space the stops interpolate in, from the "Space" dropdown.
|
||||||
SetGradientSpace { gradient_space: GradientSpace },
|
SetGradientSpace { gradient_space: GradientSpace },
|
||||||
/// Gradient hue direction choice: which way around the hue wheel the stops interpolate in a polar space, from the "Arc" dropdown.
|
/// Gradient hue direction choice: which way around the hue wheel the stops interpolate in a polar space, from the "Arc" dropdown.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ pub struct ColorPickerMessageHandler {
|
|||||||
gradient: Option<Gradient>,
|
gradient: Option<Gradient>,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
active_marker_index: Option<u32>,
|
active_marker_index: Option<u32>,
|
||||||
active_marker_is_midpoint: bool,
|
active_marker_is_midpoint: bool,
|
||||||
@@ -55,6 +56,7 @@ impl Default for ColorPickerMessageHandler {
|
|||||||
gradient: None,
|
gradient: None,
|
||||||
gradient_spread: GradientSpread::default(),
|
gradient_spread: GradientSpread::default(),
|
||||||
gradient_space: GradientSpace::default(),
|
gradient_space: GradientSpace::default(),
|
||||||
|
gradient_cyclic: false,
|
||||||
gradient_hue_direction: GradientHueDirection::default(),
|
gradient_hue_direction: GradientHueDirection::default(),
|
||||||
active_marker_index: None,
|
active_marker_index: None,
|
||||||
active_marker_is_midpoint: false,
|
active_marker_is_midpoint: false,
|
||||||
@@ -78,6 +80,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
self.gradient = None;
|
self.gradient = None;
|
||||||
self.gradient_spread = GradientSpread::default();
|
self.gradient_spread = GradientSpread::default();
|
||||||
self.gradient_space = GradientSpace::default();
|
self.gradient_space = GradientSpace::default();
|
||||||
|
self.gradient_cyclic = false;
|
||||||
self.gradient_hue_direction = GradientHueDirection::default();
|
self.gradient_hue_direction = GradientHueDirection::default();
|
||||||
self.active_marker_index = None;
|
self.active_marker_index = None;
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
@@ -86,6 +89,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
self.gradient = None;
|
self.gradient = None;
|
||||||
self.gradient_spread = GradientSpread::default();
|
self.gradient_spread = GradientSpread::default();
|
||||||
self.gradient_space = GradientSpace::default();
|
self.gradient_space = GradientSpace::default();
|
||||||
|
self.gradient_cyclic = false;
|
||||||
self.gradient_hue_direction = GradientHueDirection::default();
|
self.gradient_hue_direction = GradientHueDirection::default();
|
||||||
self.active_marker_index = None;
|
self.active_marker_index = None;
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
@@ -96,6 +100,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
self.gradient_spread = ramp.gradient_spread;
|
self.gradient_spread = ramp.gradient_spread;
|
||||||
self.gradient_space = ramp.gradient_space;
|
self.gradient_space = ramp.gradient_space;
|
||||||
|
self.gradient_cyclic = ramp.gradient_cyclic;
|
||||||
self.gradient_hue_direction = ramp.gradient_hue_direction;
|
self.gradient_hue_direction = ramp.gradient_hue_direction;
|
||||||
let gradient = Gradient::from(ramp);
|
let gradient = Gradient::from(ramp);
|
||||||
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
|
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
|
||||||
@@ -210,6 +215,22 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
value: FillChoice::Gradient(GradientRamp {
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space: self.gradient_space,
|
gradient_space: self.gradient_space,
|
||||||
|
gradient_cyclic: self.gradient_cyclic,
|
||||||
|
gradient_hue_direction: self.gradient_hue_direction,
|
||||||
|
..GradientRamp::from(gradient)
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
|
ColorPickerMessage::SetGradientCyclic { gradient_cyclic } => {
|
||||||
|
let Some(gradient) = &self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
self.gradient_cyclic = gradient_cyclic;
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
|
gradient_spread: self.gradient_spread,
|
||||||
|
gradient_space: self.gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction: self.gradient_hue_direction,
|
gradient_hue_direction: self.gradient_hue_direction,
|
||||||
..GradientRamp::from(gradient)
|
..GradientRamp::from(gradient)
|
||||||
}),
|
}),
|
||||||
@@ -224,6 +245,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
value: FillChoice::Gradient(GradientRamp {
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread: self.gradient_spread,
|
gradient_spread: self.gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic: self.gradient_cyclic,
|
||||||
gradient_hue_direction: self.gradient_hue_direction,
|
gradient_hue_direction: self.gradient_hue_direction,
|
||||||
..GradientRamp::from(gradient)
|
..GradientRamp::from(gradient)
|
||||||
}),
|
}),
|
||||||
@@ -238,6 +260,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
value: FillChoice::Gradient(GradientRamp {
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread: self.gradient_spread,
|
gradient_spread: self.gradient_spread,
|
||||||
gradient_space: self.gradient_space,
|
gradient_space: self.gradient_space,
|
||||||
|
gradient_cyclic: self.gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
..GradientRamp::from(gradient)
|
..GradientRamp::from(gradient)
|
||||||
}),
|
}),
|
||||||
@@ -331,6 +354,7 @@ impl ColorPickerMessageHandler {
|
|||||||
value: FillChoice::Gradient(GradientRamp {
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread: self.gradient_spread,
|
gradient_spread: self.gradient_spread,
|
||||||
gradient_space: self.gradient_space,
|
gradient_space: self.gradient_space,
|
||||||
|
gradient_cyclic: self.gradient_cyclic,
|
||||||
gradient_hue_direction: self.gradient_hue_direction,
|
gradient_hue_direction: self.gradient_hue_direction,
|
||||||
..GradientRamp::from(&*gradient)
|
..GradientRamp::from(&*gradient)
|
||||||
}),
|
}),
|
||||||
@@ -378,7 +402,7 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
match update {
|
match update {
|
||||||
SpectrumInputUpdate::MoveMarker { index, position } => {
|
SpectrumInputUpdate::MoveMarker { index, position } => {
|
||||||
let new_index = gradient.move_stop(index as usize, position);
|
let new_index = gradient.move_stop(index as usize, position, self.gradient_cyclic);
|
||||||
if Some(index) == self.active_marker_index {
|
if Some(index) == self.active_marker_index {
|
||||||
self.active_marker_index = Some(new_index as u32);
|
self.active_marker_index = Some(new_index as u32);
|
||||||
}
|
}
|
||||||
@@ -390,7 +414,7 @@ impl ColorPickerMessageHandler {
|
|||||||
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
|
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
|
||||||
}
|
}
|
||||||
SpectrumInputUpdate::InsertMarker { position } => {
|
SpectrumInputUpdate::InsertMarker { position } => {
|
||||||
let new_index = gradient.insert_stop(position, self.gradient_space, self.gradient_hue_direction);
|
let new_index = gradient.insert_stop(position, self.gradient_cyclic, self.gradient_space, self.gradient_hue_direction);
|
||||||
self.active_marker_index = Some(new_index as u32);
|
self.active_marker_index = Some(new_index as u32);
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
if let Some(color) = gradient.color(new_index) {
|
if let Some(color) = gradient.color(new_index) {
|
||||||
@@ -400,7 +424,9 @@ impl ColorPickerMessageHandler {
|
|||||||
}
|
}
|
||||||
SpectrumInputUpdate::InsertDuplicate { index, position } => {
|
SpectrumInputUpdate::InsertDuplicate { index, position } => {
|
||||||
let source = index as usize;
|
let source = index as usize;
|
||||||
let Some(insert_index) = gradient.duplicate_stop(source, position) else { return };
|
let Some(insert_index) = gradient.duplicate_stop(source, position, self.gradient_cyclic) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
// The dragged stop (the duplication source) stays active. Its index shifts up if the frozen copy landed at or before it.
|
// The dragged stop (the duplication source) stays active. Its index shifts up if the frozen copy landed at or before it.
|
||||||
let dragged_index = if insert_index <= source { source + 1 } else { source };
|
let dragged_index = if insert_index <= source { source + 1 } else { source };
|
||||||
self.active_marker_index = Some(dragged_index as u32);
|
self.active_marker_index = Some(dragged_index as u32);
|
||||||
@@ -446,12 +472,13 @@ impl ColorPickerMessageHandler {
|
|||||||
if i >= count {
|
if i >= count {
|
||||||
return;
|
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.
|
// 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 (or fifths when cyclic, leaving the wrapped interval its share). 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 left = if i == 0 { 0. } else { gradient.position(i - 1, self.gradient_cyclic) };
|
||||||
let right = if i + 1 < count { gradient.position(i + 1) } else { 1. };
|
let right = if i + 1 < count { gradient.position(i + 1, self.gradient_cyclic) } else { 1. };
|
||||||
let natural = if count <= 1 { 0. } else { i as f64 / (count - 1) as f64 };
|
let denominator = if self.gradient_cyclic { count } else { count - 1 };
|
||||||
|
let natural = if count <= 1 { 0. } else { i as f64 / denominator as f64 };
|
||||||
let new_position = if (left..=right).contains(&natural) { natural } else { (left + right) / 2. };
|
let new_position = if (left..=right).contains(&natural) { natural } else { (left + right) / 2. };
|
||||||
let new_index = gradient.move_stop(i, new_position);
|
let new_index = gradient.move_stop(i, new_position, self.gradient_cyclic);
|
||||||
if Some(index) == self.active_marker_index {
|
if Some(index) == self.active_marker_index {
|
||||||
self.active_marker_index = Some(new_index as u32);
|
self.active_marker_index = Some(new_index as u32);
|
||||||
}
|
}
|
||||||
@@ -463,6 +490,7 @@ impl ColorPickerMessageHandler {
|
|||||||
value: FillChoice::Gradient(GradientRamp {
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread: self.gradient_spread,
|
gradient_spread: self.gradient_spread,
|
||||||
gradient_space: self.gradient_space,
|
gradient_space: self.gradient_space,
|
||||||
|
gradient_cyclic: self.gradient_cyclic,
|
||||||
gradient_hue_direction: self.gradient_hue_direction,
|
gradient_hue_direction: self.gradient_hue_direction,
|
||||||
..GradientRamp::from(&gradient)
|
..GradientRamp::from(&gradient)
|
||||||
}),
|
}),
|
||||||
@@ -491,10 +519,13 @@ impl ColorPickerMessageHandler {
|
|||||||
// Gradient editor (only present when the picker is in gradient mode)
|
// Gradient editor (only present when the picker is in gradient mode)
|
||||||
if let Some(gradient) = &self.gradient {
|
if let Some(gradient) = &self.gradient {
|
||||||
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
||||||
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
let markers = (0..gradient.len())
|
||||||
|
.filter_map(|i| Some(SpectrumMarker::new(gradient.position(i, self.gradient_cyclic), gradient.midpoint(i), gradient.color(i)?)))
|
||||||
|
.collect();
|
||||||
let mut row_widgets = vec![
|
let mut row_widgets = vec![
|
||||||
SpectrumInput::new(GradientStops::from(gradient))
|
SpectrumInput::new(GradientStops::from(gradient))
|
||||||
.track_space(self.gradient_space)
|
.track_space(self.gradient_space)
|
||||||
|
.track_cyclic(self.gradient_cyclic)
|
||||||
.track_hue_direction(self.gradient_hue_direction)
|
.track_hue_direction(self.gradient_hue_direction)
|
||||||
.markers(markers)
|
.markers(markers)
|
||||||
.active_marker_index(self.active_marker_index)
|
.active_marker_index(self.active_marker_index)
|
||||||
@@ -513,7 +544,7 @@ impl ColorPickerMessageHandler {
|
|||||||
let position_value = match (self.active_marker_is_midpoint, active_index < gradient.len()) {
|
let position_value = match (self.active_marker_is_midpoint, active_index < gradient.len()) {
|
||||||
(_, false) => 0.,
|
(_, false) => 0.,
|
||||||
(true, true) => gradient.midpoint(active_index),
|
(true, true) => gradient.midpoint(active_index),
|
||||||
(false, true) => gradient.position(active_index),
|
(false, true) => gradient.position(active_index, self.gradient_cyclic),
|
||||||
};
|
};
|
||||||
let is_midpoint = self.active_marker_is_midpoint;
|
let is_midpoint = self.active_marker_is_midpoint;
|
||||||
let captured_index = active;
|
let captured_index = active;
|
||||||
@@ -667,14 +698,31 @@ impl ColorPickerMessageHandler {
|
|||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Gradient spread (only present when the picker is in gradient mode)
|
// Gradient spread, trailed by the cyclic wrap toggle (only present when the picker is in gradient mode)
|
||||||
if self.gradient.is_some() {
|
if self.gradient.is_some() {
|
||||||
let entries = RadioEntryData::list_from_choice_type(|gradient_spread| ColorPickerMessage::SetGradientSpread { gradient_spread }.into());
|
let entries = RadioEntryData::list_from_choice_type(|gradient_spread| ColorPickerMessage::SetGradientSpread { gradient_spread }.into());
|
||||||
|
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Ends").tooltip_label("Gradient Spread").tooltip_description(ENDS_DESCRIPTION).widget_instance(),
|
TextLabel::new("Ends").tooltip_label("Gradient Spread / Cyclic").tooltip_description(ENDS_DESCRIPTION).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
RadioInput::new(entries).selected_index(Some(self.gradient_spread as u32)).disabled(self.disabled).widget_instance(),
|
RadioInput::new(entries)
|
||||||
|
.narrow(true)
|
||||||
|
.selected_index(Some(self.gradient_spread as u32))
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
CheckboxInput::new(self.gradient_cyclic)
|
||||||
|
.icon("Link")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Cyclic")
|
||||||
|
.tooltip_description(CYCLIC_DESCRIPTION)
|
||||||
|
.on_update(|checkbox_input: &CheckboxInput| {
|
||||||
|
ColorPickerMessage::SetGradientCyclic {
|
||||||
|
gradient_cyclic: checkbox_input.checked,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_instance(),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,7 +807,8 @@ const HUE_DESCRIPTION: &str = "The shade along the spectrum of the rainbow.";
|
|||||||
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
||||||
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
||||||
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
||||||
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends.";
|
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends, and whether its stops cycle back around from last to first.";
|
||||||
|
const CYCLIC_DESCRIPTION: &str = "Treats the stops as a cycle, interpolating from the last stop back around to the first.";
|
||||||
const SPACE_DESCRIPTION: &str = "The color space where stops interpolate toward their neighbors.";
|
const SPACE_DESCRIPTION: &str = "The color space where stops interpolate toward their neighbors.";
|
||||||
const HUE_DIRECTION_DESCRIPTION: &str = "Which way around the hue wheel the stops interpolate.";
|
const HUE_DIRECTION_DESCRIPTION: &str = "Which way around the hue wheel the stops interpolate.";
|
||||||
|
|
||||||
|
|||||||
@@ -531,9 +531,17 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
|||||||
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
||||||
}
|
}
|
||||||
Widget::SpectrumInput(spectrum_input) => {
|
Widget::SpectrumInput(spectrum_input) => {
|
||||||
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_space, spectrum_input.track_hue_direction);
|
spectrum_input.track_css = spectrum_input
|
||||||
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
.track
|
||||||
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
.to_css_linear_gradient(spectrum_input.track_cyclic, spectrum_input.track_space, spectrum_input.track_hue_direction);
|
||||||
|
// The end caps sample the track's boundary colors, which a cyclic wrap makes the wrapped interval's boundary-crossing color rather than the outermost stops'
|
||||||
|
let track_gradient = graphene_std::vector::style::Gradient::from(&spectrum_input.track);
|
||||||
|
let cap = |t: f64| {
|
||||||
|
let color = track_gradient.evaluate(t, Default::default(), spectrum_input.track_cyclic, spectrum_input.track_space, spectrum_input.track_hue_direction);
|
||||||
|
SRGBA8::from(color).to_css_hex()
|
||||||
|
};
|
||||||
|
spectrum_input.track_start_css = cap(0.);
|
||||||
|
spectrum_input.track_end_css = cap(1.);
|
||||||
}
|
}
|
||||||
Widget::ColorComparisonInput(comparison) => {
|
Widget::ColorComparisonInput(comparison) => {
|
||||||
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
|
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
|
||||||
|
|||||||
@@ -589,6 +589,9 @@ pub struct SpectrumInput {
|
|||||||
/// The color space the track's stops interpolate in, used to compute `track_css`. Not sent to the frontend.
|
/// The color space the track's stops interpolate in, used to compute `track_css`. Not sent to the frontend.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub track_space: GradientSpace,
|
pub track_space: GradientSpace,
|
||||||
|
/// Whether the track's stops wrap as a cycle, used to compute `track_css` and by the frontend to draw the wrapped interval's midpoint diamond.
|
||||||
|
#[serde(rename = "trackCyclic")]
|
||||||
|
pub track_cyclic: bool,
|
||||||
/// The hue direction the track's stops interpolate with in a polar space, used to compute `track_css`. Not sent to the frontend.
|
/// The hue direction the track's stops interpolate with in a polar space, used to compute `track_css`. Not sent to the frontend.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub track_hue_direction: GradientHueDirection,
|
pub track_hue_direction: GradientHueDirection,
|
||||||
@@ -596,11 +599,11 @@ pub struct SpectrumInput {
|
|||||||
#[serde(rename = "trackCSS")]
|
#[serde(rename = "trackCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub track_css: String,
|
pub track_css: String,
|
||||||
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated from `track`'s first stop.
|
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated by evaluating `track` at position 0.
|
||||||
#[serde(rename = "trackStartCSS")]
|
#[serde(rename = "trackStartCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub track_start_css: String,
|
pub track_start_css: String,
|
||||||
/// Hex string for the track strip's rightmost solid-color end-cap. Auto-populated from `track`'s last stop.
|
/// Hex string for the track strip's rightmost solid-color end-cap. Auto-populated by evaluating `track` at position 1.
|
||||||
#[serde(rename = "trackEndCSS")]
|
#[serde(rename = "trackEndCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub track_end_css: String,
|
pub track_end_css: String,
|
||||||
@@ -641,7 +644,8 @@ pub struct SpectrumInput {
|
|||||||
pub struct SpectrumMarker {
|
pub struct SpectrumMarker {
|
||||||
/// Position (0..1) of the marker along the spectrum track.
|
/// Position (0..1) of the marker along the spectrum track.
|
||||||
position: f64,
|
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 is ignored.
|
/// 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: f64,
|
midpoint: f64,
|
||||||
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
|
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
|
||||||
#[serde(rename = "handleColorCSS")]
|
#[serde(rename = "handleColorCSS")]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
|
|||||||
use graphene_std::vector::misc::{
|
use graphene_std::vector::misc::{
|
||||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientRamp, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
||||||
use graphene_std::{Artboard, Color, Context, Graphic};
|
use graphene_std::{Artboard, Color, Context, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
@@ -215,6 +215,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
List<GradientForm>,
|
List<GradientForm>,
|
||||||
List<GradientSpread>,
|
List<GradientSpread>,
|
||||||
List<GradientSpace>,
|
List<GradientSpace>,
|
||||||
|
List<GradientHueDirection>,
|
||||||
List<DashPattern>,
|
List<DashPattern>,
|
||||||
List<BoxCorners>,
|
List<BoxCorners>,
|
||||||
List<StrokeJoin>,
|
List<StrokeJoin>,
|
||||||
@@ -269,6 +270,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
Item<GradientForm>,
|
Item<GradientForm>,
|
||||||
Item<GradientSpread>,
|
Item<GradientSpread>,
|
||||||
Item<GradientSpace>,
|
Item<GradientSpace>,
|
||||||
|
Item<GradientHueDirection>,
|
||||||
Item<DashPattern>,
|
Item<DashPattern>,
|
||||||
Item<BoxCorners>,
|
Item<BoxCorners>,
|
||||||
Item<StrokeJoin>,
|
Item<StrokeJoin>,
|
||||||
@@ -1006,6 +1008,7 @@ impl_table_item_layout_for_choice_enum!(
|
|||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientSpace,
|
GradientSpace,
|
||||||
|
GradientHueDirection,
|
||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
@@ -1219,6 +1222,7 @@ macro_rules! known_item_types {
|
|||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientSpace,
|
GradientSpace,
|
||||||
|
GradientHueDirection,
|
||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ pub enum GraphOperationMessage {
|
|||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
},
|
},
|
||||||
@@ -67,6 +68,10 @@ pub enum GraphOperationMessage {
|
|||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
},
|
},
|
||||||
|
GradientCyclicSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_cyclic: bool,
|
||||||
|
},
|
||||||
GradientHueDirectionSet {
|
GradientHueDirectionSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
gradient_form,
|
gradient_form,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
transform,
|
transform,
|
||||||
} => {
|
} => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, gradient_hue_direction, transform);
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, gradient_cyclic, gradient_hue_direction, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
||||||
@@ -97,6 +98,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.gradient_space_set(gradient_space);
|
modify_inputs.gradient_space_set(gradient_space);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::GradientCyclicSet { layer, gradient_cyclic } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_cyclic_set(gradient_cyclic);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction } => {
|
GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
|
modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
|
||||||
@@ -977,7 +983,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
let gradient_spread = convert_gradient_spread(linear.spread_method());
|
let gradient_spread = convert_gradient_spread(linear.spread_method());
|
||||||
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
|
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
|
||||||
let gradient_space = gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma);
|
let gradient_space = gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma);
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, Default::default(), transform);
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, false, Default::default(), transform);
|
||||||
}
|
}
|
||||||
usvg::Paint::RadialGradient(radial) => {
|
usvg::Paint::RadialGradient(radial) => {
|
||||||
let gradient_transform = usvg_transform(radial.transform());
|
let gradient_transform = usvg_transform(radial.transform());
|
||||||
@@ -1003,7 +1009,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
let gradient_spread = convert_gradient_spread(radial.spread_method());
|
let gradient_spread = convert_gradient_spread(radial.spread_method());
|
||||||
let gradient_space = gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma);
|
let gradient_space = gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma);
|
||||||
|
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, Default::default(), transform);
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, false, Default::default(), transform);
|
||||||
}
|
}
|
||||||
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -432,12 +432,14 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn fill_gradient_set(
|
pub fn fill_gradient_set(
|
||||||
&mut self,
|
&mut self,
|
||||||
gradient: Gradient,
|
gradient: Gradient,
|
||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
) {
|
) {
|
||||||
@@ -450,6 +452,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let ramp = GradientRamp {
|
let ramp = GradientRamp {
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
..ramp
|
..ramp
|
||||||
};
|
};
|
||||||
@@ -791,6 +794,19 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the cyclic wrap flag on the chain's gradient value, which is where the ramp carries it.
|
||||||
|
pub fn gradient_cyclic_set(&mut self, gradient_cyclic: bool) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
|
let ramp = GradientRamp { gradient_cyclic, ..ramp };
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the hue direction on the chain's gradient value, which is where the ramp carries it.
|
/// Set the hue direction on the chain's gradient value, which is where the ramp carries it.
|
||||||
pub fn gradient_hue_direction_set(&mut self, gradient_hue_direction: GradientHueDirection) {
|
pub fn gradient_hue_direction_set(&mut self, gradient_hue_direction: GradientHueDirection) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|||||||
@@ -2408,6 +2408,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
/// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire.
|
/// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire.
|
||||||
@@ -2440,6 +2441,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
gradient_form: gradient.gradient_form,
|
gradient_form: gradient.gradient_form,
|
||||||
gradient_spread: gradient.gradient_spread,
|
gradient_spread: gradient.gradient_spread,
|
||||||
gradient_space: gradient.gradient_space,
|
gradient_space: gradient.gradient_space,
|
||||||
|
gradient_cyclic: gradient.gradient_cyclic,
|
||||||
gradient_hue_direction: gradient.gradient_hue_direction,
|
gradient_hue_direction: gradient.gradient_hue_direction,
|
||||||
transform: gradient.transform,
|
transform: gradient.transform,
|
||||||
transform_is_value: gradient.transform_is_value,
|
transform_is_value: gradient.transform_is_value,
|
||||||
@@ -2472,12 +2474,14 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
gradient: stops,
|
gradient: stops,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let stops = stops.clone();
|
let stops = stops.clone();
|
||||||
let gradient_spread = *gradient_spread;
|
let gradient_spread = *gradient_spread;
|
||||||
let gradient_space = *gradient_space;
|
let gradient_space = *gradient_space;
|
||||||
|
let gradient_cyclic = *gradient_cyclic;
|
||||||
let gradient_hue_direction = *gradient_hue_direction;
|
let gradient_hue_direction = *gradient_hue_direction;
|
||||||
|
|
||||||
let reverse_button = IconButton::new("Reverse", 24)
|
let reverse_button = IconButton::new("Reverse", 24)
|
||||||
@@ -2488,8 +2492,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
TaggedValue::GradientRamp(GradientRamp {
|
TaggedValue::GradientRamp(GradientRamp {
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
..GradientRamp::from(stops.reversed())
|
..GradientRamp::from(stops.reversed(gradient_cyclic))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
node_id,
|
node_id,
|
||||||
@@ -2514,11 +2519,13 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
gradient: stops,
|
gradient: stops,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
..
|
..
|
||||||
} => FillChoice::<SRGBA8>::Gradient(GradientRamp {
|
} => FillChoice::<SRGBA8>::Gradient(GradientRamp {
|
||||||
gradient_spread: *gradient_spread,
|
gradient_spread: *gradient_spread,
|
||||||
gradient_space: *gradient_space,
|
gradient_space: *gradient_space,
|
||||||
|
gradient_cyclic: *gradient_cyclic,
|
||||||
gradient_hue_direction: *gradient_hue_direction,
|
gradient_hue_direction: *gradient_hue_direction,
|
||||||
..GradientRamp::from(stops)
|
..GradientRamp::from(stops)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -407,6 +407,11 @@ pub fn get_chain_source_gradient_space(layer: LayerNodeIdentifier, network_inter
|
|||||||
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_space)
|
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_space)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The cyclic wrap flag baked into the 'Gradient Value' node feeding a layer's chain.
|
||||||
|
pub fn get_chain_source_gradient_cyclic(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
|
||||||
|
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_cyclic)
|
||||||
|
}
|
||||||
|
|
||||||
/// The hue direction baked into the 'Gradient Value' node feeding a layer's chain.
|
/// The hue direction baked into the 'Gradient Value' node feeding a layer's chain.
|
||||||
pub fn get_chain_source_gradient_hue_direction(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientHueDirection> {
|
pub fn get_chain_source_gradient_hue_direction(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientHueDirection> {
|
||||||
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_hue_direction)
|
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_hue_direction)
|
||||||
@@ -768,6 +773,7 @@ pub struct FillNodeGradient {
|
|||||||
pub gradient_form: GradientForm,
|
pub gradient_form: GradientForm,
|
||||||
pub gradient_spread: GradientSpread,
|
pub gradient_spread: GradientSpread,
|
||||||
pub gradient_space: GradientSpace,
|
pub gradient_space: GradientSpace,
|
||||||
|
pub gradient_cyclic: bool,
|
||||||
pub gradient_hue_direction: GradientHueDirection,
|
pub gradient_hue_direction: GradientHueDirection,
|
||||||
pub transform: DAffine2,
|
pub transform: DAffine2,
|
||||||
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
|
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
|
||||||
@@ -783,6 +789,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
|
|||||||
};
|
};
|
||||||
let gradient_spread = ramp.gradient_spread;
|
let gradient_spread = ramp.gradient_spread;
|
||||||
let gradient_space = ramp.gradient_space;
|
let gradient_space = ramp.gradient_space;
|
||||||
|
let gradient_cyclic = ramp.gradient_cyclic;
|
||||||
let gradient_hue_direction = ramp.gradient_hue_direction;
|
let gradient_hue_direction = ramp.gradient_hue_direction;
|
||||||
let stops = Gradient::from(ramp);
|
let stops = Gradient::from(ramp);
|
||||||
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) {
|
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) {
|
||||||
@@ -802,6 +809,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
|
|||||||
gradient_form,
|
gradient_form,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
transform,
|
transform,
|
||||||
transform_is_value: transform_input.is_some(),
|
transform_is_value: transform_input.is_some(),
|
||||||
@@ -953,6 +961,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
|
|||||||
gradient_form,
|
gradient_form,
|
||||||
gradient_spread: ramp.gradient_spread,
|
gradient_spread: ramp.gradient_spread,
|
||||||
gradient_space: ramp.gradient_space,
|
gradient_space: ramp.gradient_space,
|
||||||
|
gradient_cyclic: ramp.gradient_cyclic,
|
||||||
gradient_hue_direction: ramp.gradient_hue_direction,
|
gradient_hue_direction: ramp.gradient_hue_direction,
|
||||||
transform,
|
transform,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
|
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||||
self, NodeGraphLayer, get_chain_source_gradient_hue_direction, get_chain_source_gradient_space, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops,
|
self, NodeGraphLayer, get_chain_source_gradient_cyclic, get_chain_source_gradient_hue_direction, get_chain_source_gradient_space, get_chain_source_gradient_spread,
|
||||||
get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description,
|
get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description,
|
||||||
};
|
};
|
||||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||||
use glam::DMat2;
|
use glam::DMat2;
|
||||||
@@ -31,6 +31,7 @@ pub struct GradientOptions {
|
|||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +99,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
responses.add(ToolMessage::UpdateCursor);
|
responses.add(ToolMessage::UpdateCursor);
|
||||||
}
|
}
|
||||||
GradientOptionsUpdate::ReverseStops => {
|
GradientOptionsUpdate::ReverseStops => {
|
||||||
apply_gradient_update(&mut self.data, context, responses, |_| true, |(gradient, _appearance)| *gradient = gradient.reversed());
|
apply_gradient_update(
|
||||||
|
&mut self.data,
|
||||||
|
context,
|
||||||
|
responses,
|
||||||
|
|_| true,
|
||||||
|
|(gradient, appearance)| *gradient = gradient.reversed(appearance.gradient_cyclic),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GradientOptionsUpdate::ReverseDirection => apply_gradient_update(
|
GradientOptionsUpdate::ReverseDirection => apply_gradient_update(
|
||||||
&mut self.data,
|
&mut self.data,
|
||||||
@@ -141,6 +148,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
let ramp = GradientRamp::from(&ramp);
|
let ramp = GradientRamp::from(&ramp);
|
||||||
self.options.gradient_spread = ramp.gradient_spread;
|
self.options.gradient_spread = ramp.gradient_spread;
|
||||||
self.options.gradient_space = ramp.gradient_space;
|
self.options.gradient_space = ramp.gradient_space;
|
||||||
|
self.options.gradient_cyclic = ramp.gradient_cyclic;
|
||||||
self.options.gradient_hue_direction = ramp.gradient_hue_direction;
|
self.options.gradient_hue_direction = ramp.gradient_hue_direction;
|
||||||
apply_stops_update(
|
apply_stops_update(
|
||||||
&mut self.data,
|
&mut self.data,
|
||||||
@@ -149,6 +157,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
Gradient::from(&ramp),
|
Gradient::from(&ramp),
|
||||||
ramp.gradient_spread,
|
ramp.gradient_spread,
|
||||||
ramp.gradient_space,
|
ramp.gradient_space,
|
||||||
|
ramp.gradient_cyclic,
|
||||||
ramp.gradient_hue_direction,
|
ramp.gradient_hue_direction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,6 +201,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
self.options.gradient_space = appearance.gradient_space;
|
self.options.gradient_space = appearance.gradient_space;
|
||||||
needs_refresh = true;
|
needs_refresh = true;
|
||||||
}
|
}
|
||||||
|
if self.options.gradient_cyclic != appearance.gradient_cyclic {
|
||||||
|
self.options.gradient_cyclic = appearance.gradient_cyclic;
|
||||||
|
needs_refresh = true;
|
||||||
|
}
|
||||||
if self.options.gradient_hue_direction != appearance.gradient_hue_direction {
|
if self.options.gradient_hue_direction != appearance.gradient_hue_direction {
|
||||||
self.options.gradient_hue_direction = appearance.gradient_hue_direction;
|
self.options.gradient_hue_direction = appearance.gradient_hue_direction;
|
||||||
needs_refresh = true;
|
needs_refresh = true;
|
||||||
@@ -277,6 +290,7 @@ impl LayoutHolder for GradientTool {
|
|||||||
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
|
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread: self.options.gradient_spread,
|
gradient_spread: self.options.gradient_spread,
|
||||||
gradient_space: self.options.gradient_space,
|
gradient_space: self.options.gradient_space,
|
||||||
|
gradient_cyclic: self.options.gradient_cyclic,
|
||||||
gradient_hue_direction: self.options.gradient_hue_direction,
|
gradient_hue_direction: self.options.gradient_hue_direction,
|
||||||
..GradientRamp::from(&stops_value)
|
..GradientRamp::from(&stops_value)
|
||||||
}))
|
}))
|
||||||
@@ -379,6 +393,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
|||||||
gradient_form: gradient.gradient_form,
|
gradient_form: gradient.gradient_form,
|
||||||
gradient_spread: gradient.gradient_spread,
|
gradient_spread: gradient.gradient_spread,
|
||||||
gradient_space: gradient.gradient_space,
|
gradient_space: gradient.gradient_space,
|
||||||
|
gradient_cyclic: gradient.gradient_cyclic,
|
||||||
gradient_hue_direction: gradient.gradient_hue_direction,
|
gradient_hue_direction: gradient.gradient_hue_direction,
|
||||||
transform: gradient.transform,
|
transform: gradient.transform,
|
||||||
},
|
},
|
||||||
@@ -400,6 +415,7 @@ struct GradientAppearance {
|
|||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +458,7 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod
|
|||||||
gradient_form: gradient_form.unwrap_or_default(),
|
gradient_form: gradient_form.unwrap_or_default(),
|
||||||
gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(),
|
gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(),
|
||||||
gradient_space: get_chain_source_gradient_space(layer, network_interface).unwrap_or_default(),
|
gradient_space: get_chain_source_gradient_space(layer, network_interface).unwrap_or_default(),
|
||||||
|
gradient_cyclic: get_chain_source_gradient_cyclic(layer, network_interface).unwrap_or_default(),
|
||||||
gradient_hue_direction: get_chain_source_gradient_hue_direction(layer, network_interface).unwrap_or_default(),
|
gradient_hue_direction: get_chain_source_gradient_hue_direction(layer, network_interface).unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,6 +496,70 @@ fn midpoint_hidden_by_proximity(left_stop_pos: f64, right_stop_pos: f64, viewpor
|
|||||||
(right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2.
|
(right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cyclic gradient's wrapped interval as `(start, end)` along the gradient line, where `end` runs past 1 by however far
|
||||||
|
/// the interval continues beyond the boundary to reach the first stop.
|
||||||
|
fn wrapped_interval_span(gradient: &Gradient) -> (f64, f64) {
|
||||||
|
(gradient.position(gradient.len() - 1, true), gradient.position(0, true) + 1.)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gradient's visible midpoint diamonds as `(owning stop index, position along the gradient line)`, omitting intervals
|
||||||
|
/// whose stops are too closely packed. A cyclic gradient's wrapped interval adds a final diamond owned by the last stop.
|
||||||
|
fn midpoint_diamonds(gradient: &Gradient, gradient_cyclic: bool, viewport_line_length: f64) -> Vec<(usize, f64)> {
|
||||||
|
let mut diamonds = Vec::with_capacity(gradient.len());
|
||||||
|
|
||||||
|
for index in 0..gradient.len().saturating_sub(1) {
|
||||||
|
let left = gradient.position(index, gradient_cyclic);
|
||||||
|
let right = gradient.position(index + 1, gradient_cyclic);
|
||||||
|
if midpoint_hidden_by_proximity(left, right, viewport_line_length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
diamonds.push((index, left + gradient.midpoint(index) * (right - left)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if gradient_cyclic && gradient.len() >= 2 {
|
||||||
|
let last = gradient.len() - 1;
|
||||||
|
let (left, right) = wrapped_interval_span(gradient);
|
||||||
|
if !midpoint_hidden_by_proximity(left, right, viewport_line_length) {
|
||||||
|
diamonds.push((last, (left + gradient.midpoint(last) * (right - left)).rem_euclid(1.)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diamonds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps a pointer's unclamped position along the gradient line to a midpoint ratio within the interval owned by the
|
||||||
|
/// stop at `index`, or `None` when that interval has no width. The wrapped interval continues past the line's end,
|
||||||
|
/// so overdragging keeps tracking with the line's length as an offset.
|
||||||
|
fn midpoint_ratio_at(gradient: &Gradient, index: usize, gradient_cyclic: bool, position_along_line: f64) -> Option<f64> {
|
||||||
|
let is_wrapped_interval = gradient_cyclic && index + 1 == gradient.len();
|
||||||
|
let (left, right) = if is_wrapped_interval {
|
||||||
|
wrapped_interval_span(gradient)
|
||||||
|
} else {
|
||||||
|
(gradient.position(index, gradient_cyclic), gradient.position(index + 1, gradient_cyclic))
|
||||||
|
};
|
||||||
|
|
||||||
|
let span = right - left;
|
||||||
|
if span <= 0. {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = gradient.position(0, gradient_cyclic);
|
||||||
|
let dead_zone_split = match (is_wrapped_interval, left >= 1., first > 0.) {
|
||||||
|
(true, true, _) => f64::INFINITY,
|
||||||
|
(true, false, true) => (first + left) / 2.,
|
||||||
|
_ => f64::NEG_INFINITY,
|
||||||
|
};
|
||||||
|
|
||||||
|
let local = if position_along_line < dead_zone_split {
|
||||||
|
position_along_line + 1. - left
|
||||||
|
} else {
|
||||||
|
position_along_line - left
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((local / span).clamp(GRADIENT_MIDPOINT_MIN, GRADIENT_MIDPOINT_MAX))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
|
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
|
||||||
pub enum GradientDragTarget {
|
pub enum GradientDragTarget {
|
||||||
Start,
|
Start,
|
||||||
@@ -504,13 +585,13 @@ struct SelectedGradient {
|
|||||||
is_gradient_chain: bool,
|
is_gradient_chain: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, mouse: DVec2) -> Option<f64> {
|
fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, gradient_cyclic: bool, mouse: DVec2) -> Option<f64> {
|
||||||
let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length();
|
let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length();
|
||||||
let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
||||||
|
|
||||||
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
|
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
|
||||||
for stop in stops {
|
for i in 0..stops.len() {
|
||||||
let stop_pos = start.lerp(end, stop.position);
|
let stop_pos = start.lerp(end, stops.position(i, gradient_cyclic));
|
||||||
if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) {
|
if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -521,16 +602,8 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, mouse: DVec2)
|
|||||||
|
|
||||||
// Don't insert when clicking near a (currently visible) midpoint diamond
|
// Don't insert when clicking near a (currently visible) midpoint diamond
|
||||||
let line_length = start.distance(end);
|
let line_length = start.distance(end);
|
||||||
for i in 0..stops.len().saturating_sub(1) {
|
for (_, midpoint_position) in midpoint_diamonds(stops, gradient_cyclic, line_length) {
|
||||||
let left = stops.position(i);
|
let midpoint_viewport = start.lerp(end, midpoint_position);
|
||||||
let right = stops.position(i + 1);
|
|
||||||
|
|
||||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let midpoint_pos = left + stops.midpoint(i) * (right - left);
|
|
||||||
let midpoint_viewport = start.lerp(end, midpoint_pos);
|
|
||||||
if midpoint_viewport.distance_squared(mouse) < GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2) {
|
if midpoint_viewport.distance_squared(mouse) < GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -697,18 +770,19 @@ impl SelectedGradient {
|
|||||||
let min_gap = GRADIENT_STOP_MIN_VIEWPORT_GAP / line_length;
|
let min_gap = GRADIENT_STOP_MIN_VIEWPORT_GAP / line_length;
|
||||||
let last_index = self.gradient.len() - 1;
|
let last_index = self.gradient.len() - 1;
|
||||||
|
|
||||||
let has_other_stop_at_zero = stop != 0 && !self.gradient.is_empty() && self.gradient.position(0).abs() < f64::EPSILON * 1000.;
|
let gradient_cyclic = self.appearance.gradient_cyclic;
|
||||||
let has_other_stop_at_one = stop != last_index && !self.gradient.is_empty() && (1. - self.gradient.position(last_index)).abs() < f64::EPSILON * 1000.;
|
let has_other_stop_at_zero = stop != 0 && !self.gradient.is_empty() && self.gradient.position(0, gradient_cyclic).abs() < f64::EPSILON * 1000.;
|
||||||
|
let has_other_stop_at_one = stop != last_index && !self.gradient.is_empty() && (1. - self.gradient.position(last_index, gradient_cyclic)).abs() < f64::EPSILON * 1000.;
|
||||||
|
|
||||||
let left_bound = if has_other_stop_at_zero { min_gap } else { 0. };
|
let left_bound = if has_other_stop_at_zero { min_gap } else { 0. };
|
||||||
let right_bound = if has_other_stop_at_one { 1. - min_gap } else { 1. };
|
let right_bound = if has_other_stop_at_one { 1. - min_gap } else { 1. };
|
||||||
|
|
||||||
let clamped = new_pos.clamp(left_bound, right_bound);
|
let clamped = new_pos.clamp(left_bound, right_bound);
|
||||||
self.gradient.set_position(stop, clamped);
|
self.gradient.set_position(stop, clamped, gradient_cyclic);
|
||||||
let new_position = clamped;
|
let new_position = clamped;
|
||||||
let new_color = self.gradient.color(stop).unwrap_or(Color::BLACK);
|
let new_color = self.gradient.color(stop).unwrap_or(Color::BLACK);
|
||||||
|
|
||||||
self.gradient.sort();
|
self.gradient.sort(gradient_cyclic);
|
||||||
if let Some(new_index) = self.gradient.iter().position(|s| s.position == new_position && s.color == new_color) {
|
if let Some(new_index) = self.gradient.iter().position(|s| s.position == new_position && s.color == new_color) {
|
||||||
self.dragging = GradientDragTarget::Stop(new_index);
|
self.dragging = GradientDragTarget::Stop(new_index);
|
||||||
}
|
}
|
||||||
@@ -755,12 +829,8 @@ impl SelectedGradient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to a midpoint ratio within the interval between the two surrounding stops
|
// Convert to a midpoint ratio within the interval owned by the dragged diamond's stop
|
||||||
let left_stop = self.gradient.position(midpoint_index);
|
if let Some(midpoint_ratio) = midpoint_ratio_at(&self.gradient, midpoint_index, self.appearance.gradient_cyclic, full_pos) {
|
||||||
let right_stop = self.gradient.position(midpoint_index + 1);
|
|
||||||
let range = right_stop - left_stop;
|
|
||||||
if range > 0. {
|
|
||||||
let midpoint_ratio = ((full_pos - left_stop) / range).clamp(GRADIENT_MIDPOINT_MIN, GRADIENT_MIDPOINT_MAX);
|
|
||||||
self.gradient.set_midpoint(midpoint_index, midpoint_ratio);
|
self.gradient.set_midpoint(midpoint_index, midpoint_ratio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -780,6 +850,7 @@ impl SelectedGradient {
|
|||||||
gradient_form: self.appearance.gradient_form,
|
gradient_form: self.appearance.gradient_form,
|
||||||
gradient_spread: self.appearance.gradient_spread,
|
gradient_spread: self.appearance.gradient_spread,
|
||||||
gradient_space: self.appearance.gradient_space,
|
gradient_space: self.appearance.gradient_space,
|
||||||
|
gradient_cyclic: self.appearance.gradient_cyclic,
|
||||||
gradient_hue_direction: self.appearance.gradient_hue_direction,
|
gradient_hue_direction: self.appearance.gradient_hue_direction,
|
||||||
transform: self.appearance.transform,
|
transform: self.appearance.transform,
|
||||||
});
|
});
|
||||||
@@ -801,7 +872,7 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
|
|||||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: gradient.clone() });
|
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: gradient.clone() });
|
||||||
responses.add(GraphOperationMessage::GradientPositionsSet {
|
responses.add(GraphOperationMessage::GradientPositionsSet {
|
||||||
layer,
|
layer,
|
||||||
positions: gradient.nondefault_positions().unwrap_or_default(),
|
positions: gradient.nondefault_positions(appearance.gradient_cyclic).unwrap_or_default(),
|
||||||
});
|
});
|
||||||
responses.add(GraphOperationMessage::GradientMidpointsSet {
|
responses.add(GraphOperationMessage::GradientMidpointsSet {
|
||||||
layer,
|
layer,
|
||||||
@@ -823,6 +894,10 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
|
|||||||
layer,
|
layer,
|
||||||
gradient_space: appearance.gradient_space,
|
gradient_space: appearance.gradient_space,
|
||||||
});
|
});
|
||||||
|
responses.add(GraphOperationMessage::GradientCyclicSet {
|
||||||
|
layer,
|
||||||
|
gradient_cyclic: appearance.gradient_cyclic,
|
||||||
|
});
|
||||||
responses.add(GraphOperationMessage::GradientHueDirectionSet {
|
responses.add(GraphOperationMessage::GradientHueDirectionSet {
|
||||||
layer,
|
layer,
|
||||||
gradient_hue_direction: appearance.gradient_hue_direction,
|
gradient_hue_direction: appearance.gradient_hue_direction,
|
||||||
@@ -930,8 +1005,9 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let end_hex = gradient.color(gradient.len().saturating_sub(1)).map(color_to_hex).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
let end_hex = gradient.color(gradient.len().saturating_sub(1)).map(color_to_hex).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||||
|
|
||||||
// Check if the first/last stops are at position ~0/~1 (rendered as the endpoint dots rather than as separate stops)
|
// Check if the first/last stops are at position ~0/~1 (rendered as the endpoint dots rather than as separate stops)
|
||||||
let first_at_start = !gradient.is_empty() && gradient.position(0).abs() < f64::EPSILON * 1000.;
|
let gradient_cyclic = appearance.gradient_cyclic;
|
||||||
let last_at_end = !gradient.is_empty() && (1. - gradient.position(gradient.len() - 1)).abs() < f64::EPSILON * 1000.;
|
let first_at_start = !gradient.is_empty() && gradient.position(0, gradient_cyclic).abs() < f64::EPSILON * 1000.;
|
||||||
|
let last_at_end = !gradient.is_empty() && (1. - gradient.position(gradient.len() - 1, gradient_cyclic)).abs() < f64::EPSILON * 1000.;
|
||||||
|
|
||||||
overlay_context.line(start, end, None, None);
|
overlay_context.line(start, end, None, None);
|
||||||
|
|
||||||
@@ -956,11 +1032,12 @@ impl Fsm for GradientToolFsmState {
|
|||||||
};
|
};
|
||||||
check(start.distance_squared(mouse), StopId::Start);
|
check(start.distance_squared(mouse), StopId::Start);
|
||||||
check(end.distance_squared(mouse), StopId::End);
|
check(end.distance_squared(mouse), StopId::End);
|
||||||
for (index, stop) in gradient.iter().enumerate() {
|
for index in 0..gradient.len() {
|
||||||
if stop.position.abs() < f64::EPSILON * 1000. || (1. - stop.position).abs() < f64::EPSILON * 1000. {
|
let position = gradient.position(index, gradient_cyclic);
|
||||||
|
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
check(start.lerp(end, stop.position).distance_squared(mouse), StopId::Middle(index));
|
check(start.lerp(end, position).distance_squared(mouse), StopId::Middle(index));
|
||||||
}
|
}
|
||||||
best.map(|(_, id)| id)
|
best.map(|(_, id)| id)
|
||||||
} else {
|
} else {
|
||||||
@@ -982,8 +1059,8 @@ impl Fsm for GradientToolFsmState {
|
|||||||
StopId::Start => overlay_context.gradient_color_stop(start, emphasis, &start_hex, !first_at_start),
|
StopId::Start => overlay_context.gradient_color_stop(start, emphasis, &start_hex, !first_at_start),
|
||||||
StopId::End => overlay_context.gradient_color_stop(end, emphasis, &end_hex, !last_at_end),
|
StopId::End => overlay_context.gradient_color_stop(end, emphasis, &end_hex, !last_at_end),
|
||||||
StopId::Middle(i) => {
|
StopId::Middle(i) => {
|
||||||
if let Some(stop) = gradient.iter().nth(i) {
|
if let Some(color) = gradient.color(i) {
|
||||||
overlay_context.gradient_color_stop(start.lerp(end, stop.position), emphasis, &color_to_hex(stop.color), false);
|
overlay_context.gradient_color_stop(start.lerp(end, gradient.position(i, gradient_cyclic)), emphasis, &color_to_hex(color), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -995,8 +1072,9 @@ impl Fsm for GradientToolFsmState {
|
|||||||
if !is_deferred(StopId::End) {
|
if !is_deferred(StopId::End) {
|
||||||
draw_stop(StopId::End, emphasis_for(StopId::End));
|
draw_stop(StopId::End, emphasis_for(StopId::End));
|
||||||
}
|
}
|
||||||
for (index, stop) in gradient.iter().enumerate() {
|
for index in 0..gradient.len() {
|
||||||
if stop.position.abs() < f64::EPSILON * 1000. || (1. - stop.position).abs() < f64::EPSILON * 1000. {
|
let position = gradient.position(index, gradient_cyclic);
|
||||||
|
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let id = StopId::Middle(index);
|
let id = StopId::Middle(index);
|
||||||
@@ -1022,18 +1100,10 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let line_angle = (end - start).to_angle();
|
let line_angle = (end - start).to_angle();
|
||||||
let line_length = start.distance(end);
|
let line_length = start.distance(end);
|
||||||
let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2);
|
let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2);
|
||||||
for i in 0..gradient.len().saturating_sub(1) {
|
for (index, midpoint_position) in midpoint_diamonds(gradient, gradient_cyclic, line_length) {
|
||||||
let left = gradient.position(i);
|
let midpoint_viewport = start.lerp(end, midpoint_position);
|
||||||
let right = gradient.position(i + 1);
|
|
||||||
|
|
||||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
let emphasis = if dragging == Some(GradientDragTarget::Midpoint(index)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let midpoint_pos = left + gradient.midpoint(i) * (right - left);
|
|
||||||
let midpoint_viewport = start.lerp(end, midpoint_pos);
|
|
||||||
|
|
||||||
let emphasis = if dragging == Some(GradientDragTarget::Midpoint(i)) {
|
|
||||||
GizmoEmphasis::Active
|
GizmoEmphasis::Active
|
||||||
} else if !matches!(self, GradientToolFsmState::Drawing { .. }) && midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
} else if !matches!(self, GradientToolFsmState::Drawing { .. }) && midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
||||||
GizmoEmphasis::Hovered
|
GizmoEmphasis::Hovered
|
||||||
@@ -1044,7 +1114,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(self, GradientToolFsmState::Drawing { .. })
|
if !matches!(self, GradientToolFsmState::Drawing { .. })
|
||||||
&& calculate_insertion(start, end, gradient, mouse).is_some()
|
&& calculate_insertion(start, end, gradient, gradient_cyclic, mouse).is_some()
|
||||||
&& let Some(dir) = (end - start).try_normalize()
|
&& let Some(dir) = (end - start).try_normalize()
|
||||||
{
|
{
|
||||||
let perp = dir.perp();
|
let perp = dir.perp();
|
||||||
@@ -1093,7 +1163,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let gradient = &selected_gradient.gradient;
|
let gradient = &selected_gradient.gradient;
|
||||||
if stop_index < gradient.len() {
|
if stop_index < gradient.len() {
|
||||||
let color = gradient.color(stop_index).unwrap_or(Color::BLACK);
|
let color = gradient.color(stop_index).unwrap_or(Color::BLACK);
|
||||||
let position = gradient.position(stop_index);
|
let position = gradient.position(stop_index, selected_gradient.appearance.gradient_cyclic);
|
||||||
let start = transform.transform_point2(DVec2::ZERO);
|
let start = transform.transform_point2(DVec2::ZERO);
|
||||||
let end = transform.transform_point2(DVec2::X);
|
let end = transform.transform_point2(DVec2::X);
|
||||||
let position = start.lerp(end, position).into();
|
let position = start.lerp(end, position).into();
|
||||||
@@ -1133,10 +1203,11 @@ impl Fsm for GradientToolFsmState {
|
|||||||
GradientDragTarget::Start | GradientDragTarget::End | GradientDragTarget::Stop(_) => {
|
GradientDragTarget::Start | GradientDragTarget::End | GradientDragTarget::Stop(_) => {
|
||||||
// Find the stop index from the drag target
|
// Find the stop index from the drag target
|
||||||
let gradient = &selected_gradient.gradient;
|
let gradient = &selected_gradient.gradient;
|
||||||
|
let gradient_cyclic = selected_gradient.appearance.gradient_cyclic;
|
||||||
let stop_index = match selected_gradient.dragging {
|
let stop_index = match selected_gradient.dragging {
|
||||||
GradientDragTarget::Stop(i) => Some(i),
|
GradientDragTarget::Stop(i) => Some(i),
|
||||||
GradientDragTarget::Start => (0..gradient.len()).position(|i| gradient.position(i).abs() < f64::EPSILON * 1000.),
|
GradientDragTarget::Start => (0..gradient.len()).position(|i| gradient.position(i, gradient_cyclic).abs() < f64::EPSILON * 1000.),
|
||||||
GradientDragTarget::End => (0..gradient.len()).position(|i| (1. - gradient.position(i)).abs() < f64::EPSILON * 1000.),
|
GradientDragTarget::End => (0..gradient.len()).position(|i| (1. - gradient.position(i, gradient_cyclic)).abs() < f64::EPSILON * 1000.),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(stop_index) = stop_index
|
if let Some(stop_index) = stop_index
|
||||||
@@ -1148,7 +1219,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
tool_data.color_picker_transaction_open = false;
|
tool_data.color_picker_transaction_open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stop_pos = selected_gradient.gradient.position(stop_index);
|
let stop_pos = selected_gradient.gradient.position(stop_index, selected_gradient.appearance.gradient_cyclic);
|
||||||
let (start, end) = selected_gradient.viewport_handle_positions();
|
let (start, end) = selected_gradient.viewport_handle_positions();
|
||||||
let viewport_pos = start.lerp(end, stop_pos);
|
let viewport_pos = start.lerp(end, stop_pos);
|
||||||
let position = viewport_pos.into();
|
let position = viewport_pos.into();
|
||||||
@@ -1191,7 +1262,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
match selected_gradient.dragging {
|
match selected_gradient.dragging {
|
||||||
GradientDragTarget::Start => {
|
GradientDragTarget::Start => {
|
||||||
// Only delete if there's a real color stop at position ~0 (not the endpoint of the line which isn't itself a color stop)
|
// Only delete if there's a real color stop at position ~0 (not the endpoint of the line which isn't itself a color stop)
|
||||||
if !selected_gradient.gradient.is_empty() && selected_gradient.gradient.position(0).abs() < f64::EPSILON * 1000. {
|
if !selected_gradient.gradient.is_empty() && selected_gradient.gradient.position(0, selected_gradient.appearance.gradient_cyclic).abs() < f64::EPSILON * 1000. {
|
||||||
selected_gradient.gradient.remove(0);
|
selected_gradient.gradient.remove(0);
|
||||||
} else {
|
} else {
|
||||||
responses.add(DocumentMessage::AbortTransaction);
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
@@ -1200,7 +1271,9 @@ impl Fsm for GradientToolFsmState {
|
|||||||
}
|
}
|
||||||
GradientDragTarget::End => {
|
GradientDragTarget::End => {
|
||||||
// Only delete if there's a real color stop at position ~1 (not the endpoint of the line which isn't itself a color stop)
|
// Only delete if there's a real color stop at position ~1 (not the endpoint of the line which isn't itself a color stop)
|
||||||
if !selected_gradient.gradient.is_empty() && (1. - selected_gradient.gradient.position(selected_gradient.gradient.len() - 1)).abs() < f64::EPSILON * 1000. {
|
if !selected_gradient.gradient.is_empty()
|
||||||
|
&& (1. - selected_gradient.gradient.position(selected_gradient.gradient.len() - 1, selected_gradient.appearance.gradient_cyclic)).abs() < f64::EPSILON * 1000.
|
||||||
|
{
|
||||||
let _ = selected_gradient.gradient.pop();
|
let _ = selected_gradient.gradient.pop();
|
||||||
} else {
|
} else {
|
||||||
responses.add(DocumentMessage::AbortTransaction);
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
@@ -1241,7 +1314,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the minimum and maximum positions
|
// Find the minimum and maximum positions
|
||||||
let positions = selected_gradient.gradient.positions();
|
let positions = selected_gradient.gradient.positions(selected_gradient.appearance.gradient_cyclic);
|
||||||
let min_position = positions.iter().copied().reduce(f64::min).expect("No min");
|
let min_position = positions.iter().copied().reduce(f64::min).expect("No min");
|
||||||
let max_position = positions.iter().copied().reduce(f64::max).expect("No max");
|
let max_position = positions.iter().copied().reduce(f64::max).expect("No max");
|
||||||
|
|
||||||
@@ -1277,7 +1350,14 @@ impl Fsm for GradientToolFsmState {
|
|||||||
// If click is on the line then insert point
|
// If click is on the line then insert point
|
||||||
if distance < (SELECTION_THRESHOLD * 2.) {
|
if distance < (SELECTION_THRESHOLD * 2.) {
|
||||||
// Try and insert the new stop
|
// Try and insert the new stop
|
||||||
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.gradient_space, appearance.gradient_hue_direction) {
|
if let Some(index) = insert_stop_at_point(
|
||||||
|
&mut gradient,
|
||||||
|
mouse,
|
||||||
|
unit_to_viewport,
|
||||||
|
appearance.gradient_cyclic,
|
||||||
|
appearance.gradient_space,
|
||||||
|
appearance.gradient_hue_direction,
|
||||||
|
) {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
|
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
|
||||||
@@ -1330,19 +1410,11 @@ impl Fsm for GradientToolFsmState {
|
|||||||
if drag_hint.is_none() {
|
if drag_hint.is_none() {
|
||||||
let line_length = start.distance(end);
|
let line_length = start.distance(end);
|
||||||
let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2);
|
let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2);
|
||||||
for i in 0..gradient.len().saturating_sub(1) {
|
for (index, midpoint_position) in midpoint_diamonds(&gradient, appearance.gradient_cyclic, line_length) {
|
||||||
let left = gradient.position(i);
|
let midpoint_viewport = start.lerp(end, midpoint_position);
|
||||||
let right = gradient.position(i + 1);
|
|
||||||
|
|
||||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let midpoint_pos = left + gradient.midpoint(i) * (right - left);
|
|
||||||
let midpoint_viewport = start.lerp(end, midpoint_pos);
|
|
||||||
|
|
||||||
if midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
if midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
||||||
let resettable = midpoint_is_resettable(gradient.midpoint(i));
|
let resettable = midpoint_is_resettable(gradient.midpoint(index));
|
||||||
drag_hint = Some(GradientDragHintState::Midpoint { resettable });
|
drag_hint = Some(GradientDragHintState::Midpoint { resettable });
|
||||||
|
|
||||||
tool_data.selected_gradient = Some(SelectedGradient {
|
tool_data.selected_gradient = Some(SelectedGradient {
|
||||||
@@ -1351,7 +1423,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
appearance,
|
appearance,
|
||||||
initial_gradient_transform: appearance.transform,
|
initial_gradient_transform: appearance.transform,
|
||||||
dragging: GradientDragTarget::Midpoint(i),
|
dragging: GradientDragTarget::Midpoint(index),
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
is_gradient_chain,
|
is_gradient_chain,
|
||||||
});
|
});
|
||||||
@@ -1364,15 +1436,15 @@ impl Fsm for GradientToolFsmState {
|
|||||||
// Check for dragging the closest stop to the mouse pointer
|
// Check for dragging the closest stop to the mouse pointer
|
||||||
if drag_hint.is_none() {
|
if drag_hint.is_none() {
|
||||||
let mut best: Option<(f64, usize)> = None;
|
let mut best: Option<(f64, usize)> = None;
|
||||||
for (index, stop) in gradient.iter().enumerate() {
|
for index in 0..gradient.len() {
|
||||||
let pos = start.lerp(end, stop.position);
|
let pos = start.lerp(end, gradient.position(index, appearance.gradient_cyclic));
|
||||||
let dist_sq = pos.distance_squared(mouse);
|
let dist_sq = pos.distance_squared(mouse);
|
||||||
if dist_sq < tolerance && best.as_ref().is_none_or(|&(best_dist, _)| dist_sq < best_dist) {
|
if dist_sq < tolerance && best.as_ref().is_none_or(|&(best_dist, _)| dist_sq < best_dist) {
|
||||||
best = Some((dist_sq, index));
|
best = Some((dist_sq, index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((_, index)) = best {
|
if let Some((_, index)) = best {
|
||||||
let stop_position = gradient.position(index);
|
let stop_position = gradient.position(index, appearance.gradient_cyclic);
|
||||||
// Stops at position 0 or 1 are locked endpoints: dragging moves the
|
// Stops at position 0 or 1 are locked endpoints: dragging moves the
|
||||||
// gradient line endpoint geometry (start/end) instead of stop position
|
// gradient line endpoint geometry (start/end) instead of stop position
|
||||||
let drag_target = if stop_position.abs() < f64::EPSILON * 1000. {
|
let drag_target = if stop_position.abs() < f64::EPSILON * 1000. {
|
||||||
@@ -1427,7 +1499,14 @@ impl Fsm for GradientToolFsmState {
|
|||||||
|
|
||||||
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
|
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
|
||||||
let mut new_gradient = gradient.clone();
|
let mut new_gradient = gradient.clone();
|
||||||
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.gradient_space, appearance.gradient_hue_direction) {
|
if let Some(index) = insert_stop_at_point(
|
||||||
|
&mut new_gradient,
|
||||||
|
mouse,
|
||||||
|
unit_to_viewport,
|
||||||
|
appearance.gradient_cyclic,
|
||||||
|
appearance.gradient_space,
|
||||||
|
appearance.gradient_hue_direction,
|
||||||
|
) {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
transaction_started = true;
|
transaction_started = true;
|
||||||
|
|
||||||
@@ -1507,6 +1586,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
gradient_form: tool_options.gradient_form,
|
gradient_form: tool_options.gradient_form,
|
||||||
gradient_spread: tool_options.gradient_spread,
|
gradient_spread: tool_options.gradient_spread,
|
||||||
gradient_space: tool_options.gradient_space,
|
gradient_space: tool_options.gradient_space,
|
||||||
|
gradient_cyclic: tool_options.gradient_cyclic,
|
||||||
gradient_hue_direction: tool_options.gradient_hue_direction,
|
gradient_hue_direction: tool_options.gradient_hue_direction,
|
||||||
},
|
},
|
||||||
// A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted
|
// A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted
|
||||||
@@ -1605,8 +1685,10 @@ impl Fsm for GradientToolFsmState {
|
|||||||
|
|
||||||
// Clear the selection if we were dragging an endpoint of the gradient which isn't a stop
|
// Clear the selection if we were dragging an endpoint of the gradient which isn't a stop
|
||||||
if tool_data.selected_gradient.as_ref().is_some_and(|selected| match selected.dragging {
|
if tool_data.selected_gradient.as_ref().is_some_and(|selected| match selected.dragging {
|
||||||
GradientDragTarget::Start => selected.gradient.is_empty() || selected.gradient.position(0).abs() >= f64::EPSILON * 1000.,
|
GradientDragTarget::Start => selected.gradient.is_empty() || selected.gradient.position(0, selected.appearance.gradient_cyclic).abs() >= f64::EPSILON * 1000.,
|
||||||
GradientDragTarget::End => selected.gradient.is_empty() || (1. - selected.gradient.position(selected.gradient.len() - 1)).abs() >= f64::EPSILON * 1000.,
|
GradientDragTarget::End => {
|
||||||
|
selected.gradient.is_empty() || (1. - selected.gradient.position(selected.gradient.len() - 1, selected.appearance.gradient_cyclic)).abs() >= f64::EPSILON * 1000.
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}) {
|
}) {
|
||||||
tool_data.selected_gradient = None;
|
tool_data.selected_gradient = None;
|
||||||
@@ -1739,10 +1821,17 @@ impl Fsm for GradientToolFsmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Option<usize> {
|
fn insert_stop_at_point(
|
||||||
|
gradient: &mut Gradient,
|
||||||
|
point: DVec2,
|
||||||
|
unit_to_viewport: DAffine2,
|
||||||
|
gradient_cyclic: bool,
|
||||||
|
gradient_space: GradientSpace,
|
||||||
|
gradient_hue_direction: GradientHueDirection,
|
||||||
|
) -> Option<usize> {
|
||||||
let (start, end) = gradient_handle_positions(unit_to_viewport);
|
let (start, end) = gradient_handle_positions(unit_to_viewport);
|
||||||
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
|
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
|
||||||
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_space, gradient_hue_direction))
|
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_cyclic, gradient_space, gradient_hue_direction))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) {
|
fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) {
|
||||||
@@ -1769,18 +1858,11 @@ fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> Gradi
|
|||||||
let line_length = start.distance(end);
|
let line_length = start.distance(end);
|
||||||
|
|
||||||
// Check midpoint diamonds first (smaller hit area, higher priority)
|
// Check midpoint diamonds first (smaller hit area, higher priority)
|
||||||
for i in 0..gradient.len().saturating_sub(1) {
|
for (index, midpoint_position) in midpoint_diamonds(&gradient, appearance.gradient_cyclic, line_length) {
|
||||||
let left = gradient.position(i);
|
|
||||||
let right = gradient.position(i + 1);
|
|
||||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let midpoint_position = left + gradient.midpoint(i) * (right - left);
|
|
||||||
let midpoint_viewport = start.lerp(end, midpoint_position);
|
let midpoint_viewport = start.lerp(end, midpoint_position);
|
||||||
|
|
||||||
if midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
if midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
||||||
let resettable = midpoint_is_resettable(gradient.midpoint(i));
|
let resettable = midpoint_is_resettable(gradient.midpoint(index));
|
||||||
return GradientHoverTarget::Midpoint { resettable };
|
return GradientHoverTarget::Midpoint { resettable };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1805,7 +1887,7 @@ fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> Gradi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check insertion point on line
|
// Check insertion point on line
|
||||||
if calculate_insertion(start, end, &gradient, mouse).is_some() {
|
if calculate_insertion(start, end, &gradient, appearance.gradient_cyclic, mouse).is_some() {
|
||||||
return GradientHoverTarget::InsertionPoint;
|
return GradientHoverTarget::InsertionPoint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1867,6 +1949,7 @@ fn apply_gradient_update(
|
|||||||
gradient_form: appearance.gradient_form,
|
gradient_form: appearance.gradient_form,
|
||||||
gradient_spread: appearance.gradient_spread,
|
gradient_spread: appearance.gradient_spread,
|
||||||
gradient_space: appearance.gradient_space,
|
gradient_space: appearance.gradient_space,
|
||||||
|
gradient_cyclic: appearance.gradient_cyclic,
|
||||||
gradient_hue_direction: appearance.gradient_hue_direction,
|
gradient_hue_direction: appearance.gradient_hue_direction,
|
||||||
transform: appearance.transform,
|
transform: appearance.transform,
|
||||||
});
|
});
|
||||||
@@ -1891,6 +1974,7 @@ fn apply_gradient_update(
|
|||||||
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
||||||
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
||||||
/// single undo entry by the surrounding 'on_commit' callback.
|
/// single undo entry by the surrounding 'on_commit' callback.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn apply_stops_update(
|
fn apply_stops_update(
|
||||||
data: &mut GradientToolData,
|
data: &mut GradientToolData,
|
||||||
context: &mut ToolActionMessageContext,
|
context: &mut ToolActionMessageContext,
|
||||||
@@ -1898,6 +1982,7 @@ fn apply_stops_update(
|
|||||||
new_gradient: Gradient,
|
new_gradient: Gradient,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
) {
|
) {
|
||||||
let selected_layers: Vec<_> = context
|
let selected_layers: Vec<_> = context
|
||||||
@@ -1917,6 +2002,7 @@ fn apply_stops_update(
|
|||||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
|
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
|
||||||
responses.add(GraphOperationMessage::GradientSpreadSet { layer, gradient_spread });
|
responses.add(GraphOperationMessage::GradientSpreadSet { layer, gradient_spread });
|
||||||
responses.add(GraphOperationMessage::GradientSpaceSet { layer, gradient_space });
|
responses.add(GraphOperationMessage::GradientSpaceSet { layer, gradient_space });
|
||||||
|
responses.add(GraphOperationMessage::GradientCyclicSet { layer, gradient_cyclic });
|
||||||
responses.add(GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction });
|
responses.add(GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction });
|
||||||
updated_any_layer = true;
|
updated_any_layer = true;
|
||||||
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
|
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
|
||||||
@@ -1926,6 +2012,7 @@ fn apply_stops_update(
|
|||||||
gradient_form: appearance.gradient_form,
|
gradient_form: appearance.gradient_form,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
gradient_space,
|
gradient_space,
|
||||||
|
gradient_cyclic,
|
||||||
gradient_hue_direction,
|
gradient_hue_direction,
|
||||||
transform: appearance.transform,
|
transform: appearance.transform,
|
||||||
});
|
});
|
||||||
@@ -1937,6 +2024,7 @@ fn apply_stops_update(
|
|||||||
selected_gradient.gradient = new_gradient.clone();
|
selected_gradient.gradient = new_gradient.clone();
|
||||||
selected_gradient.appearance.gradient_spread = gradient_spread;
|
selected_gradient.appearance.gradient_spread = gradient_spread;
|
||||||
selected_gradient.appearance.gradient_space = gradient_space;
|
selected_gradient.appearance.gradient_space = gradient_space;
|
||||||
|
selected_gradient.appearance.gradient_cyclic = gradient_cyclic;
|
||||||
selected_gradient.appearance.gradient_hue_direction = gradient_hue_direction;
|
selected_gradient.appearance.gradient_hue_direction = gradient_hue_direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2021,6 +2109,8 @@ enum GradientDragHintState {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_gradient {
|
mod test_gradient {
|
||||||
|
use super::{gradient_to_viewport_transform, midpoint_diamonds, midpoint_ratio_at};
|
||||||
|
use crate::consts::{GRADIENT_MIDPOINT_MAX, GRADIENT_MIDPOINT_MIN};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
|
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
|
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
@@ -2037,7 +2127,56 @@ mod test_gradient {
|
|||||||
use graphene_std::vector::style::{GradientForm, GradientSpread, build_transform_with_y_preservation};
|
use graphene_std::vector::style::{GradientForm, GradientSpread, build_transform_with_y_preservation};
|
||||||
use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill};
|
use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill};
|
||||||
|
|
||||||
use super::gradient_to_viewport_transform;
|
/// A line long enough that no interval in these tests trips the closely-packed-stops hiding rule.
|
||||||
|
const UNCROWDED_LINE_LENGTH: f64 = 10_000.;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_adds_a_wrap_diamond_owned_by_the_last_stop() {
|
||||||
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
|
// The elided cyclic stops sit at 0 and 0.5, so the wrapped interval spans the other half and centers its diamond at 0.75
|
||||||
|
assert_eq!(midpoint_diamonds(&gradient, false, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
|
||||||
|
assert_eq!(midpoint_diamonds(&gradient, true, UNCROWDED_LINE_LENGTH), vec![(0, 0.25), (1, 0.75)]);
|
||||||
|
|
||||||
|
// A wrapped interval crossing the boundary places its diamond on whichever side the midpoint lands
|
||||||
|
let mut offset = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
offset.set_positions(&[0.25, 0.5]);
|
||||||
|
offset.set_midpoints(&[0.5, 0.9]);
|
||||||
|
let diamonds = midpoint_diamonds(&offset, true, UNCROWDED_LINE_LENGTH);
|
||||||
|
assert_eq!(diamonds[1].0, 1);
|
||||||
|
assert!((diamonds[1].1 - 0.175).abs() < 1e-9, "the late wrap midpoint should land past the boundary, got {}", diamonds[1].1);
|
||||||
|
|
||||||
|
// Stops pinned to both ends leave the wrapped interval no width, so it contributes no diamond
|
||||||
|
let mut spanning = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
spanning.set_positions(&[0., 1.]);
|
||||||
|
assert_eq!(midpoint_diamonds(&spanning, true, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrap_midpoint_drag_tracks_past_the_ends_and_saturates_across_the_dead_zone() {
|
||||||
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
gradient.set_positions(&[0.25, 0.5]);
|
||||||
|
|
||||||
|
// Within the interval the ratio maps linearly, and overdragging past the line's end keeps tracking
|
||||||
|
assert_eq!(midpoint_ratio_at(&gradient, 1, true, 0.875), Some(0.5));
|
||||||
|
assert_eq!(midpoint_ratio_at(&gradient, 1, true, 1.0625), Some(0.75));
|
||||||
|
|
||||||
|
// The dead zone between the outermost stops splits at its midpoint (0.375), each half saturating against the nearer end
|
||||||
|
assert_eq!(midpoint_ratio_at(&gradient, 1, true, 0.45), Some(GRADIENT_MIDPOINT_MIN));
|
||||||
|
assert_eq!(midpoint_ratio_at(&gradient, 1, true, 0.3), Some(GRADIENT_MIDPOINT_MAX));
|
||||||
|
|
||||||
|
// A first stop at 0 leaves no room before the boundary, so the one-sided interval saturates like any other
|
||||||
|
let mut one_sided = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
one_sided.set_positions(&[0., 0.5]);
|
||||||
|
assert_eq!(midpoint_ratio_at(&one_sided, 1, true, 0.4), Some(GRADIENT_MIDPOINT_MIN));
|
||||||
|
assert_eq!(midpoint_ratio_at(&one_sided, 1, true, 0.75), Some(0.5));
|
||||||
|
|
||||||
|
// Ordinary intervals never wrap, and a zero-width interval has no ratio to give
|
||||||
|
assert_eq!(midpoint_ratio_at(&gradient, 0, true, 0.375), Some(0.5));
|
||||||
|
let mut degenerate = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
degenerate.set_positions(&[0.5, 0.5]);
|
||||||
|
assert_eq!(midpoint_ratio_at(°enerate, 0, false, 0.5), None);
|
||||||
|
}
|
||||||
|
|
||||||
struct ResolvedGradient {
|
struct ResolvedGradient {
|
||||||
stops: Gradient,
|
stops: Gradient,
|
||||||
@@ -2200,7 +2339,11 @@ mod test_gradient {
|
|||||||
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
|
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
|
||||||
panic!("expected a gradient default, got {stops:?}")
|
panic!("expected a gradient default, got {stops:?}")
|
||||||
};
|
};
|
||||||
assert_eq!(Gradient::from(ramp).positions(), vec![0., 1.], "the parameter default should be the black-to-white starting gradient");
|
assert_eq!(
|
||||||
|
Gradient::from(ramp).positions(false),
|
||||||
|
vec![0., 1.],
|
||||||
|
"the parameter default should be the black-to-white starting gradient"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_fill_gradient_chain_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier {
|
async fn create_fill_gradient_chain_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier {
|
||||||
@@ -2476,7 +2619,7 @@ mod test_gradient {
|
|||||||
|
|
||||||
// Verify initial stop positions and colors
|
// Verify initial stop positions and colors
|
||||||
let mut stops = initial_gradient.stops.clone();
|
let mut stops = initial_gradient.stops.clone();
|
||||||
stops.sort();
|
stops.sort(false);
|
||||||
|
|
||||||
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
|
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
|
||||||
assert_stops_at_positions(&positions, &[0., 0.25, 1.], 0.1);
|
assert_stops_at_positions(&positions, &[0., 0.25, 1.], 0.1);
|
||||||
@@ -2515,7 +2658,7 @@ mod test_gradient {
|
|||||||
|
|
||||||
// Verify updated stop positions and colors
|
// Verify updated stop positions and colors
|
||||||
let mut updated_stops = updated_gradient.stops.clone();
|
let mut updated_stops = updated_gradient.stops.clone();
|
||||||
updated_stops.sort();
|
updated_stops.sort(false);
|
||||||
|
|
||||||
// Check positions are now correctly ordered
|
// Check positions are now correctly ordered
|
||||||
let updated_positions: Vec<f64> = updated_stops.iter().map(|stop| stop.position).collect();
|
let updated_positions: Vec<f64> = updated_stops.iter().map(|stop| stop.position).collect();
|
||||||
@@ -2907,7 +3050,7 @@ mod test_gradient {
|
|||||||
let updated = ResolvedGradient::new(updated, appearance);
|
let updated = ResolvedGradient::new(updated, appearance);
|
||||||
|
|
||||||
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
|
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
|
||||||
assert_stops_at_positions(&updated.stops.positions(), &[0., 0.5, 1.], 1e-10);
|
assert_stops_at_positions(&updated.stops.positions(false), &[0., 0.5, 1.], 1e-10);
|
||||||
assert_eq!(SRGBA8::from(updated.stops.color(0).unwrap()), SRGBA8::from(Color::RED), "First stop color should be preserved");
|
assert_eq!(SRGBA8::from(updated.stops.color(0).unwrap()), SRGBA8::from(Color::RED), "First stop color should be preserved");
|
||||||
assert_eq!(SRGBA8::from(updated.stops.color(1).unwrap()), SRGBA8::from(Color::GREEN), "Middle stop color should be preserved");
|
assert_eq!(SRGBA8::from(updated.stops.color(1).unwrap()), SRGBA8::from(Color::GREEN), "Middle stop color should be preserved");
|
||||||
assert_eq!(SRGBA8::from(updated.stops.color(2).unwrap()), SRGBA8::from(Color::BLUE), "Last stop color should be preserved");
|
assert_eq!(SRGBA8::from(updated.stops.color(2).unwrap()), SRGBA8::from(Color::BLUE), "Last stop color should be preserved");
|
||||||
@@ -3112,14 +3255,14 @@ mod test_gradient {
|
|||||||
editor.handle_message(GraphOperationMessage::GradientMidpointsSet { layer, midpoints: vec![0.7, 0.5] }).await;
|
editor.handle_message(GraphOperationMessage::GradientMidpointsSet { layer, midpoints: vec![0.7, 0.5] }).await;
|
||||||
let document = editor.active_document();
|
let document = editor.active_document();
|
||||||
let stops = get_gradient_stops(layer, &document.network_interface).expect("the chain should resolve stops");
|
let stops = get_gradient_stops(layer, &document.network_interface).expect("the chain should resolve stops");
|
||||||
assert_stops_at_positions(&stops.positions(), &[0., 0.25], 1e-10);
|
assert_stops_at_positions(&stops.positions(false), &[0., 0.25], 1e-10);
|
||||||
assert_eq!(stops.midpoints(), vec![0.7, 0.5]);
|
assert_eq!(stops.midpoints(), vec![0.7, 0.5]);
|
||||||
|
|
||||||
// An empty update restores the default placement, overriding the value's own explicit placement at runtime
|
// An empty update restores the default placement, overriding the value's own explicit placement at runtime
|
||||||
editor.handle_message(GraphOperationMessage::GradientPositionsSet { layer, positions: vec![] }).await;
|
editor.handle_message(GraphOperationMessage::GradientPositionsSet { layer, positions: vec![] }).await;
|
||||||
let document = editor.active_document();
|
let document = editor.active_document();
|
||||||
let stops = get_gradient_stops(layer, &document.network_interface).expect("the chain should resolve stops");
|
let stops = get_gradient_stops(layer, &document.network_interface).expect("the chain should resolve stops");
|
||||||
assert_stops_at_positions(&stops.positions(), &[0., 1.], 1e-10);
|
assert_stops_at_positions(&stops.positions(false), &[0., 1.], 1e-10);
|
||||||
assert!(!stops.has_position_attribute(), "the empty setter value should clear the attribute");
|
assert!(!stops.has_position_attribute(), "the empty setter value should clear the attribute");
|
||||||
|
|
||||||
// A wired setter input is procedural authorship, which the update must leave untouched rather than bake over
|
// A wired setter input is procedural authorship, which the update must leave untouched rather than bake over
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
export let trackCSS: string;
|
export let trackCSS: string;
|
||||||
export let trackStartCSS: string;
|
export let trackStartCSS: string;
|
||||||
export let trackEndCSS: string;
|
export let trackEndCSS: string;
|
||||||
|
export let trackCyclic = false;
|
||||||
export let markers: SpectrumMarker[];
|
export let markers: SpectrumMarker[];
|
||||||
export let activeMarkerIndex: number | undefined = 0;
|
export let activeMarkerIndex: number | undefined = 0;
|
||||||
export let activeMarkerIsMidpoint = false;
|
export let activeMarkerIsMidpoint = false;
|
||||||
@@ -54,11 +55,11 @@
|
|||||||
emit({ ActiveMarker: { activeMarkerIndex: index, activeMarkerIsMidpoint: isMidpoint } });
|
emit({ ActiveMarker: { activeMarkerIndex: index, activeMarkerIsMidpoint: isMidpoint } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointerPosition(e: MouseEvent): number | undefined {
|
function pointerPosition(e: MouseEvent, clamp = true): number | undefined {
|
||||||
const rect = markerTrackElement?.div()?.getBoundingClientRect();
|
const rect = markerTrackElement?.div()?.getBoundingClientRect();
|
||||||
if (!rect) return undefined;
|
if (!rect) return undefined;
|
||||||
const ratio = (e.clientX - rect.left) / rect.width;
|
const ratio = (e.clientX - rect.left) / rect.width;
|
||||||
return Math.max(0, Math.min(1, ratio));
|
return clamp ? Math.max(0, Math.min(1, ratio)) : ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampToNeighbors(index: number, position: number): number {
|
function clampToNeighbors(index: number, position: number): number {
|
||||||
@@ -237,18 +238,32 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolute = pointerPosition(e);
|
// The wrapped interval's diamond (cyclic only) belongs to the last marker and spans through the 1|0 boundary to the first.
|
||||||
|
// Its pointer ratio stays unclamped so overdragging past the strip's right or left edge keeps tracking after the 1|0 wrap.
|
||||||
|
const isWrappedInterval = trackCyclic && activeMarkerIndex === markers.length - 1;
|
||||||
|
const absolute = pointerPosition(e, !isWrappedInterval);
|
||||||
if (absolute === undefined) return;
|
if (absolute === undefined) return;
|
||||||
|
|
||||||
const left = markers[activeMarkerIndex]?.position;
|
const left = markers[activeMarkerIndex]?.position;
|
||||||
const right = markers[activeMarkerIndex + 1]?.position;
|
const right = isWrappedInterval ? markers[0].position + 1 : markers[activeMarkerIndex + 1]?.position;
|
||||||
if (left === undefined || right === undefined) return;
|
if (left === undefined || right === undefined) return;
|
||||||
const range = right - left;
|
const range = right - left;
|
||||||
if (range <= 0) return;
|
if (range <= 0) return;
|
||||||
|
|
||||||
|
// The dead zone between the first and last stops splits so each half saturates against the wrapped interval's nearer end,
|
||||||
|
// except when an outermost stop leaves no room on its side of the boundary, where the one-sided interval skips the split
|
||||||
|
// and saturates like any other
|
||||||
|
let deadZoneSplit = Number.NEGATIVE_INFINITY;
|
||||||
|
if (isWrappedInterval) {
|
||||||
|
const first = markers[0].position;
|
||||||
|
if (left >= 1) deadZoneSplit = Number.POSITIVE_INFINITY;
|
||||||
|
else if (first > 0) deadZoneSplit = (first + left) / 2;
|
||||||
|
}
|
||||||
|
const local = absolute < deadZoneSplit ? absolute + 1 - left : absolute - left;
|
||||||
|
|
||||||
midpointDragged = true;
|
midpointDragged = true;
|
||||||
dispatch("dragging", true);
|
dispatch("dragging", true);
|
||||||
emit({ MoveMidpoint: { index: activeMarkerIndex, position: (absolute - left) / range } });
|
emit({ MoveMidpoint: { index: activeMarkerIndex, position: local / range } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function abortDrag() {
|
function abortDrag() {
|
||||||
@@ -347,7 +362,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map midpoint pairs to absolute track positions for rendering the diamond markers.
|
// Map midpoint pairs to absolute track positions for rendering the diamond markers.
|
||||||
$: midpointPositions = !showMidpoints || markers.length < 2 ? [] : markers.slice(0, -1).map((marker, i) => marker.position + marker.midpoint * (markers[i + 1].position - marker.position));
|
// A rendered diamond's index is the index of the interval's left marker, which for the cyclic wrapped interval's diamond is the last marker.
|
||||||
|
function diamondPositions(markers: SpectrumMarker[], showMidpoints: boolean, trackCyclic: boolean): number[] {
|
||||||
|
if (!showMidpoints || markers.length < 2) return [];
|
||||||
|
const positions = markers.slice(0, -1).map((marker, i) => marker.position + marker.midpoint * (markers[i + 1].position - marker.position));
|
||||||
|
|
||||||
|
// The wrapped interval's diamond may land on either side of the 1|0 boundary
|
||||||
|
if (trackCyclic) {
|
||||||
|
const first = markers[0];
|
||||||
|
const last = markers[markers.length - 1];
|
||||||
|
const wrapLength = first.position + 1 - last.position;
|
||||||
|
if (wrapLength > 1e-9) positions.push((last.position + last.midpoint * wrapLength) % 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
$: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener("keydown", deleteShortcut);
|
document.addEventListener("keydown", deleteShortcut);
|
||||||
|
|||||||
@@ -1086,7 +1086,7 @@ mod gradient_shape_migration {
|
|||||||
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp flat form should carry the era's gamma");
|
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp flat form should carry the era's gamma");
|
||||||
|
|
||||||
let gradient = Gradient::from(ramp);
|
let gradient = Gradient::from(ramp);
|
||||||
assert_eq!(gradient.positions(), vec![0., 0.25]);
|
assert_eq!(gradient.positions(false), vec![0., 0.25]);
|
||||||
assert!(gradient.has_midpoint_attribute(), "the flat form must parse faithfully");
|
assert!(gradient.has_midpoint_attribute(), "the flat form must parse faithfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,7 +1100,7 @@ mod gradient_shape_migration {
|
|||||||
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp tuple form should carry the era's gamma");
|
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp tuple form should carry the era's gamma");
|
||||||
|
|
||||||
let gradient = Gradient::from(ramp);
|
let gradient = Gradient::from(ramp);
|
||||||
assert_eq!(gradient.positions(), vec![0., 1.]);
|
assert_eq!(gradient.positions(false), vec![0., 1.]);
|
||||||
assert!(!gradient.has_position_attribute(), "even legacy tuple positions should elide");
|
assert!(!gradient.has_position_attribute(), "even legacy tuple positions should elide");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,6 +1122,10 @@ mod gradient_shape_migration {
|
|||||||
let TaggedValue::LegacyGradient(legacy) = load(json) else {
|
let TaggedValue::LegacyGradient(legacy) = load(json) else {
|
||||||
panic!("the ancient full struct should become a legacy gradient value")
|
panic!("the ancient full struct should become a legacy gradient value")
|
||||||
};
|
};
|
||||||
assert_eq!(Gradient::from(legacy.stops).positions(), vec![0., 1.], "the nested tuple stops should parse through the field adapter");
|
assert_eq!(
|
||||||
|
Gradient::from(legacy.stops).positions(false),
|
||||||
|
vec![0., 1.],
|
||||||
|
"the nested tuple stops should parse through the field adapter"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ pub use graphene_hash;
|
|||||||
pub use graphene_hash::CacheHash;
|
pub use graphene_hash::CacheHash;
|
||||||
pub use list::{
|
pub use list::{
|
||||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
||||||
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION,
|
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT,
|
||||||
ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
||||||
};
|
};
|
||||||
pub use memo::MemoHash;
|
pub use memo::MemoHash;
|
||||||
pub use no_std_types::AsU32;
|
pub use no_std_types::AsU32;
|
||||||
|
|||||||
@@ -65,11 +65,14 @@ pub const ATTR_GRADIENT_SPACE: &str = "gradient_space";
|
|||||||
/// Gradient's `GradientHueDirection` (`Shorter`, `Longer`, `Increasing`, or `Decreasing`), which way around the
|
/// Gradient's `GradientHueDirection` (`Shorter`, `Longer`, `Increasing`, or `Decreasing`), which way around the
|
||||||
/// hue wheel the stops interpolate when the gradient space is polar.
|
/// hue wheel the stops interpolate when the gradient space is polar.
|
||||||
pub const ATTR_GRADIENT_HUE_DIRECTION: &str = "gradient_hue_direction";
|
pub const ATTR_GRADIENT_HUE_DIRECTION: &str = "gradient_hue_direction";
|
||||||
|
/// Gradient's `bool` (implicit default `false`) for treating the stop list as a cycle, where a wrapped interval
|
||||||
|
/// interpolates from the last stop through the 1|0 boundary back to the first.
|
||||||
|
pub const ATTR_GRADIENT_CYCLIC: &str = "gradient_cyclic";
|
||||||
/// Gradient stop's `f64` position from 0 to 1 along the gradient, on the `List<Color>` inside a `Gradient`.
|
/// Gradient stop's `f64` position from 0 to 1 along the gradient, on the `List<Color>` inside a `Gradient`.
|
||||||
/// When the attribute is absent, stops distribute evenly across the 0 to 1 range.
|
/// When the attribute is absent, stops distribute evenly across the 0 to 1 range.
|
||||||
pub const ATTR_POSITION: &str = "position";
|
pub const ATTR_POSITION: &str = "position";
|
||||||
/// Gradient stop's `f64` midpoint (implicit default `0.5`, linear), a factor from 0 to 1 across the distance
|
/// Gradient stop's `f64` midpoint (implicit default `0.5`, linear), a factor from 0 to 1 across the distance to the next
|
||||||
/// to the next stop, on the `List<Color>` inside a `Gradient`. The final stop's midpoint is ignored.
|
/// stop, on the `List<Color>` inside a `Gradient`. The final stop's midpoint is ignored if "gradient_cyclic" is false.
|
||||||
pub const ATTR_MIDPOINT: &str = "midpoint";
|
pub const ATTR_MIDPOINT: &str = "midpoint";
|
||||||
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
|
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
|
||||||
pub const ATTR_FILL: &str = "fill";
|
pub const ATTR_FILL: &str = "fill";
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ pub mod migrations {
|
|||||||
let position: Vec<f64> = stops.iter().map(|(position, _)| *position).collect();
|
let position: Vec<f64> = stops.iter().map(|(position, _)| *position).collect();
|
||||||
let mut gradient = Gradient::from(stops.into_iter().map(|(_, color)| color).collect::<Vec<_>>());
|
let mut gradient = Gradient::from(stops.into_iter().map(|(_, color)| color).collect::<Vec<_>>());
|
||||||
gradient.set_positions(&position);
|
gradient.set_positions(&position);
|
||||||
gradient.elide_default_attributes();
|
gradient.elide_default_attributes(false);
|
||||||
|
|
||||||
GradientRamp {
|
GradientRamp {
|
||||||
gradient_space: GradientSpace::RgbGamma,
|
gradient_space: GradientSpace::RgbGamma,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{Render, RenderSvgSegmentList, SvgRender};
|
|||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::uuid::generate_uuid;
|
use core_types::uuid::generate_uuid;
|
||||||
use core_types::{ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_TRANSFORM, Color};
|
use core_types::{ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_TRANSFORM, Color};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::Graphic;
|
use graphic_types::Graphic;
|
||||||
use graphic_types::vector_types::gradient::GradientForm;
|
use graphic_types::vector_types::gradient::GradientForm;
|
||||||
@@ -97,9 +97,18 @@ impl RenderExt for List<Gradient> {
|
|||||||
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
||||||
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, 0);
|
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, 0);
|
||||||
|
let gradient_cyclic: bool = self.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC, 0);
|
||||||
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, 0);
|
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, 0);
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::SvgStopOrder);
|
let (samples, _) = spread_adjusted_samples(
|
||||||
|
stops,
|
||||||
|
gradient_spread,
|
||||||
|
gradient_form,
|
||||||
|
gradient_cyclic,
|
||||||
|
gradient_space,
|
||||||
|
gradient_hue_direction,
|
||||||
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
|
);
|
||||||
|
|
||||||
for (position, color, original_midpoint) in samples {
|
for (position, color, original_midpoint) in samples {
|
||||||
stop.push_str("<stop");
|
stop.push_str("<stop");
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use core_types::transform::Footprint;
|
|||||||
use core_types::uuid::{NodeId, generate_uuid};
|
use core_types::uuid::{NodeId, generate_uuid};
|
||||||
use core_types::{
|
use core_types::{
|
||||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
||||||
ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION,
|
ATTR_FONT_SIZE, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT,
|
||||||
ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||||
};
|
};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
@@ -420,11 +420,12 @@ pub(crate) fn spread_adjusted_samples(
|
|||||||
gradient: &Gradient,
|
gradient: &Gradient,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
|
gradient_cyclic: bool,
|
||||||
gradient_space: GradientSpace,
|
gradient_space: GradientSpace,
|
||||||
gradient_hue_direction: GradientHueDirection,
|
gradient_hue_direction: GradientHueDirection,
|
||||||
guards: ClearGuardPlacement,
|
guards: ClearGuardPlacement,
|
||||||
) -> (GradientSamples, (f64, f64)) {
|
) -> (GradientSamples, (f64, f64)) {
|
||||||
let samples = gradient.interpolated_samples(gradient_space, gradient_hue_direction);
|
let samples = gradient.interpolated_samples(gradient_cyclic, gradient_space, gradient_hue_direction);
|
||||||
if gradient_spread != GradientSpread::Clear {
|
if gradient_spread != GradientSpread::Clear {
|
||||||
return (samples, (0., 1.));
|
return (samples, (0., 1.));
|
||||||
}
|
}
|
||||||
@@ -510,9 +511,18 @@ fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_trans
|
|||||||
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
let gradient_spread: GradientSpread = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
let gradient_spread: GradientSpread = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
||||||
let gradient_space: GradientSpace = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, 0);
|
let gradient_space: GradientSpace = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, 0);
|
||||||
|
let gradient_cyclic: bool = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC, 0);
|
||||||
let gradient_hue_direction: GradientHueDirection = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, 0);
|
let gradient_hue_direction: GradientHueDirection = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, 0);
|
||||||
|
|
||||||
let (samples, span) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::VelloRampTexels);
|
let (samples, span) = spread_adjusted_samples(
|
||||||
|
stops,
|
||||||
|
gradient_spread,
|
||||||
|
gradient_form,
|
||||||
|
gradient_cyclic,
|
||||||
|
gradient_space,
|
||||||
|
gradient_hue_direction,
|
||||||
|
ClearGuardPlacement::VelloRampTexels,
|
||||||
|
);
|
||||||
|
|
||||||
let peniko_stops = peniko_color_stops(&samples);
|
let peniko_stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
@@ -2194,6 +2204,7 @@ impl Render for List<Gradient> {
|
|||||||
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, index);
|
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, index);
|
||||||
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index);
|
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index);
|
||||||
|
let gradient_cyclic: bool = self.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC, index);
|
||||||
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index);
|
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index);
|
||||||
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
|
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
|
||||||
render.leaf_tag(tag, |attributes| {
|
render.leaf_tag(tag, |attributes| {
|
||||||
@@ -2210,7 +2221,15 @@ impl Render for List<Gradient> {
|
|||||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::SvgStopOrder);
|
let (samples, _) = spread_adjusted_samples(
|
||||||
|
gradient,
|
||||||
|
gradient_spread,
|
||||||
|
gradient_form,
|
||||||
|
gradient_cyclic,
|
||||||
|
gradient_space,
|
||||||
|
gradient_hue_direction,
|
||||||
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
|
);
|
||||||
|
|
||||||
let mut stop_string = String::new();
|
let mut stop_string = String::new();
|
||||||
for (position, color, original_midpoint) in samples {
|
for (position, color, original_midpoint) in samples {
|
||||||
@@ -2293,8 +2312,17 @@ impl Render for List<Gradient> {
|
|||||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||||
|
|
||||||
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index);
|
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index);
|
||||||
|
let gradient_cyclic: bool = self.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC, index);
|
||||||
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index);
|
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index);
|
||||||
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::VelloRampTexels);
|
let (samples, span) = spread_adjusted_samples(
|
||||||
|
gradient,
|
||||||
|
gradient_spread,
|
||||||
|
gradient_form,
|
||||||
|
gradient_cyclic,
|
||||||
|
gradient_space,
|
||||||
|
gradient_hue_direction,
|
||||||
|
ClearGuardPlacement::VelloRampTexels,
|
||||||
|
);
|
||||||
|
|
||||||
let stops = peniko_color_stops(&samples);
|
let stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
@@ -2847,18 +2875,20 @@ mod tests {
|
|||||||
&gradient,
|
&gradient,
|
||||||
GradientSpread::Repeat,
|
GradientSpread::Repeat,
|
||||||
GradientForm::Linear,
|
GradientForm::Linear,
|
||||||
|
false,
|
||||||
GradientSpace::RgbGamma,
|
GradientSpace::RgbGamma,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
ClearGuardPlacement::SvgStopOrder,
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
);
|
);
|
||||||
assert_eq!(span, (0., 1.));
|
assert_eq!(span, (0., 1.));
|
||||||
assert_eq!(samples, gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default()));
|
assert_eq!(samples, gradient.interpolated_samples(false, GradientSpace::RgbGamma, Default::default()));
|
||||||
|
|
||||||
// SVG guards share the range ends' exact offsets, ordered so the pad extension resolves to the transparent outer stops
|
// SVG guards share the range ends' exact offsets, ordered so the pad extension resolves to the transparent outer stops
|
||||||
let (samples, span) = spread_adjusted_samples(
|
let (samples, span) = spread_adjusted_samples(
|
||||||
&gradient,
|
&gradient,
|
||||||
GradientSpread::Clear,
|
GradientSpread::Clear,
|
||||||
GradientForm::Linear,
|
GradientForm::Linear,
|
||||||
|
false,
|
||||||
GradientSpace::RgbGamma,
|
GradientSpace::RgbGamma,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
ClearGuardPlacement::SvgStopOrder,
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
@@ -2875,6 +2905,7 @@ mod tests {
|
|||||||
&gradient,
|
&gradient,
|
||||||
GradientSpread::Clear,
|
GradientSpread::Clear,
|
||||||
GradientForm::Linear,
|
GradientForm::Linear,
|
||||||
|
false,
|
||||||
GradientSpace::RgbGamma,
|
GradientSpace::RgbGamma,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
ClearGuardPlacement::VelloRampTexels,
|
ClearGuardPlacement::VelloRampTexels,
|
||||||
@@ -2895,6 +2926,7 @@ mod tests {
|
|||||||
&gradient,
|
&gradient,
|
||||||
GradientSpread::Clear,
|
GradientSpread::Clear,
|
||||||
GradientForm::Radial,
|
GradientForm::Radial,
|
||||||
|
false,
|
||||||
GradientSpace::RgbGamma,
|
GradientSpace::RgbGamma,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
ClearGuardPlacement::VelloRampTexels,
|
ClearGuardPlacement::VelloRampTexels,
|
||||||
@@ -2910,6 +2942,7 @@ mod tests {
|
|||||||
&Gradient::from(Vec::new()),
|
&Gradient::from(Vec::new()),
|
||||||
GradientSpread::Clear,
|
GradientSpread::Clear,
|
||||||
GradientForm::Linear,
|
GradientForm::Linear,
|
||||||
|
false,
|
||||||
GradientSpace::RgbGamma,
|
GradientSpace::RgbGamma,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
ClearGuardPlacement::SvgStopOrder,
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_MIDPOINT, ATTR_POSITION, Item, List};
|
use core_types::list::{ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_MIDPOINT, ATTR_POSITION, Item, List};
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -76,7 +76,7 @@ impl From<GradientStops<Color>> for Gradient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color picker round-trip: attributes that merely restate the defaults are elided to keep the canonical absence-as-default form
|
// Color picker round-trip: faithful, since eliding default-restating attributes needs the cyclic flag that only the ramp conversion holds
|
||||||
impl From<&GradientStops<SRGBA8>> for Gradient {
|
impl From<&GradientStops<SRGBA8>> for Gradient {
|
||||||
fn from(stops: &GradientStops<SRGBA8>) -> Self {
|
fn from(stops: &GradientStops<SRGBA8>) -> Self {
|
||||||
let mut gradient = Gradient::from(stops.color.iter().map(|&color| Color::from(color)).collect::<Vec<_>>());
|
let mut gradient = Gradient::from(stops.color.iter().map(|&color| Color::from(color)).collect::<Vec<_>>());
|
||||||
@@ -86,15 +86,14 @@ impl From<&GradientStops<SRGBA8>> for Gradient {
|
|||||||
if let Some(midpoint) = &stops.midpoint {
|
if let Some(midpoint) = &stops.midpoint {
|
||||||
gradient.set_midpoints(midpoint);
|
gradient.set_midpoints(midpoint);
|
||||||
}
|
}
|
||||||
gradient.elide_default_attributes();
|
|
||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientStops<SRGBA8> {
|
impl GradientStops<SRGBA8> {
|
||||||
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
|
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
|
||||||
pub fn to_css_linear_gradient(&self, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> String {
|
pub fn to_css_linear_gradient(&self, gradient_cyclic: bool, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> String {
|
||||||
Gradient::from(self).to_css_linear_gradient(gradient_space, gradient_hue_direction)
|
Gradient::from(self).to_css_linear_gradient(gradient_cyclic, gradient_space, gradient_hue_direction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +111,9 @@ pub struct GradientRamp<C = Color> {
|
|||||||
// TODO: Elide the default again (removing `legacy_gamma` and the serde aliases) when switching to the new document format and Ctrl-C node serialization format
|
// TODO: Elide the default again (removing `legacy_gamma` and the serde aliases) when switching to the new document format and Ctrl-C node serialization format
|
||||||
#[cfg_attr(feature = "serde", serde(default = "GradientSpace::legacy_gamma", alias = "gradient_interpolation"))]
|
#[cfg_attr(feature = "serde", serde(default = "GradientSpace::legacy_gamma", alias = "gradient_interpolation"))]
|
||||||
pub gradient_space: GradientSpace,
|
pub gradient_space: GradientSpace,
|
||||||
|
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "std::ops::Not::not"))]
|
||||||
|
#[cfg_attr(feature = "wasm", tsify(optional))]
|
||||||
|
pub gradient_cyclic: bool,
|
||||||
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "GradientHueDirection::is_default"))]
|
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "GradientHueDirection::is_default"))]
|
||||||
#[cfg_attr(feature = "wasm", tsify(optional))]
|
#[cfg_attr(feature = "wasm", tsify(optional))]
|
||||||
pub gradient_hue_direction: GradientHueDirection,
|
pub gradient_hue_direction: GradientHueDirection,
|
||||||
@@ -127,6 +129,7 @@ impl<C> From<GradientStops<C>> for GradientRamp<C> {
|
|||||||
stops,
|
stops,
|
||||||
gradient_spread: Default::default(),
|
gradient_spread: Default::default(),
|
||||||
gradient_space: Default::default(),
|
gradient_space: Default::default(),
|
||||||
|
gradient_cyclic: Default::default(),
|
||||||
gradient_hue_direction: Default::default(),
|
gradient_hue_direction: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +141,7 @@ impl From<&Gradient> for GradientRamp {
|
|||||||
stops: gradient.into(),
|
stops: gradient.into(),
|
||||||
gradient_spread: Default::default(),
|
gradient_spread: Default::default(),
|
||||||
gradient_space: Default::default(),
|
gradient_space: Default::default(),
|
||||||
|
gradient_cyclic: Default::default(),
|
||||||
gradient_hue_direction: Default::default(),
|
gradient_hue_direction: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +176,9 @@ impl From<GradientRamp> for Item<Gradient> {
|
|||||||
if !ramp.gradient_space.is_default() {
|
if !ramp.gradient_space.is_default() {
|
||||||
item.set_attribute(ATTR_GRADIENT_SPACE, ramp.gradient_space);
|
item.set_attribute(ATTR_GRADIENT_SPACE, ramp.gradient_space);
|
||||||
}
|
}
|
||||||
|
if ramp.gradient_cyclic {
|
||||||
|
item.set_attribute(ATTR_GRADIENT_CYCLIC, ramp.gradient_cyclic);
|
||||||
|
}
|
||||||
if !ramp.gradient_hue_direction.is_default() {
|
if !ramp.gradient_hue_direction.is_default() {
|
||||||
item.set_attribute(ATTR_GRADIENT_HUE_DIRECTION, ramp.gradient_hue_direction);
|
item.set_attribute(ATTR_GRADIENT_HUE_DIRECTION, ramp.gradient_hue_direction);
|
||||||
}
|
}
|
||||||
@@ -185,6 +192,7 @@ impl From<&Item<Gradient>> for GradientRamp {
|
|||||||
stops: item.element().into(),
|
stops: item.element().into(),
|
||||||
gradient_spread: item.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD),
|
gradient_spread: item.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD),
|
||||||
gradient_space: item.attribute_cloned_or_default(ATTR_GRADIENT_SPACE),
|
gradient_space: item.attribute_cloned_or_default(ATTR_GRADIENT_SPACE),
|
||||||
|
gradient_cyclic: item.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC),
|
||||||
gradient_hue_direction: item.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION),
|
gradient_hue_direction: item.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +208,6 @@ impl From<&GradientRamp> for GradientStops<SRGBA8> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color picker round-trip: routes through the runtime type so default-restating attributes elide
|
|
||||||
impl From<&GradientStops<SRGBA8>> for GradientRamp {
|
impl From<&GradientStops<SRGBA8>> for GradientRamp {
|
||||||
fn from(stops: &GradientStops<SRGBA8>) -> Self {
|
fn from(stops: &GradientStops<SRGBA8>) -> Self {
|
||||||
Self::from(Gradient::from(stops))
|
Self::from(Gradient::from(stops))
|
||||||
@@ -213,6 +220,7 @@ impl From<&GradientRamp> for GradientRamp<SRGBA8> {
|
|||||||
stops: ramp.into(),
|
stops: ramp.into(),
|
||||||
gradient_spread: ramp.gradient_spread,
|
gradient_spread: ramp.gradient_spread,
|
||||||
gradient_space: ramp.gradient_space,
|
gradient_space: ramp.gradient_space,
|
||||||
|
gradient_cyclic: ramp.gradient_cyclic,
|
||||||
gradient_hue_direction: ramp.gradient_hue_direction,
|
gradient_hue_direction: ramp.gradient_hue_direction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,18 +232,24 @@ impl From<&Gradient> for GradientRamp<SRGBA8> {
|
|||||||
stops: gradient.into(),
|
stops: gradient.into(),
|
||||||
gradient_spread: Default::default(),
|
gradient_spread: Default::default(),
|
||||||
gradient_space: Default::default(),
|
gradient_space: Default::default(),
|
||||||
|
gradient_cyclic: Default::default(),
|
||||||
gradient_hue_direction: Default::default(),
|
gradient_hue_direction: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color picker round-trip: the picker sends every position explicitly, so elide the ones restating the even distribution the cyclic flag selects
|
||||||
impl From<&GradientRamp<SRGBA8>> for GradientRamp {
|
impl From<&GradientRamp<SRGBA8>> for GradientRamp {
|
||||||
fn from(ramp: &GradientRamp<SRGBA8>) -> Self {
|
fn from(ramp: &GradientRamp<SRGBA8>) -> Self {
|
||||||
|
let mut gradient = Gradient::from(&ramp.stops);
|
||||||
|
gradient.elide_default_attributes(ramp.gradient_cyclic);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
gradient_spread: ramp.gradient_spread,
|
gradient_spread: ramp.gradient_spread,
|
||||||
gradient_space: ramp.gradient_space,
|
gradient_space: ramp.gradient_space,
|
||||||
|
gradient_cyclic: ramp.gradient_cyclic,
|
||||||
gradient_hue_direction: ramp.gradient_hue_direction,
|
gradient_hue_direction: ramp.gradient_hue_direction,
|
||||||
..Self::from(&ramp.stops)
|
..Self::from(gradient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,8 +449,9 @@ impl Iterator for GradientStopsIter<'_> {
|
|||||||
type Item = GradientStop;
|
type Item = GradientStop;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// Elided positions fall back to the non-cyclic even distribution; cyclic-aware callers should read `position(index, gradient_cyclic)` instead
|
||||||
let stop = GradientStop {
|
let stop = GradientStop {
|
||||||
position: self.stops.position(self.index),
|
position: self.stops.position(self.index, false),
|
||||||
midpoint: self.stops.midpoint(self.index),
|
midpoint: self.stops.midpoint(self.index),
|
||||||
color: self.stops.color(self.index)?,
|
color: self.stops.color(self.index)?,
|
||||||
};
|
};
|
||||||
@@ -471,8 +486,13 @@ impl IntoIterator for Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The fallback position of the gradient stop at `index` when no `position` attribute exists, where all `count` stops are spaced evenly from 0 to 1.
|
/// The fallback position of the gradient stop at `index` when no `position` attribute exists, where all `count` stops are spaced evenly from 0 to 1.
|
||||||
fn even_position(index: usize, count: usize) -> f64 {
|
fn even_position(index: usize, count: usize, gradient_cyclic: bool) -> f64 {
|
||||||
if count <= 1 { 0. } else { index as f64 / (count - 1) as f64 }
|
if count <= 1 {
|
||||||
|
return 0.;
|
||||||
|
}
|
||||||
|
// A cyclic gradient reserves the same span for the wrapped interval as for each interval between stops
|
||||||
|
let denominator = if gradient_cyclic { count } else { count - 1 };
|
||||||
|
index as f64 / denominator as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gradient {
|
impl Gradient {
|
||||||
@@ -518,8 +538,11 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The effective position of the stop at the given index: its `position` attribute value, or its share of an even distribution when the attribute is absent.
|
/// The effective position of the stop at the given index: its `position` attribute value, or its share of an even distribution when the attribute is absent.
|
||||||
pub fn position(&self, index: usize) -> f64 {
|
pub fn position(&self, index: usize, gradient_cyclic: bool) -> f64 {
|
||||||
self.0.attribute::<f64>(ATTR_POSITION, index).copied().unwrap_or_else(|| even_position(index, self.len()))
|
self.0
|
||||||
|
.attribute::<f64>(ATTR_POSITION, index)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| even_position(index, self.len(), gradient_cyclic))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The effective midpoint of the stop at the given index: its `midpoint` attribute value, or the linear interpolation default of `0.5` when the attribute is absent.
|
/// The effective midpoint of the stop at the given index: its `midpoint` attribute value, or the linear interpolation default of `0.5` when the attribute is absent.
|
||||||
@@ -528,8 +551,8 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The effective positions of all stops.
|
/// The effective positions of all stops.
|
||||||
pub fn positions(&self) -> Vec<f64> {
|
pub fn positions(&self, gradient_cyclic: bool) -> Vec<f64> {
|
||||||
(0..self.len()).map(|index| self.position(index)).collect()
|
(0..self.len()).map(|index| self.position(index, gradient_cyclic)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The effective midpoints of all stops.
|
/// The effective midpoints of all stops.
|
||||||
@@ -558,13 +581,13 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `position` attribute when present and meaningfully different from the even distribution, which is the form worth persisting in the graph.
|
/// The `position` attribute when present and meaningfully different from the even distribution, which is the form worth persisting in the graph.
|
||||||
pub fn nondefault_positions(&self) -> Option<Vec<f64>> {
|
pub fn nondefault_positions(&self, gradient_cyclic: bool) -> Option<Vec<f64>> {
|
||||||
let positions = self.position_attribute()?;
|
let positions = self.position_attribute()?;
|
||||||
let count = self.len();
|
let count = self.len();
|
||||||
positions
|
positions
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.any(|(index, &position)| !position.is_finite() || (position - even_position(index, count)).abs() > 1e-6)
|
.any(|(index, &position)| !position.is_finite() || (position - even_position(index, count, gradient_cyclic)).abs() > 1e-6)
|
||||||
.then_some(positions)
|
.then_some(positions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,8 +598,8 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the `position`/`midpoint` attributes when they merely restate the defaults, restoring the canonical absence-as-default form.
|
/// Removes the `position`/`midpoint` attributes when they merely restate the defaults, restoring the canonical absence-as-default form.
|
||||||
pub fn elide_default_attributes(&mut self) {
|
pub fn elide_default_attributes(&mut self, gradient_cyclic: bool) {
|
||||||
if self.has_position_attribute() && self.nondefault_positions().is_none() {
|
if self.has_position_attribute() && self.nondefault_positions(gradient_cyclic).is_none() {
|
||||||
self.0.remove_attribute(ATTR_POSITION);
|
self.0.remove_attribute(ATTR_POSITION);
|
||||||
}
|
}
|
||||||
if self.has_midpoint_attribute() && self.nondefault_midpoints().is_none() {
|
if self.has_midpoint_attribute() && self.nondefault_midpoints().is_none() {
|
||||||
@@ -585,14 +608,14 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the whole `position` attribute from the effective values, since the even-distribution default is index-dependent and can't be produced by cell-wise padding.
|
/// Writes the whole `position` attribute from the effective values, since the even-distribution default is index-dependent and can't be produced by cell-wise padding.
|
||||||
fn materialize_default_positions(&mut self) {
|
fn materialize_default_positions(&mut self, gradient_cyclic: bool) {
|
||||||
if self.has_position_attribute() {
|
if self.has_position_attribute() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = self.len();
|
let count = self.len();
|
||||||
for index in 0..count {
|
for index in 0..count {
|
||||||
self.0.set_attribute(ATTR_POSITION, index, even_position(index, count));
|
self.0.set_attribute(ATTR_POSITION, index, even_position(index, count, gradient_cyclic));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,11 +627,11 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the position of the stop at `index`, if it exists, materializing the whole `position` attribute so the other stops keep their effective placements.
|
/// Sets the position of the stop at `index`, if it exists, materializing the whole `position` attribute so the other stops keep their effective placements.
|
||||||
pub fn set_position(&mut self, index: usize, position: f64) {
|
pub fn set_position(&mut self, index: usize, position: f64, gradient_cyclic: bool) {
|
||||||
if index >= self.len() {
|
if index >= self.len() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.materialize_default_positions();
|
self.materialize_default_positions(gradient_cyclic);
|
||||||
self.0.set_attribute(ATTR_POSITION, index, position);
|
self.0.set_attribute(ATTR_POSITION, index, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,37 +693,46 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Move the stop at `index` to a new position, re-sorting the stops by position. Returns the new index of the moved stop.
|
/// Move the stop at `index` to a new position, re-sorting the stops by position. Returns the new index of the moved stop.
|
||||||
pub fn move_stop(&mut self, index: usize, position: f64) -> usize {
|
pub fn move_stop(&mut self, index: usize, position: f64, gradient_cyclic: bool) -> usize {
|
||||||
if index >= self.len() {
|
if index >= self.len() {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
self.set_position(index, position);
|
self.set_position(index, position, gradient_cyclic);
|
||||||
self.sort_returning_new_index(index)
|
self.sort_returning_new_index(index, gradient_cyclic)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color.
|
/// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color.
|
||||||
/// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start).
|
/// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start of a non-cyclic gradient).
|
||||||
/// Returns the index where the new stop was inserted.
|
/// Returns the index where the new stop was inserted.
|
||||||
pub fn insert_stop(&mut self, position: f64, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> usize {
|
pub fn insert_stop(&mut self, position: f64, gradient_cyclic: bool, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> usize {
|
||||||
let color = self.evaluate(position, Default::default(), gradient_space, gradient_hue_direction);
|
let color = self.evaluate(position, Default::default(), gradient_cyclic, gradient_space, gradient_hue_direction);
|
||||||
let index = (0..self.len()).position(|i| self.position(i) > position).unwrap_or(self.len());
|
let index = (0..self.len()).position(|i| self.position(i, gradient_cyclic) > position).unwrap_or(self.len());
|
||||||
let midpoint = if index > 0 { self.midpoint(index - 1) } else { 0.5 };
|
|
||||||
self.insert_stop_values(position, midpoint, color)
|
// Inserting before the first stop of a cyclic gradient splits the wrapped interval, so its handle is inherited
|
||||||
|
let midpoint = if index > 0 {
|
||||||
|
self.midpoint(index - 1)
|
||||||
|
} else if gradient_cyclic && !self.is_empty() {
|
||||||
|
self.midpoint(self.len() - 1)
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
self.insert_stop_values(position, midpoint, color, gradient_cyclic)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a copy of the stop at `source_index` (same color and midpoint) at `position`, keeping the stops sorted by position.
|
/// Insert a copy of the stop at `source_index` (same color and midpoint) at `position`, keeping the stops sorted by position.
|
||||||
/// Returns the index where the copy was inserted, or `None` if `source_index` is out of range.
|
/// Returns the index where the copy was inserted, or `None` if `source_index` is out of range.
|
||||||
pub fn duplicate_stop(&mut self, source_index: usize, position: f64) -> Option<usize> {
|
pub fn duplicate_stop(&mut self, source_index: usize, position: f64, gradient_cyclic: bool) -> Option<usize> {
|
||||||
let color = self.color(source_index)?;
|
let color = self.color(source_index)?;
|
||||||
let midpoint = self.midpoint(source_index);
|
let midpoint = self.midpoint(source_index);
|
||||||
Some(self.insert_stop_values(position, midpoint, color))
|
Some(self.insert_stop_values(position, midpoint, color, gradient_cyclic))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splices a new stop into the sorted position, materializing explicit positions (an arbitrary insertion breaks even distribution)
|
/// Splices a new stop into the sorted position, materializing explicit positions (an arbitrary insertion breaks even distribution)
|
||||||
/// while giving the new stop a midpoint cell only if the attribute already exists.
|
/// while giving the new stop a midpoint cell only if the attribute already exists.
|
||||||
fn insert_stop_values(&mut self, position: f64, midpoint: f64, color: Color) -> usize {
|
fn insert_stop_values(&mut self, position: f64, midpoint: f64, color: Color, gradient_cyclic: bool) -> usize {
|
||||||
self.materialize_default_positions();
|
self.materialize_default_positions(gradient_cyclic);
|
||||||
let index = (0..self.len()).position(|i| self.position(i) > position).unwrap_or(self.len());
|
let index = (0..self.len()).position(|i| self.position(i, gradient_cyclic) > position).unwrap_or(self.len());
|
||||||
|
|
||||||
let mut item = Item::new_from_element(color).with_attribute(ATTR_POSITION, position);
|
let mut item = Item::new_from_element(color).with_attribute(ATTR_POSITION, position);
|
||||||
if self.has_midpoint_attribute() {
|
if self.has_midpoint_attribute() {
|
||||||
@@ -727,14 +759,14 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sort the stops in place by position; returns the new index of the stop that was at `previous_index` before sorting.
|
/// Sort the stops in place by position; returns the new index of the stop that was at `previous_index` before sorting.
|
||||||
fn sort_returning_new_index(&mut self, previous_index: usize) -> usize {
|
fn sort_returning_new_index(&mut self, previous_index: usize, gradient_cyclic: bool) -> usize {
|
||||||
// An absent position attribute is an even distribution, which is already sorted
|
// An absent position attribute is an even distribution, which is already sorted
|
||||||
if !self.has_position_attribute() {
|
if !self.has_position_attribute() {
|
||||||
return previous_index;
|
return previous_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut indices: Vec<usize> = (0..self.len()).collect();
|
let mut indices: Vec<usize> = (0..self.len()).collect();
|
||||||
indices.sort_by(|&a, &b| self.position(a).total_cmp(&self.position(b)));
|
indices.sort_by(|&a, &b| self.position(a, gradient_cyclic).total_cmp(&self.position(b, gradient_cyclic)));
|
||||||
let new_index = indices.iter().position(|&i| i == previous_index).unwrap_or(previous_index);
|
let new_index = indices.iter().position(|&i| i == previous_index).unwrap_or(previous_index);
|
||||||
self.0 = self.reordered(indices);
|
self.0 = self.reordered(indices);
|
||||||
new_index
|
new_index
|
||||||
@@ -743,10 +775,10 @@ impl Gradient {
|
|||||||
/// Gradient stops as evaluation and rendering should see them: positions clamped to the 0 to 1 range
|
/// Gradient stops as evaluation and rendering should see them: positions clamped to the 0 to 1 range
|
||||||
/// (infinities landing at the ends, a NaN dropping its stop from sampling since it has no defined placement)
|
/// (infinities landing at the ends, a NaN dropping its stop from sampling since it has no defined placement)
|
||||||
/// and sorted ascending, so the sampler and every renderer agree on how non-compliant authored data behaves.
|
/// and sorted ascending, so the sampler and every renderer agree on how non-compliant authored data behaves.
|
||||||
fn normalized_stops(&self) -> Vec<GradientStop> {
|
fn normalized_stops(&self, gradient_cyclic: bool) -> Vec<GradientStop> {
|
||||||
let mut stops: Vec<GradientStop> = (0..self.len())
|
let mut stops: Vec<GradientStop> = (0..self.len())
|
||||||
.filter_map(|index| {
|
.filter_map(|index| {
|
||||||
let position = self.position(index).clamp(0., 1.);
|
let position = self.position(index, gradient_cyclic).clamp(0., 1.);
|
||||||
if position.is_nan() {
|
if position.is_nan() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -763,7 +795,7 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Samples the gradient's color at `t`. Given a `t` outside the 0 to 1 range, the `gradient_spread` determines how the gradient extends.
|
/// Samples the gradient's color at `t`. Given a `t` outside the 0 to 1 range, the `gradient_spread` determines how the gradient extends.
|
||||||
pub fn evaluate(&self, t: f64, gradient_spread: GradientSpread, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Color {
|
pub fn evaluate(&self, t: f64, gradient_spread: GradientSpread, gradient_cyclic: bool, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Color {
|
||||||
let t = match gradient_spread {
|
let t = match gradient_spread {
|
||||||
GradientSpread::Pad => t.clamp(0., 1.),
|
GradientSpread::Pad => t.clamp(0., 1.),
|
||||||
GradientSpread::Repeat => t.rem_euclid(1.),
|
GradientSpread::Repeat => t.rem_euclid(1.),
|
||||||
@@ -779,8 +811,19 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stops = self.normalized_stops();
|
let stops = self.normalized_stops(gradient_cyclic);
|
||||||
let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return Color::BLACK };
|
let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return Color::BLACK };
|
||||||
|
|
||||||
|
if gradient_cyclic && (t < first.position || t > last.position) {
|
||||||
|
let wrap_length = first.position + 1. - last.position;
|
||||||
|
if wrap_length <= f64::EPSILON {
|
||||||
|
return first.color;
|
||||||
|
}
|
||||||
|
let local = if t >= last.position { t - last.position } else { t + 1. - last.position };
|
||||||
|
let adjusted_t = apply_midpoint(local / wrap_length, last.midpoint);
|
||||||
|
return interpolate_stop_colors(last.color, first.color, adjusted_t as f32, gradient_space, gradient_hue_direction);
|
||||||
|
}
|
||||||
|
|
||||||
if t <= first.position {
|
if t <= first.position {
|
||||||
return first.color;
|
return first.color;
|
||||||
}
|
}
|
||||||
@@ -800,11 +843,11 @@ impl Gradient {
|
|||||||
Color::BLACK
|
Color::BLACK
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort(&mut self) {
|
pub fn sort(&mut self, gradient_cyclic: bool) {
|
||||||
self.sort_returning_new_index(0);
|
self.sort_returning_new_index(0, gradient_cyclic);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reversed(&self) -> Self {
|
pub fn reversed(&self, gradient_cyclic: bool) -> Self {
|
||||||
let count = self.len();
|
let count = self.len();
|
||||||
let mut list = self.reordered((0..count).rev());
|
let mut list = self.reordered((0..count).rev());
|
||||||
|
|
||||||
@@ -815,11 +858,27 @@ impl Gradient {
|
|||||||
for position in positions {
|
for position in positions {
|
||||||
*position = 1. - *position;
|
*position = 1. - *position;
|
||||||
}
|
}
|
||||||
|
} else if gradient_cyclic && !self.has_position_attribute() {
|
||||||
|
// The cyclic even distribution starts at 0 rather than being symmetric across the range, so its flip must materialize
|
||||||
|
for index in 0..count {
|
||||||
|
list.set_attribute(ATTR_POSITION, index, 1. - even_position(count - 1 - index, count, true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Midpoints belong to the interval to a stop's right, so they shift by one stop as well as flipping
|
// Midpoints belong to the interval to a stop's right, so they shift by one stop as well as flipping;
|
||||||
|
// a cyclic gradient's final midpoint is its wrap handle, which flips in place
|
||||||
if self.has_midpoint_attribute() {
|
if self.has_midpoint_attribute() {
|
||||||
let midpoints: Vec<f64> = (0..count).map(|i| if i + 1 < count { 1. - self.midpoint(count - 2 - i) } else { 0.5 }).collect();
|
let midpoints: Vec<f64> = (0..count)
|
||||||
|
.map(|i| {
|
||||||
|
if i + 1 < count {
|
||||||
|
1. - self.midpoint(count - 2 - i)
|
||||||
|
} else if gradient_cyclic {
|
||||||
|
1. - self.midpoint(count - 1)
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
for (index, midpoint) in midpoints.into_iter().enumerate() {
|
for (index, midpoint) in midpoints.into_iter().enumerate() {
|
||||||
list.set_attribute(ATTR_MIDPOINT, index, midpoint);
|
list.set_attribute(ATTR_MIDPOINT, index, midpoint);
|
||||||
}
|
}
|
||||||
@@ -835,13 +894,13 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves and color space so the rendered gradient matches Graphite's interpolation rather than browser defaults.
|
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves and color space so the rendered gradient matches Graphite's interpolation rather than browser defaults.
|
||||||
pub fn to_css_linear_gradient(&self, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> String {
|
pub fn to_css_linear_gradient(&self, gradient_cyclic: bool, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> String {
|
||||||
if self.len() <= 1 {
|
if self.len() <= 1 {
|
||||||
let hex = self.color(0).map(|c| SRGBA8::from(c).to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
|
let hex = self.color(0).map(|c| SRGBA8::from(c).to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
|
||||||
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
||||||
}
|
}
|
||||||
let pieces = self
|
let pieces = self
|
||||||
.interpolated_samples(gradient_space, gradient_hue_direction)
|
.interpolated_samples(gradient_cyclic, gradient_space, gradient_hue_direction)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(position, color, _)| {
|
.map(|(position, color, _)| {
|
||||||
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||||
@@ -861,7 +920,7 @@ impl Gradient {
|
|||||||
/// The downstream SVG/CSS and Vello renderers interpolate between adjacent emitted stops in gamma sRGB space, so the
|
/// The downstream SVG/CSS and Vello renderers interpolate between adjacent emitted stops in gamma sRGB space, so the
|
||||||
/// subdivision emits enough samples that the gamma-drawn segments match the ramp's true curve: the midpoint bias, and
|
/// subdivision emits enough samples that the gamma-drawn segments match the ramp's true curve: the midpoint bias, and
|
||||||
/// the color space when it is not gamma itself.
|
/// the color space when it is not gamma itself.
|
||||||
pub fn interpolated_samples(&self, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Vec<(f64, Color, Option<f64>)> {
|
pub fn interpolated_samples(&self, gradient_cyclic: bool, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Vec<(f64, Color, Option<f64>)> {
|
||||||
/// Controls accuracy vs. number of samples tradeoff.
|
/// Controls accuracy vs. number of samples tradeoff.
|
||||||
/// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias.
|
/// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias.
|
||||||
const THRESHOLD: f64 = 2. / 255.;
|
const THRESHOLD: f64 = 2. / 255.;
|
||||||
@@ -918,7 +977,7 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let stops = self.normalized_stops();
|
let stops = self.normalized_stops(gradient_cyclic);
|
||||||
let count = stops.len();
|
let count = stops.len();
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return vec![];
|
return vec![];
|
||||||
@@ -938,7 +997,7 @@ impl Gradient {
|
|||||||
let midpoint = sanitized_midpoint(stops[i].midpoint);
|
let midpoint = sanitized_midpoint(stops[i].midpoint);
|
||||||
let next_midpoint = sanitized_midpoint(stops[i + 1].midpoint);
|
let next_midpoint = sanitized_midpoint(stops[i + 1].midpoint);
|
||||||
|
|
||||||
// Add the start stop (subsequent segments share the previous end stop)
|
// Add the start stop (subsequent intervals share the previous end stop)
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
result.push((pos_a, color_a, Some(midpoint)));
|
result.push((pos_a, color_a, Some(midpoint)));
|
||||||
}
|
}
|
||||||
@@ -952,6 +1011,58 @@ impl Gradient {
|
|||||||
result.push((pos_b, color_b, Some(next_midpoint)));
|
result.push((pos_b, color_b, Some(next_midpoint)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bake the wrapped interval into the flat list: the piece from the last stop to the 1|0 boundary, and the piece
|
||||||
|
// continuing from the boundary to the first stop, so both ends of the emitted list share the boundary-crossing
|
||||||
|
// color and downstream renderers stay unaware of the cycle
|
||||||
|
if gradient_cyclic {
|
||||||
|
let (first, last) = (&stops[0], &stops[count - 1]);
|
||||||
|
let wrap_length = first.position + 1. - last.position;
|
||||||
|
if wrap_length > f64::EPSILON {
|
||||||
|
let wrap_midpoint = sanitized_midpoint(last.midpoint);
|
||||||
|
let boundary_fraction = (1. - last.position) / wrap_length;
|
||||||
|
let y_boundary = apply_midpoint(boundary_fraction, wrap_midpoint);
|
||||||
|
let boundary_color = interpolate_stop_colors(last.color, first.color, y_boundary as f32, gradient_space, gradient_hue_direction);
|
||||||
|
|
||||||
|
if last.position < 1. {
|
||||||
|
let virtual_end = last.position + wrap_length;
|
||||||
|
subdivide(
|
||||||
|
0.,
|
||||||
|
boundary_fraction,
|
||||||
|
wrap_midpoint,
|
||||||
|
last.position,
|
||||||
|
virtual_end,
|
||||||
|
last.color,
|
||||||
|
first.color,
|
||||||
|
gradient_space,
|
||||||
|
gradient_hue_direction,
|
||||||
|
&mut result,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
result.push((1., boundary_color, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
if first.position > 0. {
|
||||||
|
let virtual_start = last.position - 1.;
|
||||||
|
let mut leading = vec![(0., boundary_color, None)];
|
||||||
|
subdivide(
|
||||||
|
boundary_fraction,
|
||||||
|
1.,
|
||||||
|
wrap_midpoint,
|
||||||
|
virtual_start,
|
||||||
|
first.position,
|
||||||
|
last.color,
|
||||||
|
first.color,
|
||||||
|
gradient_space,
|
||||||
|
gradient_hue_direction,
|
||||||
|
&mut leading,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
leading.append(&mut result);
|
||||||
|
result = leading;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If every midpoint is 0.5 (or within epsilon), turn all midpoints to None
|
// If every midpoint is 0.5 (or within epsilon), turn all midpoints to None
|
||||||
if result.iter().all(|(_, _, midpoint)| matches!(midpoint, Some(m) if (m - 0.5).abs() < 1e-6)) {
|
if result.iter().all(|(_, _, midpoint)| matches!(midpoint, Some(m) if (m - 0.5).abs() < 1e-6)) {
|
||||||
result.iter_mut().for_each(|(_, _, midpoint)| *midpoint = None);
|
result.iter_mut().for_each(|(_, _, midpoint)| *midpoint = None);
|
||||||
@@ -1013,11 +1124,11 @@ impl GradientSpread {
|
|||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[widget(Dropdown)]
|
#[widget(Dropdown)]
|
||||||
pub enum GradientSpace {
|
pub enum GradientSpace {
|
||||||
/// Interpolates between stops in the OkLab perceptual color space, keeping transitions visually even.
|
/// Interpolates between stops in the OkLab color space, the modern perceptual standard.
|
||||||
#[default]
|
#[default]
|
||||||
#[label("Perceptual (OkLab)")]
|
#[label("Perceptual (OkLab)")]
|
||||||
OkLab,
|
OkLab,
|
||||||
/// Interpolates between stops in the CIE Lab color space, the longtime perceptual standard.
|
/// Interpolates between stops in the CIE Lab color space, the traditional perceptual standard.
|
||||||
#[label("Perceptual (Lab)")]
|
#[label("Perceptual (Lab)")]
|
||||||
Lab,
|
Lab,
|
||||||
/// Interpolates between stops in the polar form of OkLab, arcing through hue instead of fading through gray.
|
/// Interpolates between stops in the polar form of OkLab, arcing through hue instead of fading through gray.
|
||||||
@@ -1026,7 +1137,7 @@ pub enum GradientSpace {
|
|||||||
/// Interpolates between stops in the polar form of CIE Lab, arcing through hue instead of fading through gray.
|
/// Interpolates between stops in the polar form of CIE Lab, arcing through hue instead of fading through gray.
|
||||||
#[label("Perceptual Hue (LCh)")]
|
#[label("Perceptual Hue (LCh)")]
|
||||||
LCh,
|
LCh,
|
||||||
/// Interpolates between stops in linear light, keeping transitions evenly bright.
|
/// Interpolates between stops in linear light, keeping transitions uniformly bright.
|
||||||
#[menu_separator]
|
#[menu_separator]
|
||||||
#[cfg_attr(feature = "serde", serde(alias = "SrgbLinear"))]
|
#[cfg_attr(feature = "serde", serde(alias = "SrgbLinear"))]
|
||||||
#[label("Linear (RGB)")]
|
#[label("Linear (RGB)")]
|
||||||
@@ -1035,10 +1146,10 @@ pub enum GradientSpace {
|
|||||||
#[cfg_attr(feature = "serde", serde(alias = "SrgbGamma"))]
|
#[cfg_attr(feature = "serde", serde(alias = "SrgbGamma"))]
|
||||||
#[label("Classic (RGB)")]
|
#[label("Classic (RGB)")]
|
||||||
RgbGamma,
|
RgbGamma,
|
||||||
/// Interpolates between stops in the hue, saturation, and value cylinder, keeping tints at full brightness.
|
/// Interpolates between stops in the hue/saturation/value cylinder, keeping tints at full brightness.
|
||||||
#[label("Classic Hue (HSV)")]
|
#[label("Classic Hue (HSV)")]
|
||||||
Hsv,
|
Hsv,
|
||||||
/// Interpolates between stops in the hue, saturation, and lightness cylinder.
|
/// Interpolates between stops in the hue/saturation/lightness cylinder.
|
||||||
#[label("Classic Hue (HSL)")]
|
#[label("Classic Hue (HSL)")]
|
||||||
Hsl,
|
Hsl,
|
||||||
}
|
}
|
||||||
@@ -1151,14 +1262,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn default_is_empty_and_black_to_white_is_the_artist_starting_gradient() {
|
fn default_is_empty_and_black_to_white_is_the_artist_starting_gradient() {
|
||||||
assert!(Gradient::default().is_empty());
|
assert!(Gradient::default().is_empty());
|
||||||
assert_eq!(Gradient::black_to_white().positions(), vec![0., 1.]);
|
assert_eq!(Gradient::black_to_white().positions(false), vec![0., 1.]);
|
||||||
assert_eq!(Gradient::default().evaluate(0.5, Default::default(), Default::default(), Default::default()), Color::BLACK);
|
assert_eq!(Gradient::default().evaluate(0.5, Default::default(), false, Default::default(), Default::default()), Color::BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn absent_attributes_default_to_even_positions_and_linear_midpoints() {
|
fn absent_attributes_default_to_even_positions_and_linear_midpoints() {
|
||||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
assert_eq!(gradient.positions(), vec![0., 0.5, 1.]);
|
assert_eq!(gradient.positions(false), vec![0., 0.5, 1.]);
|
||||||
assert_eq!(gradient.midpoints(), vec![0.5, 0.5, 0.5]);
|
assert_eq!(gradient.midpoints(), vec![0.5, 0.5, 0.5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,11 +1384,11 @@ mod tests {
|
|||||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
// Gamma needs no synthesized samples since the renderers already draw gamma segments
|
// Gamma needs no synthesized samples since the renderers already draw gamma segments
|
||||||
assert_eq!(gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default()).len(), 2);
|
assert_eq!(gradient.interpolated_samples(false, GradientSpace::RgbGamma, Default::default()).len(), 2);
|
||||||
|
|
||||||
// A linear black-to-white ramp curves away from any single gamma segment, so samples must densify,
|
// A linear black-to-white ramp curves away from any single gamma segment, so samples must densify,
|
||||||
// keeping the end stops in place and every synthesized color on the linear-light line
|
// keeping the end stops in place and every synthesized color on the linear-light line
|
||||||
let samples = gradient.interpolated_samples(GradientSpace::RgbLinear, Default::default());
|
let samples = gradient.interpolated_samples(false, GradientSpace::RgbLinear, Default::default());
|
||||||
assert!(samples.len() > 2, "the linear space should synthesize samples, got {}", samples.len());
|
assert!(samples.len() > 2, "the linear space should synthesize samples, got {}", samples.len());
|
||||||
assert_eq!(samples.first().unwrap().0, 0.);
|
assert_eq!(samples.first().unwrap().0, 0.);
|
||||||
assert_eq!(samples.last().unwrap().0, 1.);
|
assert_eq!(samples.last().unwrap().0, 1.);
|
||||||
@@ -1291,7 +1402,7 @@ mod tests {
|
|||||||
|
|
||||||
// Identical end colors leave nothing to densify
|
// Identical end colors leave nothing to densify
|
||||||
let flat = Gradient::from(vec![Color::WHITE, Color::WHITE]);
|
let flat = Gradient::from(vec![Color::WHITE, Color::WHITE]);
|
||||||
assert_eq!(flat.interpolated_samples(GradientSpace::RgbLinear, Default::default()).len(), 2);
|
assert_eq!(flat.interpolated_samples(false, GradientSpace::RgbLinear, Default::default()).len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1322,7 +1433,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut gradient = Gradient::from(vec![color_a, color_b]);
|
let mut gradient = Gradient::from(vec![color_a, color_b]);
|
||||||
gradient.set_midpoints(&[midpoint, 0.5]);
|
gradient.set_midpoints(&[midpoint, 0.5]);
|
||||||
let samples = gradient.interpolated_samples(gradient_space, gradient_hue_direction);
|
let samples = gradient.interpolated_samples(false, gradient_space, gradient_hue_direction);
|
||||||
|
|
||||||
for probe in 0..=1000 {
|
for probe in 0..=1000 {
|
||||||
let t = probe as f64 / 1000.;
|
let t = probe as f64 / 1000.;
|
||||||
@@ -1358,13 +1469,13 @@ mod tests {
|
|||||||
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
|
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
|
||||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear, Default::default(), Default::default()), Color::TRANSPARENT);
|
assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear, false, Default::default(), Default::default()), Color::TRANSPARENT);
|
||||||
assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear, Default::default(), Default::default()), Color::TRANSPARENT);
|
assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear, false, Default::default(), Default::default()), Color::TRANSPARENT);
|
||||||
|
|
||||||
for t in [0., 0.25, 1.] {
|
for t in [0., 0.25, 1.] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
gradient.evaluate(t, GradientSpread::Clear, Default::default(), Default::default()),
|
gradient.evaluate(t, GradientSpread::Clear, false, Default::default(), Default::default()),
|
||||||
gradient.evaluate(t, GradientSpread::Pad, Default::default(), Default::default()),
|
gradient.evaluate(t, GradientSpread::Pad, false, Default::default(), Default::default()),
|
||||||
"inside the range Clear must match Pad at t = {t}"
|
"inside the range Clear must match Pad at t = {t}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1374,9 +1485,9 @@ mod tests {
|
|||||||
fn evaluate_follows_the_gradient_space() {
|
fn evaluate_follows_the_gradient_space() {
|
||||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
let oklab = gradient.evaluate(0.5, Default::default(), GradientSpace::OkLab, Default::default());
|
let oklab = gradient.evaluate(0.5, Default::default(), false, GradientSpace::OkLab, Default::default());
|
||||||
let linear = gradient.evaluate(0.5, Default::default(), GradientSpace::RgbLinear, Default::default());
|
let linear = gradient.evaluate(0.5, Default::default(), false, GradientSpace::RgbLinear, Default::default());
|
||||||
let gamma = gradient.evaluate(0.5, Default::default(), GradientSpace::RgbGamma, Default::default());
|
let gamma = gradient.evaluate(0.5, Default::default(), false, GradientSpace::RgbGamma, Default::default());
|
||||||
|
|
||||||
assert_eq!(linear, Color::BLACK.lerp(&Color::WHITE, 0.5));
|
assert_eq!(linear, Color::BLACK.lerp(&Color::WHITE, 0.5));
|
||||||
assert_eq!(gamma, Color::BLACK.lerp_gamma_srgb(&Color::WHITE, 0.5));
|
assert_eq!(gamma, Color::BLACK.lerp_gamma_srgb(&Color::WHITE, 0.5));
|
||||||
@@ -1398,28 +1509,28 @@ mod tests {
|
|||||||
|
|
||||||
// Red to blue in HSL crosses through magenta on the shorter arc (300 degrees), not through green (120 degrees)
|
// Red to blue in HSL crosses through magenta on the shorter arc (300 degrees), not through green (120 degrees)
|
||||||
let red_to_blue = Gradient::from(vec![Color::RED, Color::BLUE]);
|
let red_to_blue = Gradient::from(vec![Color::RED, Color::BLUE]);
|
||||||
let magenta = red_to_blue.evaluate(0.5, Default::default(), GradientSpace::Hsl, Default::default());
|
let magenta = red_to_blue.evaluate(0.5, Default::default(), false, GradientSpace::Hsl, Default::default());
|
||||||
for (channel, expected) in [(magenta.r(), 1.), (magenta.g(), 0.), (magenta.b(), 1.)] {
|
for (channel, expected) in [(magenta.r(), 1.), (magenta.g(), 0.), (magenta.b(), 1.)] {
|
||||||
assert!((channel - expected).abs() < 1e-3, "the HSL mid color of red and blue should be magenta, got {magenta:?}");
|
assert!((channel - expected).abs() < 1e-3, "the HSL mid color of red and blue should be magenta, got {magenta:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// White's hue is powerless, so an OkLCh interpolation toward it keeps red's hue instead of drifting toward white's arbitrary hue
|
// White's hue is powerless, so an OkLCh interpolation toward it keeps red's hue instead of drifting toward white's arbitrary hue
|
||||||
let red_to_white = Gradient::from(vec![Color::RED, Color::WHITE]);
|
let red_to_white = Gradient::from(vec![Color::RED, Color::WHITE]);
|
||||||
let pink = red_to_white.evaluate(0.5, Default::default(), GradientSpace::OkLCh, Default::default());
|
let pink = red_to_white.evaluate(0.5, Default::default(), false, GradientSpace::OkLCh, Default::default());
|
||||||
let [_, _, red_hue] = color::Oklch::from_linear_srgb([Color::RED.r(), Color::RED.g(), Color::RED.b()]);
|
let [_, _, red_hue] = color::Oklch::from_linear_srgb([Color::RED.r(), Color::RED.g(), Color::RED.b()]);
|
||||||
let [_, pink_chroma, pink_hue] = color::Oklch::from_linear_srgb([pink.r(), pink.g(), pink.b()]);
|
let [_, pink_chroma, pink_hue] = color::Oklch::from_linear_srgb([pink.r(), pink.g(), pink.b()]);
|
||||||
assert!(pink_chroma > 0.05, "the mid color should stay chromatic, got {pink:?}");
|
assert!(pink_chroma > 0.05, "the mid color should stay chromatic, got {pink:?}");
|
||||||
assert!((pink_hue - red_hue).abs() < 0.5, "the mid hue should hold red's {red_hue} degrees, got {pink_hue}");
|
assert!((pink_hue - red_hue).abs() < 0.5, "the mid hue should hold red's {red_hue} degrees, got {pink_hue}");
|
||||||
|
|
||||||
// HSV rides the cube's top face toward white, keeping the mid tint at full brightness where HSL dips
|
// HSV rides the cube's top face toward white, keeping the mid tint at full brightness where HSL dips
|
||||||
let tint = red_to_white.evaluate(0.5, Default::default(), GradientSpace::Hsv, Default::default());
|
let tint = red_to_white.evaluate(0.5, Default::default(), false, GradientSpace::Hsv, Default::default());
|
||||||
for (channel, target) in tint.to_gamma_srgb_channels().into_iter().zip([1., 0.5, 0.5, 1.]) {
|
for (channel, target) in tint.to_gamma_srgb_channels().into_iter().zip([1., 0.5, 0.5, 1.]) {
|
||||||
assert!((channel - target).abs() < 1e-3, "the HSV mid tint of red and white should be gamma (1, 0.5, 0.5), got {tint:?}");
|
assert!((channel - target).abs() < 1e-3, "the HSV mid tint of red and white should be gamma (1, 0.5, 0.5), got {tint:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toward black both saturation and value halve, the classic HSV shade that neither HSL nor HWB produces
|
// Toward black both saturation and value halve, the classic HSV shade that neither HSL nor HWB produces
|
||||||
let red_to_black = Gradient::from(vec![Color::RED, Color::BLACK]);
|
let red_to_black = Gradient::from(vec![Color::RED, Color::BLACK]);
|
||||||
let shade = red_to_black.evaluate(0.5, Default::default(), GradientSpace::Hsv, Default::default());
|
let shade = red_to_black.evaluate(0.5, Default::default(), false, GradientSpace::Hsv, Default::default());
|
||||||
for (channel, target) in shade.to_gamma_srgb_channels().into_iter().zip([0.5, 0.25, 0.25, 1.]) {
|
for (channel, target) in shade.to_gamma_srgb_channels().into_iter().zip([0.5, 0.25, 0.25, 1.]) {
|
||||||
assert!((channel - target).abs() < 1e-3, "the HSV mid shade of red and black should be gamma (0.5, 0.25, 0.25), got {shade:?}");
|
assert!((channel - target).abs() < 1e-3, "the HSV mid shade of red and black should be gamma (0.5, 0.25, 0.25), got {shade:?}");
|
||||||
}
|
}
|
||||||
@@ -1437,7 +1548,7 @@ mod tests {
|
|||||||
(GradientHueDirection::Decreasing, [1., 0., 1.]),
|
(GradientHueDirection::Decreasing, [1., 0., 1.]),
|
||||||
];
|
];
|
||||||
for (gradient_hue_direction, expected_rgb) in expectations {
|
for (gradient_hue_direction, expected_rgb) in expectations {
|
||||||
let mid = red_to_blue.evaluate(0.5, Default::default(), GradientSpace::Hsl, gradient_hue_direction);
|
let mid = red_to_blue.evaluate(0.5, Default::default(), false, GradientSpace::Hsl, gradient_hue_direction);
|
||||||
for (channel, target) in [mid.r(), mid.g(), mid.b()].into_iter().zip(expected_rgb) {
|
for (channel, target) in [mid.r(), mid.g(), mid.b()].into_iter().zip(expected_rgb) {
|
||||||
assert!(
|
assert!(
|
||||||
(channel - target).abs() < 1e-3,
|
(channel - target).abs() < 1e-3,
|
||||||
@@ -1448,7 +1559,7 @@ mod tests {
|
|||||||
|
|
||||||
// Identical hues under Longer take a full turn around the wheel, passing through cyan halfway
|
// Identical hues under Longer take a full turn around the wheel, passing through cyan halfway
|
||||||
let red_to_red = Gradient::from(vec![Color::RED, Color::RED]);
|
let red_to_red = Gradient::from(vec![Color::RED, Color::RED]);
|
||||||
let mid = red_to_red.evaluate(0.5, Default::default(), GradientSpace::Hsl, GradientHueDirection::Longer);
|
let mid = red_to_red.evaluate(0.5, Default::default(), false, GradientSpace::Hsl, GradientHueDirection::Longer);
|
||||||
for (channel, target) in [mid.r(), mid.g(), mid.b()].into_iter().zip([0., 1., 1.]) {
|
for (channel, target) in [mid.r(), mid.g(), mid.b()].into_iter().zip([0., 1., 1.]) {
|
||||||
assert!((channel - target).abs() < 1e-3, "the full-turn mid of red and red should be cyan, got {mid:?}");
|
assert!((channel - target).abs() < 1e-3, "the full-turn mid of red and red should be cyan, got {mid:?}");
|
||||||
}
|
}
|
||||||
@@ -1456,29 +1567,46 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gradient_ui_write_back_elides_default_attributes() {
|
fn gradient_ui_write_back_elides_default_attributes() {
|
||||||
|
// The picker always sends explicit positions, so the write-back is what restores the canonical absence-as-default form
|
||||||
|
let write_back = |gradient: &Gradient, gradient_cyclic: bool| {
|
||||||
|
let sent = GradientRamp::<SRGBA8> { gradient_cyclic, ..gradient.into() };
|
||||||
|
Gradient::from(&GradientRamp::from(&sent))
|
||||||
|
};
|
||||||
|
|
||||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
|
gradient.set_positions(&[0., 0.5, 1.]);
|
||||||
gradient.set_midpoints(&[0.7, 0.5, 0.5]);
|
gradient.set_midpoints(&[0.7, 0.5, 0.5]);
|
||||||
|
|
||||||
let round_tripped = Gradient::from(&GradientStops::<SRGBA8>::from(&gradient));
|
let written_back = write_back(&gradient, false);
|
||||||
assert!(!round_tripped.has_position_attribute(), "materialized even positions should elide on write-back");
|
assert!(!written_back.has_position_attribute(), "the even distribution should elide on write-back");
|
||||||
assert_eq!(round_tripped.midpoints(), vec![0.7, 0.5, 0.5]);
|
assert_eq!(written_back.midpoints(), vec![0.7, 0.5, 0.5]);
|
||||||
|
|
||||||
|
// Cyclic ramps spread over one more interval, so thirds are the elidable distribution and halves are not
|
||||||
|
let mut cyclic = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
|
cyclic.set_positions(&[0., 1. / 3., 2. / 3.]);
|
||||||
|
assert!(!write_back(&cyclic, true).has_position_attribute(), "the cyclic even distribution should elide on write-back");
|
||||||
|
assert_eq!(
|
||||||
|
write_back(&gradient, true).positions(true),
|
||||||
|
vec![0., 0.5, 1.],
|
||||||
|
"positions that only look default when non-cyclic must survive"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nondefault_attributes_elide_default_values() {
|
fn nondefault_attributes_elide_default_values() {
|
||||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
assert_eq!(gradient.nondefault_positions(), None);
|
assert_eq!(gradient.nondefault_positions(false), None);
|
||||||
assert_eq!(gradient.nondefault_midpoints(), None);
|
assert_eq!(gradient.nondefault_midpoints(), None);
|
||||||
|
|
||||||
// Explicit attributes that merely restate the defaults still elide
|
// Explicit attributes that merely restate the defaults still elide
|
||||||
gradient.set_positions(&[0., 0.5, 1.]);
|
gradient.set_positions(&[0., 0.5, 1.]);
|
||||||
gradient.set_midpoints(&[0.5, 0.5, 0.5]);
|
gradient.set_midpoints(&[0.5, 0.5, 0.5]);
|
||||||
assert_eq!(gradient.nondefault_positions(), None);
|
assert_eq!(gradient.nondefault_positions(false), None);
|
||||||
assert_eq!(gradient.nondefault_midpoints(), None);
|
assert_eq!(gradient.nondefault_midpoints(), None);
|
||||||
|
|
||||||
gradient.set_positions(&[0., 0.25, 1.]);
|
gradient.set_positions(&[0., 0.25, 1.]);
|
||||||
gradient.set_midpoints(&[0.5, 0.7, 0.5]);
|
gradient.set_midpoints(&[0.5, 0.7, 0.5]);
|
||||||
assert_eq!(gradient.nondefault_positions(), Some(vec![0., 0.25, 1.]));
|
assert_eq!(gradient.nondefault_positions(false), Some(vec![0., 0.25, 1.]));
|
||||||
assert_eq!(gradient.nondefault_midpoints(), Some(vec![0.5, 0.7, 0.5]));
|
assert_eq!(gradient.nondefault_midpoints(), Some(vec![0.5, 0.7, 0.5]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1487,10 +1615,10 @@ mod tests {
|
|||||||
// Stored positions stay as authored, but consumers see them clamped to the 0 to 1 range and sorted
|
// Stored positions stay as authored, but consumers see them clamped to the 0 to 1 range and sorted
|
||||||
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK, Color::RED]);
|
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK, Color::RED]);
|
||||||
gradient.set_positions(&[1.5, 0.4, -0.5]);
|
gradient.set_positions(&[1.5, 0.4, -0.5]);
|
||||||
assert_eq!(gradient.positions(), vec![1.5, 0.4, -0.5]);
|
assert_eq!(gradient.positions(false), vec![1.5, 0.4, -0.5]);
|
||||||
|
|
||||||
let sample_positions: Vec<f64> = gradient
|
let sample_positions: Vec<f64> = gradient
|
||||||
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
|
.interpolated_samples(false, GradientSpace::RgbGamma, Default::default())
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(position, ..)| *position)
|
.map(|(position, ..)| *position)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1498,8 +1626,8 @@ mod tests {
|
|||||||
assert_eq!(sample_positions.first(), Some(&0.));
|
assert_eq!(sample_positions.first(), Some(&0.));
|
||||||
assert_eq!(sample_positions.last(), Some(&1.));
|
assert_eq!(sample_positions.last(), Some(&1.));
|
||||||
|
|
||||||
assert_eq!(gradient.evaluate(0., Default::default(), Default::default(), Default::default()), Color::RED);
|
assert_eq!(gradient.evaluate(0., Default::default(), false, Default::default(), Default::default()), Color::RED);
|
||||||
assert_eq!(gradient.evaluate(1., Default::default(), Default::default(), Default::default()), Color::WHITE);
|
assert_eq!(gradient.evaluate(1., Default::default(), false, Default::default(), Default::default()), Color::WHITE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1508,13 +1636,13 @@ mod tests {
|
|||||||
gradient.set_positions(&[f64::INFINITY, f64::NEG_INFINITY]);
|
gradient.set_positions(&[f64::INFINITY, f64::NEG_INFINITY]);
|
||||||
|
|
||||||
let sample_positions: Vec<f64> = gradient
|
let sample_positions: Vec<f64> = gradient
|
||||||
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
|
.interpolated_samples(false, GradientSpace::RgbGamma, Default::default())
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(position, ..)| *position)
|
.map(|(position, ..)| *position)
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(sample_positions, vec![0., 1.]);
|
assert_eq!(sample_positions, vec![0., 1.]);
|
||||||
assert_eq!(gradient.evaluate(0., Default::default(), Default::default(), Default::default()), Color::BLACK);
|
assert_eq!(gradient.evaluate(0., Default::default(), false, Default::default(), Default::default()), Color::BLACK);
|
||||||
assert_eq!(gradient.evaluate(1., Default::default(), Default::default(), Default::default()), Color::WHITE);
|
assert_eq!(gradient.evaluate(1., Default::default(), false, Default::default(), Default::default()), Color::WHITE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1523,24 +1651,24 @@ mod tests {
|
|||||||
gradient.set_positions(&[0., f64::NAN, 1.]);
|
gradient.set_positions(&[0., f64::NAN, 1.]);
|
||||||
|
|
||||||
let sample_positions: Vec<f64> = gradient
|
let sample_positions: Vec<f64> = gradient
|
||||||
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
|
.interpolated_samples(false, GradientSpace::RgbGamma, Default::default())
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(position, ..)| *position)
|
.map(|(position, ..)| *position)
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(sample_positions, vec![0., 1.]);
|
assert_eq!(sample_positions, vec![0., 1.]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
gradient.evaluate(0.5, Default::default(), GradientSpace::RgbLinear, Default::default()),
|
gradient.evaluate(0.5, Default::default(), false, GradientSpace::RgbLinear, Default::default()),
|
||||||
Color::WHITE.lerp(&Color::RED, 0.5)
|
Color::WHITE.lerp(&Color::RED, 0.5)
|
||||||
);
|
);
|
||||||
|
|
||||||
// A non-finite position is preserved as nondefault so write-back elision cannot resurrect the dropped stop
|
// A non-finite position is preserved as nondefault so write-back elision cannot resurrect the dropped stop
|
||||||
assert!(gradient.nondefault_positions().is_some());
|
assert!(gradient.nondefault_positions(false).is_some());
|
||||||
|
|
||||||
// With every position NaN the gradient samples as stopless, painting solid black to signal the upstream bug
|
// With every position NaN the gradient samples as stopless, painting solid black to signal the upstream bug
|
||||||
let mut gradient = Gradient::from(vec![Color::WHITE, Color::RED]);
|
let mut gradient = Gradient::from(vec![Color::WHITE, Color::RED]);
|
||||||
gradient.set_positions(&[f64::NAN, f64::NAN]);
|
gradient.set_positions(&[f64::NAN, f64::NAN]);
|
||||||
assert!(gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default()).is_empty());
|
assert!(gradient.interpolated_samples(false, GradientSpace::RgbGamma, Default::default()).is_empty());
|
||||||
assert_eq!(gradient.evaluate(0.5, Default::default(), Default::default(), Default::default()), Color::BLACK);
|
assert_eq!(gradient.evaluate(0.5, Default::default(), false, Default::default(), Default::default()), Color::BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1548,21 +1676,152 @@ mod tests {
|
|||||||
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK]);
|
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK]);
|
||||||
gradient.set_positions(&[0.3, 1.]);
|
gradient.set_positions(&[0.3, 1.]);
|
||||||
|
|
||||||
let samples = gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default());
|
let samples = gradient.interpolated_samples(false, GradientSpace::RgbGamma, Default::default());
|
||||||
assert_eq!(samples[0], (0.3, Color::WHITE, None), "renderers that need a flat lead-in before the first stop add it themselves");
|
assert_eq!(samples[0], (0.3, Color::WHITE, None), "renderers that need a flat lead-in before the first stop add it themselves");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nan_midpoints_read_as_linear() {
|
fn nan_midpoints_read_as_linear() {
|
||||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
let linear_result = gradient.evaluate(0.25, Default::default(), Default::default(), Default::default());
|
let linear_result = gradient.evaluate(0.25, Default::default(), false, Default::default(), Default::default());
|
||||||
|
|
||||||
gradient.set_midpoints(&[f64::NAN, f64::NAN]);
|
gradient.set_midpoints(&[f64::NAN, f64::NAN]);
|
||||||
assert_eq!(gradient.evaluate(0.25, Default::default(), Default::default(), Default::default()), linear_result);
|
assert_eq!(gradient.evaluate(0.25, Default::default(), false, Default::default(), Default::default()), linear_result);
|
||||||
let no_nan_annotations = gradient
|
let no_nan_annotations = gradient
|
||||||
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
|
.interpolated_samples(false, GradientSpace::RgbGamma, Default::default())
|
||||||
.iter()
|
.iter()
|
||||||
.all(|(position, _, midpoint)| position.is_finite() && !midpoint.is_some_and(|midpoint| midpoint.is_nan()));
|
.all(|(position, _, midpoint)| position.is_finite() && !midpoint.is_some_and(|midpoint| midpoint.is_nan()));
|
||||||
assert!(no_nan_annotations, "NaN must not escape into rendered sample annotations");
|
assert!(no_nan_annotations, "NaN must not escape into rendered sample annotations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gradient_cyclic_serializes_only_when_set_and_rides_the_item_attribute() {
|
||||||
|
let ramp = GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]));
|
||||||
|
let json = serde_json::to_string(&ramp).unwrap();
|
||||||
|
assert!(!json.contains("gradient_cyclic"), "the default non-cyclic flag must not serialize: {json}");
|
||||||
|
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), ramp);
|
||||||
|
|
||||||
|
let cyclic = GradientRamp { gradient_cyclic: true, ..ramp };
|
||||||
|
let json = serde_json::to_string(&cyclic).unwrap();
|
||||||
|
assert!(json.contains(r#""gradient_cyclic":true"#), "an enabled cyclic flag must serialize: {json}");
|
||||||
|
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), cyclic);
|
||||||
|
|
||||||
|
let item = Item::<Gradient>::from(cyclic.clone());
|
||||||
|
assert!(
|
||||||
|
item.attribute_cloned_or_default::<bool>(ATTR_GRADIENT_CYCLIC),
|
||||||
|
"the runtime item should carry the cyclic flag as its attribute"
|
||||||
|
);
|
||||||
|
assert_eq!(GradientRamp::from(&item), cyclic);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_reserves_the_wrapped_interval_in_the_even_distribution() {
|
||||||
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED, Color::BLUE]);
|
||||||
|
assert_eq!(gradient.positions(false), vec![0., 1. / 3., 2. / 3., 1.]);
|
||||||
|
assert_eq!(gradient.positions(true), vec![0., 0.25, 0.5, 0.75]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_evaluate_wraps_from_the_last_stop_back_to_the_first() {
|
||||||
|
// Elided cyclic positions put the stops at 0 and 0.5, so the wrapped interval spans the other half
|
||||||
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
let quarter = gradient.evaluate(0.25, Default::default(), true, GradientSpace::RgbLinear, Default::default());
|
||||||
|
let wrap_quarter = gradient.evaluate(0.75, Default::default(), true, GradientSpace::RgbLinear, Default::default());
|
||||||
|
assert_eq!(quarter, Color::BLACK.lerp(&Color::WHITE, 0.5));
|
||||||
|
assert_eq!(wrap_quarter, Color::WHITE.lerp(&Color::BLACK, 0.5));
|
||||||
|
|
||||||
|
// A wrapped interval crossing the 1|0 boundary reads as one continuous span, so its two sides agree at the seam
|
||||||
|
let mut offset = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
offset.set_positions(&[0.25, 0.5]);
|
||||||
|
let at_end = offset.evaluate(1., Default::default(), true, GradientSpace::RgbLinear, Default::default());
|
||||||
|
let at_start = offset.evaluate(0., Default::default(), true, GradientSpace::RgbLinear, Default::default());
|
||||||
|
assert_eq!(at_end, at_start, "the 1|0 boundary must be seamless");
|
||||||
|
assert_eq!(at_end, Color::WHITE.lerp(&Color::BLACK, 2. / 3.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_wrapped_interval_times_with_the_last_stops_midpoint() {
|
||||||
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
gradient.set_midpoints(&[0.5, 0.25]);
|
||||||
|
|
||||||
|
let expected_t = apply_midpoint(0.5, 0.25);
|
||||||
|
let mid = gradient.evaluate(0.75, Default::default(), true, GradientSpace::RgbLinear, Default::default());
|
||||||
|
assert_eq!(mid, Color::WHITE.lerp(&Color::BLACK, expected_t as f32));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_samples_bake_the_wrap_and_play_back_within_tolerance() {
|
||||||
|
let mut gradient = Gradient::from(vec![Color::RED, Color::WHITE]);
|
||||||
|
gradient.set_positions(&[0.25, 0.5]);
|
||||||
|
gradient.set_midpoints(&[0.5, 0.3]);
|
||||||
|
|
||||||
|
let samples = gradient.interpolated_samples(true, GradientSpace::OkLab, Default::default());
|
||||||
|
assert_eq!(samples.first().unwrap().0, 0.);
|
||||||
|
assert_eq!(samples.last().unwrap().0, 1.);
|
||||||
|
assert_eq!(samples.first().unwrap().1, samples.last().unwrap().1, "both ends must share the boundary-crossing color");
|
||||||
|
assert!(samples.windows(2).all(|pair| pair[0].0 <= pair[1].0), "samples must ascend");
|
||||||
|
|
||||||
|
// The flat samples' gamma playback must track the true cyclic curve, wrapped interval included
|
||||||
|
for probe in 0..=400 {
|
||||||
|
let t = probe as f64 / 400.;
|
||||||
|
|
||||||
|
let after = samples.iter().position(|&(position, ..)| position >= t).unwrap_or(samples.len() - 1);
|
||||||
|
let playback = if after == 0 {
|
||||||
|
samples[0].1
|
||||||
|
} else {
|
||||||
|
let (left_position, left_color, _) = samples[after - 1];
|
||||||
|
let (right_position, right_color, _) = samples[after];
|
||||||
|
let span = right_position - left_position;
|
||||||
|
if span < 1e-12 {
|
||||||
|
right_color
|
||||||
|
} else {
|
||||||
|
left_color.lerp_gamma_srgb(&right_color, ((t - left_position) / span) as f32)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let true_color = gradient.evaluate(t, Default::default(), true, GradientSpace::OkLab, Default::default());
|
||||||
|
let deviation = max_gamma_channel_deviation(playback, true_color);
|
||||||
|
assert!(deviation <= 4. / 255., "playback deviates {:.1}/255 at t={t}", deviation * 255.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_even_positions_elide_against_their_own_distribution() {
|
||||||
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
|
gradient.set_positions(&[0., 1. / 3., 2. / 3.]);
|
||||||
|
assert!(gradient.nondefault_positions(true).is_none());
|
||||||
|
assert!(gradient.nondefault_positions(false).is_some());
|
||||||
|
|
||||||
|
gradient.elide_default_attributes(true);
|
||||||
|
assert!(!gradient.has_position_attribute(), "cyclic-even positions should elide under the cyclic default");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inserting_into_the_wrapped_interval_inherits_the_wrap_handle_and_samples_its_color() {
|
||||||
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
gradient.set_midpoints(&[0.5, 0.3]);
|
||||||
|
|
||||||
|
// The elided cyclic stops sit at 0 and 0.5, so 0.75 lands mid-wrap
|
||||||
|
let index = gradient.insert_stop(0.75, true, GradientSpace::RgbLinear, Default::default());
|
||||||
|
assert_eq!(index, 2);
|
||||||
|
assert_eq!(gradient.midpoint(2), 0.3, "the wrap handle should be inherited by the split");
|
||||||
|
assert_eq!(gradient.color(2), Some(Color::WHITE.lerp(&Color::BLACK, apply_midpoint(0.5, 0.3) as f32)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_reversal_materializes_the_even_distribution_and_flips_the_wrap_handle() {
|
||||||
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
|
gradient.set_midpoints(&[0.5, 0.5, 0.25]);
|
||||||
|
|
||||||
|
let reversed = gradient.reversed(true);
|
||||||
|
let positions = reversed.positions(true);
|
||||||
|
for (actual, expected) in positions.iter().zip([1. / 3., 2. / 3., 1.]) {
|
||||||
|
assert!((actual - expected).abs() < 1e-12, "reversed positions should be thirds ending at 1, got {positions:?}");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
reversed.midpoints(),
|
||||||
|
vec![0.5, 0.5, 0.75],
|
||||||
|
"the wrap handle should flip in place while interval midpoints shift and flip"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl FillChoice<SRGBA8> {
|
|||||||
let hex = srgba.to_rgba_hex();
|
let hex = srgba.to_rgba_hex();
|
||||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||||
}
|
}
|
||||||
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_space, ramp.gradient_hue_direction)),
|
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_cyclic, ramp.gradient_space, ramp.gradient_hue_direction)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1401,6 +1401,14 @@ fn gradient_space(_: impl Ctx, gradient: Item<Gradient>, space: Item<vector_type
|
|||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets whether each gradient in the input list treats its stops as a cycle, interpolating from the last stop back around to the first.
|
||||||
|
#[node_macro::node(category("Gradient"))]
|
||||||
|
fn gradient_cyclic(_: impl Ctx, gradient: Item<Gradient>, cyclic: Item<bool>) -> Item<Gradient> {
|
||||||
|
let mut gradient = gradient;
|
||||||
|
gradient.set_attribute(core_types::ATTR_GRADIENT_CYCLIC, *cyclic.element());
|
||||||
|
gradient
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets which way around the hue wheel each gradient in the input list interpolates, for polar color spaces.
|
/// Sets which way around the hue wheel each gradient in the input list interpolates, for polar color spaces.
|
||||||
#[node_macro::node(category("Gradient"))]
|
#[node_macro::node(category("Gradient"))]
|
||||||
fn gradient_hue_direction(_: impl Ctx, gradient: Item<Gradient>, hue_direction: Item<vector_types::GradientHueDirection>) -> Item<Gradient> {
|
fn gradient_hue_direction(_: impl Ctx, gradient: Item<Gradient>, hue_direction: Item<vector_types::GradientHueDirection>) -> Item<Gradient> {
|
||||||
@@ -1422,7 +1430,7 @@ fn gradient_positions(_: impl Ctx, gradient: Item<Gradient>, positions: List<f64
|
|||||||
|
|
||||||
/// Sets the interpolation midpoint for each interval between gradient stops, a factor from 0 to 1 where the 0.5 default means linear interpolation and another value skews the transition speed toward one stop or the other.
|
/// Sets the interpolation midpoint for each interval between gradient stops, a factor from 0 to 1 where the 0.5 default means linear interpolation and another value skews the transition speed toward one stop or the other.
|
||||||
///
|
///
|
||||||
/// The final stop belongs to no interval so its midpoint is ignored.
|
/// The final stop's midpoint controls the wrap back around to the first stop when the gradient is cyclic, and is otherwise ignored.
|
||||||
///
|
///
|
||||||
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5.
|
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5.
|
||||||
#[node_macro::node(category("Gradient"))]
|
#[node_macro::node(category("Gradient"))]
|
||||||
@@ -1438,8 +1446,11 @@ fn gradient_midpoints(_: impl Ctx, gradient: Item<Gradient>, midpoints: List<f64
|
|||||||
fn sample_gradient(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
|
fn sample_gradient(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
|
||||||
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
||||||
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
|
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
|
||||||
|
let gradient_cyclic = gradient.attribute_cloned_or_default::<bool>(core_types::ATTR_GRADIENT_CYCLIC);
|
||||||
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
|
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
|
||||||
let color = gradient.element().evaluate(*position.element(), gradient_spread, gradient_space, gradient_hue_direction);
|
let color = gradient
|
||||||
|
.element()
|
||||||
|
.evaluate(*position.element(), gradient_spread, gradient_cyclic, gradient_space, gradient_hue_direction);
|
||||||
Item::new_from_element(color)
|
Item::new_from_element(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List};
|
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List};
|
||||||
use core_types::{
|
use core_types::{
|
||||||
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_OPACITY,
|
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE,
|
||||||
ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx,
|
ATTR_GRADIENT_SPREAD, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx,
|
||||||
};
|
};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
||||||
@@ -293,6 +293,9 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
if let Some(gradient_space) = attributes.remove::<GradientSpace>(ATTR_GRADIENT_SPACE) {
|
if let Some(gradient_space) = attributes.remove::<GradientSpace>(ATTR_GRADIENT_SPACE) {
|
||||||
gradient_paint.set_attribute(ATTR_GRADIENT_SPACE, 0, gradient_space);
|
gradient_paint.set_attribute(ATTR_GRADIENT_SPACE, 0, gradient_space);
|
||||||
}
|
}
|
||||||
|
if let Some(gradient_cyclic) = attributes.remove::<bool>(ATTR_GRADIENT_CYCLIC) {
|
||||||
|
gradient_paint.set_attribute(ATTR_GRADIENT_CYCLIC, 0, gradient_cyclic);
|
||||||
|
}
|
||||||
if let Some(gradient_hue_direction) = attributes.remove::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION) {
|
if let Some(gradient_hue_direction) = attributes.remove::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION) {
|
||||||
gradient_paint.set_attribute(ATTR_GRADIENT_HUE_DIRECTION, 0, gradient_hue_direction);
|
gradient_paint.set_attribute(ATTR_GRADIENT_HUE_DIRECTION, 0, gradient_hue_direction);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,22 +42,21 @@ mod blend_std {
|
|||||||
}
|
}
|
||||||
impl Blend<Color> for Gradient {
|
impl Blend<Color> for Gradient {
|
||||||
// TODO: This joining is unfaithful in several ways: it samples only at stop positions so midpoint curves flatten away;
|
// TODO: This joining is unfaithful in several ways: it samples only at stop positions so midpoint curves flatten away;
|
||||||
// it evaluates both sources with the default spread and space rather than their own attributes (which this
|
// TODO: it evaluates both sources with default whole-ramp attributes rather than their own (which this element-level impl cannot read);
|
||||||
// element-level impl cannot read); and the output keeps over's attributes despite being sampled in the default space
|
// TODO: and the output keeps over's attributes despite being sampled with defaults
|
||||||
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
|
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
|
||||||
let mut combined_stops = self.positions().into_iter().chain(under.positions()).collect::<Vec<_>>();
|
let mut combined_stops = self.positions(false).into_iter().chain(under.positions(false)).collect::<Vec<_>>();
|
||||||
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||||
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||||
let stops = combined_stops.into_iter().map(|position| {
|
let stops = combined_stops.into_iter().map(|position| {
|
||||||
let over_color = self.evaluate(position, Default::default(), Default::default(), Default::default());
|
let over_color = self.evaluate(position, Default::default(), false, Default::default(), Default::default());
|
||||||
let under_color = under.evaluate(position, Default::default(), Default::default(), Default::default());
|
let under_color = under.evaluate(position, Default::default(), false, Default::default(), Default::default());
|
||||||
let color = blend_fn(over_color, under_color);
|
let color = blend_fn(over_color, under_color);
|
||||||
GradientStop { position, midpoint: 0.5, color }
|
GradientStop { position, midpoint: 0.5, color }
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut gradient = Gradient::new(stops);
|
// Positions stay explicit because eliding them needs the cyclic flag this impl can't read, and a wrong guess would relocate the stops
|
||||||
gradient.elide_default_attributes();
|
Gradient::new(stops)
|
||||||
gradient
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ async fn gradient_map<T: Adjust<Color> + Send>(
|
|||||||
let mut image = image;
|
let mut image = image;
|
||||||
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
||||||
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
|
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
|
||||||
|
let gradient_cyclic = gradient.attribute_cloned_or_default::<bool>(core_types::ATTR_GRADIENT_CYCLIC);
|
||||||
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
|
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
|
||||||
let gradient = gradient.into_element();
|
let gradient = gradient.into_element();
|
||||||
let reverse = reverse.into_element();
|
let reverse = reverse.into_element();
|
||||||
@@ -31,7 +32,7 @@ async fn gradient_map<T: Adjust<Color> + Send>(
|
|||||||
image.element_mut().adjust(|color| {
|
image.element_mut().adjust(|color| {
|
||||||
let intensity = color.luminance_rec_709();
|
let intensity = color.luminance_rec_709();
|
||||||
let intensity = if reverse { 1. - intensity } else { intensity };
|
let intensity = if reverse { 1. - intensity } else { intensity };
|
||||||
gradient.evaluate(intensity as f64, gradient_spread, gradient_space, gradient_hue_direction)
|
gradient.evaluate(intensity as f64, gradient_spread, gradient_cyclic, gradient_space, gradient_hue_direction)
|
||||||
});
|
});
|
||||||
|
|
||||||
image
|
image
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU};
|
|||||||
use core::hash::{Hash, Hasher};
|
use core::hash::{Hash, Hasher};
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::list::{ATTR_FILL, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
|
use core_types::list::{ATTR_FILL, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
|
||||||
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
@@ -142,9 +142,10 @@ where
|
|||||||
let mut content = content;
|
let mut content = content;
|
||||||
let length = content.vector_count();
|
let length = content.vector_count();
|
||||||
let gradient_space = gradient.attribute_cloned_or_default::<GradientSpace>(ATTR_GRADIENT_SPACE);
|
let gradient_space = gradient.attribute_cloned_or_default::<GradientSpace>(ATTR_GRADIENT_SPACE);
|
||||||
|
let gradient_cyclic = gradient.attribute_cloned_or_default::<bool>(ATTR_GRADIENT_CYCLIC);
|
||||||
let gradient_hue_direction = gradient.attribute_cloned_or_default::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION);
|
let gradient_hue_direction = gradient.attribute_cloned_or_default::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION);
|
||||||
let element = gradient.into_element();
|
let element = gradient.into_element();
|
||||||
let gradient = if reverse { element.reversed() } else { element };
|
let gradient = if reverse { element.reversed(gradient_cyclic) } else { element };
|
||||||
|
|
||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The factor spans 0..=1 inclusively, so the spread deliberately stays Pad (Repeat would wrap the final element onto the first stop's color)
|
// The factor spans 0..=1 inclusively, so the spread deliberately stays Pad (Repeat would wrap the final element onto the first stop's color)
|
||||||
let color = gradient.evaluate(factor, Default::default(), gradient_space, gradient_hue_direction);
|
let color = gradient.evaluate(factor, Default::default(), gradient_cyclic, gradient_space, gradient_hue_direction);
|
||||||
let paint = List::new_from_element(color).into_graphic_list();
|
let paint = List::new_from_element(color).into_graphic_list();
|
||||||
|
|
||||||
if fill {
|
if fill {
|
||||||
|
|||||||
Reference in New Issue
Block a user