Add a gradient interpolation attribute with Stepped, Linear (existing), and Smooth modes (#4418)

* Delete dead code function

* Add a Gradient Interpolation axis with Stepped, Linear, and Smooth paths through the stops

* Keep the gradient interpolation attached when dragging stops in the picker

* Give the gradient popover's dropdowns the same tooltips as their row labels

* Order the gradient popover with Intrp. above Space

* Hold gradient stops in place when the cyclic flag is toggled

* Fix wrong Smooth gradient colors around stops sitting on the ramp boundaries

* Consolidate gradient settings plumbing and fix bugs

* Bundle whole-ramp gradient settings into GradientSettings across the editor plumbing

* Name the Smooth gradient interpolation's spline type in its tooltip

* Linearize the gamma hex stop colors recovered from imported Graphite SVGs

* Code review

* Aim the Smooth bake's seam subdivision at the copied boundary color

* Add GradientEvaluator to build gradient sampling state once per loop instead of per sample

* Correct the gradient evaluator's documented setup cost to O(n log n)
This commit is contained in:
Keavon Chambers
2026-08-07 16:43:36 -07:00
committed by GitHub
parent 4b01abe36d
commit 681b4033f5
27 changed files with 1560 additions and 734 deletions
@@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate}; use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientSpace, GradientSpread}; use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread};
/// Identifies which RGB channel a numeric input change targets. /// Identifies which RGB channel a numeric input change targets.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -55,6 +55,8 @@ pub enum ColorPickerMessage {
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.
SetGradientHueDirection { gradient_hue_direction: GradientHueDirection }, SetGradientHueDirection { gradient_hue_direction: GradientHueDirection },
/// Gradient interpolation choice: the path the stops interpolate along, from the "Intrp." dropdown.
SetGradientInterpolation { gradient_interpolation: GradientInterpolation },
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent). /// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
StartTransaction, StartTransaction,
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::core_types::misc::parse_css_color; use graphene_std::core_types::misc::parse_css_color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops}; use graphene_std::vector::style::{FillChoice, Gradient, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops};
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops). /// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
const MIN_MIDPOINT: f64 = 0.01; const MIN_MIDPOINT: f64 = 0.01;
@@ -33,6 +33,7 @@ pub struct ColorPickerMessageHandler {
gradient_space: GradientSpace, gradient_space: GradientSpace,
gradient_cyclic: bool, gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection, gradient_hue_direction: GradientHueDirection,
gradient_interpolation: GradientInterpolation,
active_marker_index: Option<u32>, active_marker_index: Option<u32>,
active_marker_is_midpoint: bool, active_marker_is_midpoint: bool,
@@ -58,6 +59,7 @@ impl Default for ColorPickerMessageHandler {
gradient_space: GradientSpace::default(), gradient_space: GradientSpace::default(),
gradient_cyclic: false, gradient_cyclic: false,
gradient_hue_direction: GradientHueDirection::default(), gradient_hue_direction: GradientHueDirection::default(),
gradient_interpolation: GradientInterpolation::default(),
active_marker_index: None, active_marker_index: None,
active_marker_is_midpoint: false, active_marker_is_midpoint: false,
allow_none: true, allow_none: true,
@@ -82,6 +84,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.gradient_space = GradientSpace::default(); self.gradient_space = GradientSpace::default();
self.gradient_cyclic = false; self.gradient_cyclic = false;
self.gradient_hue_direction = GradientHueDirection::default(); self.gradient_hue_direction = GradientHueDirection::default();
self.gradient_interpolation = GradientInterpolation::default();
self.active_marker_index = None; self.active_marker_index = None;
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
} }
@@ -91,6 +94,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.gradient_space = GradientSpace::default(); self.gradient_space = GradientSpace::default();
self.gradient_cyclic = false; self.gradient_cyclic = false;
self.gradient_hue_direction = GradientHueDirection::default(); self.gradient_hue_direction = GradientHueDirection::default();
self.gradient_interpolation = GradientInterpolation::default();
self.active_marker_index = None; self.active_marker_index = None;
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
self.adopt_color(color); self.adopt_color(color);
@@ -102,6 +106,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.gradient_space = ramp.gradient_space; self.gradient_space = ramp.gradient_space;
self.gradient_cyclic = ramp.gradient_cyclic; self.gradient_cyclic = ramp.gradient_cyclic;
self.gradient_hue_direction = ramp.gradient_hue_direction; self.gradient_hue_direction = ramp.gradient_hue_direction;
self.gradient_interpolation = ramp.gradient_interpolation;
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);
self.gradient = Some(gradient); self.gradient = Some(gradient);
@@ -212,28 +217,20 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction); responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_spread = gradient_spread; self.gradient_spread = gradient_spread;
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
}); });
self.send_layouts(responses); self.send_layouts(responses);
} }
ColorPickerMessage::SetGradientCyclic { gradient_cyclic } => { ColorPickerMessage::SetGradientCyclic { gradient_cyclic } => {
let Some(gradient) = &self.gradient else { return }; let Some(gradient) = &mut self.gradient else { return };
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction); responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_cyclic = gradient_cyclic;
let previous_cyclic = std::mem::replace(&mut self.gradient_cyclic, gradient_cyclic);
gradient.hold_positions_across_cyclic_change(previous_cyclic, gradient_cyclic);
let ramp = GradientRamp::from(&*gradient);
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
gradient_spread: self.gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
}); });
self.send_layouts(responses); self.send_layouts(responses);
} }
@@ -242,13 +239,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction); responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_space = gradient_space; self.gradient_space = gradient_space;
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
gradient_spread: self.gradient_spread,
gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
}); });
self.send_layouts(responses); self.send_layouts(responses);
} }
@@ -257,13 +248,16 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction); responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_hue_direction = gradient_hue_direction; self.gradient_hue_direction = gradient_hue_direction;
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
gradient_spread: self.gradient_spread, });
gradient_space: self.gradient_space, self.send_layouts(responses);
gradient_cyclic: self.gradient_cyclic, }
gradient_hue_direction, ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
..GradientRamp::from(gradient) let Some(gradient) = &self.gradient else { return };
}), responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_interpolation = gradient_interpolation;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
}); });
self.send_layouts(responses); self.send_layouts(responses);
} }
@@ -315,6 +309,17 @@ impl ColorPickerMessageHandler {
self.old_is_none = is_none; self.old_is_none = is_none;
} }
/// The whole-ramp settings the picker is currently editing with, bundled for sampling and for emitting ramps.
fn gradient_settings(&self) -> GradientSettings {
GradientSettings {
spread: self.gradient_spread,
cyclic: self.gradient_cyclic,
space: self.gradient_space,
hue_direction: self.gradient_hue_direction,
interpolation: self.gradient_interpolation,
}
}
fn snapshot_old(&mut self) { fn snapshot_old(&mut self) {
self.old_hue = self.hue; self.old_hue = self.hue;
self.old_saturation = self.saturation; self.old_saturation = self.saturation;
@@ -350,14 +355,9 @@ impl ColorPickerMessageHandler {
&& (active_index as usize) < gradient.len() && (active_index as usize) < gradient.len()
{ {
gradient.set_color(active_index as usize, color); gradient.set_color(active_index as usize, color);
let ramp = GradientRamp::from(&*gradient);
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
gradient_spread: self.gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(&*gradient)
}),
}); });
} else { } else {
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
@@ -414,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_cyclic, self.gradient_space, self.gradient_hue_direction); let new_index = gradient.insert_stop(position, self.gradient_settings());
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) {
@@ -487,13 +487,7 @@ impl ColorPickerMessageHandler {
} }
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp::from(&gradient).with_settings(self.gradient_settings())),
gradient_spread: self.gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(&gradient)
}),
}); });
self.gradient = Some(gradient); self.gradient = Some(gradient);
self.send_layouts(responses); self.send_layouts(responses);
@@ -527,6 +521,7 @@ impl ColorPickerMessageHandler {
.track_space(self.gradient_space) .track_space(self.gradient_space)
.track_cyclic(self.gradient_cyclic) .track_cyclic(self.gradient_cyclic)
.track_hue_direction(self.gradient_hue_direction) .track_hue_direction(self.gradient_hue_direction)
.track_interpolation(self.gradient_interpolation)
.markers(markers) .markers(markers)
.active_marker_index(self.active_marker_index) .active_marker_index(self.active_marker_index)
.active_marker_is_midpoint(self.active_marker_is_midpoint) .active_marker_is_midpoint(self.active_marker_is_midpoint)
@@ -726,6 +721,25 @@ impl ColorPickerMessageHandler {
])); ]));
} }
// Gradient interpolation (only present when the picker is in gradient mode)
if self.gradient.is_some() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into());
groups.push(LayoutGroup::row(vec![
TextLabel::new("Intrp.")
.tooltip_label("Gradient Interpolation")
.tooltip_description(INTERPOLATION_DESCRIPTION)
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries)
.selected_index(Some(self.gradient_interpolation as u32))
.disabled(self.disabled)
.tooltip_label("Gradient Interpolation")
.tooltip_description(INTERPOLATION_DESCRIPTION)
.widget_instance(),
]));
}
// Gradient color space (only present when the picker is in gradient mode) // Gradient color space (only present when the picker is in gradient mode)
if self.gradient.is_some() { if self.gradient.is_some() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_space| ColorPickerMessage::SetGradientSpace { gradient_space }.into()); let entries = MenuListEntry::sections_from_choice_type(|gradient_space| ColorPickerMessage::SetGradientSpace { gradient_space }.into());
@@ -733,7 +747,12 @@ impl ColorPickerMessageHandler {
groups.push(LayoutGroup::row(vec![ groups.push(LayoutGroup::row(vec![
TextLabel::new("Space").tooltip_label("Gradient Space").tooltip_description(SPACE_DESCRIPTION).widget_instance(), TextLabel::new("Space").tooltip_label("Gradient Space").tooltip_description(SPACE_DESCRIPTION).widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries).selected_index(Some(self.gradient_space as u32)).disabled(self.disabled).widget_instance(), DropdownInput::new(entries)
.selected_index(Some(self.gradient_space as u32))
.disabled(self.disabled)
.tooltip_label("Gradient Space")
.tooltip_description(SPACE_DESCRIPTION)
.widget_instance(),
])); ]));
} }
@@ -750,6 +769,8 @@ impl ColorPickerMessageHandler {
DropdownInput::new(entries) DropdownInput::new(entries)
.selected_index(Some(self.gradient_hue_direction as u32)) .selected_index(Some(self.gradient_hue_direction as u32))
.disabled(self.disabled) .disabled(self.disabled)
.tooltip_label("Gradient Hue Direction")
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
.widget_instance(), .widget_instance(),
])); ]));
} }
@@ -811,6 +832,7 @@ const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond
const CYCLIC_DESCRIPTION: &str = "Treats the stops as a cycle, interpolating from the last stop back around to the 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.";
const INTERPOLATION_DESCRIPTION: &str = "The path the stops interpolate along, deciding whether the gradient jumps, turns corners, or flows smoothly through them.";
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`). /// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background. /// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
@@ -531,13 +531,19 @@ 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 // The track strip spans exactly 0 to 1, which no spread affects, so the widget carries no spread of its own
.track let settings = graphene_std::vector::style::GradientSettings {
.to_css_linear_gradient(spectrum_input.track_cyclic, spectrum_input.track_space, spectrum_input.track_hue_direction); spread: Default::default(),
cyclic: spectrum_input.track_cyclic,
space: spectrum_input.track_space,
hue_direction: spectrum_input.track_hue_direction,
interpolation: spectrum_input.track_interpolation,
};
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(settings);
// 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' // 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 track_evaluator = graphene_std::vector::style::Gradient::from(&spectrum_input.track).evaluator(settings);
let cap = |t: f64| { 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); let color = track_evaluator.evaluate(t);
SRGBA8::from(color).to_css_hex() SRGBA8::from(color).to_css_hex()
}; };
spectrum_input.track_start_css = cap(0.); spectrum_input.track_start_css = cap(0.);
@@ -7,7 +7,7 @@ use derivative::*;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::transform::ReferencePoint; use graphene_std::transform::ReferencePoint;
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientSpace, GradientStops}; use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientInterpolation, GradientSpace, GradientStops};
use graphite_proc_macros::WidgetBuilder; use graphite_proc_macros::WidgetBuilder;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -595,6 +595,9 @@ pub struct SpectrumInput {
/// 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,
/// The path the track's stops interpolate along, used to compute `track_css` and by the frontend to suppress the midpoint diamonds when stepped.
#[serde(rename = "trackInterpolation")]
pub track_interpolation: GradientInterpolation,
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time. /// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
#[serde(rename = "trackCSS")] #[serde(rename = "trackCSS")]
#[widget_builder(skip)] #[widget_builder(skip)]
@@ -24,7 +24,9 @@ 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, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, 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;
@@ -216,6 +218,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
List<GradientSpread>, List<GradientSpread>,
List<GradientSpace>, List<GradientSpace>,
List<GradientHueDirection>, List<GradientHueDirection>,
List<GradientInterpolation>,
List<DashPattern>, List<DashPattern>,
List<BoxCorners>, List<BoxCorners>,
List<StrokeJoin>, List<StrokeJoin>,
@@ -271,6 +274,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
Item<GradientSpread>, Item<GradientSpread>,
Item<GradientSpace>, Item<GradientSpace>,
Item<GradientHueDirection>, Item<GradientHueDirection>,
Item<GradientInterpolation>,
Item<DashPattern>, Item<DashPattern>,
Item<BoxCorners>, Item<BoxCorners>,
Item<StrokeJoin>, Item<StrokeJoin>,
@@ -1009,6 +1013,7 @@ impl_table_item_layout_for_choice_enum!(
GradientSpread, GradientSpread,
GradientSpace, GradientSpace,
GradientHueDirection, GradientHueDirection,
GradientInterpolation,
StrokeJoin, StrokeJoin,
StrokeAlign, StrokeAlign,
StrokeCap, StrokeCap,
@@ -1223,6 +1228,7 @@ macro_rules! known_item_types {
GradientSpread, GradientSpread,
GradientSpace, GradientSpace,
GradientHueDirection, GradientHueDirection,
GradientInterpolation,
StrokeJoin, StrokeJoin,
StrokeAlign, StrokeAlign,
StrokeCap, StrokeCap,
@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread, Stroke}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, PointId, VectorModificationType}; use graphene_std::vector::{Gradient, PointId, VectorModificationType};
#[impl_message(Message, DocumentMessage, GraphOperation)] #[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -29,10 +29,7 @@ pub enum GraphOperationMessage {
#[serde(skip)] #[serde(skip)]
gradient: Gradient, gradient: Gradient,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, gradient_settings: GradientSettings,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2, transform: DAffine2,
}, },
BlendingFillSet { BlendingFillSet {
@@ -76,6 +73,10 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
gradient_hue_direction: GradientHueDirection, gradient_hue_direction: GradientHueDirection,
}, },
GradientInterpolationSet {
layer: LayerNodeIdentifier,
gradient_interpolation: GradientInterpolation,
},
OpacitySet { OpacitySet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
opacity: f64, opacity: f64,
@@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput};
use graph_craft::list; use graph_craft::list;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Gradient, GradientForm, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color}; use graphene_std::{Artboard, Color};
#[derive(ExtractField)] #[derive(ExtractField)]
@@ -48,14 +48,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
layer, layer,
gradient, gradient,
gradient_form, gradient_form,
gradient_spread, gradient_settings,
gradient_space,
gradient_cyclic,
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_cyclic, gradient_hue_direction, transform); modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_settings, transform);
} }
} }
GraphOperationMessage::BlendingFillSet { layer, fill } => { GraphOperationMessage::BlendingFillSet { layer, fill } => {
@@ -108,6 +105,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.gradient_hue_direction_set(gradient_hue_direction); modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
} }
} }
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_interpolation_set(gradient_interpolation);
}
}
GraphOperationMessage::OpacitySet { layer, opacity } => { GraphOperationMessage::OpacitySet { layer, opacity } => {
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.opacity_set(opacity); modify_inputs.opacity_set(opacity);
@@ -727,7 +729,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.; let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.; let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.; let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity)) Some(Color::from_gamma_srgb_channels(r, g, b, opacity))
} }
/// Import a usvg node as the root of an SVG import operation. /// Import a usvg node as the root of an SVG import operation.
@@ -980,10 +982,13 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
Gradient::new(stops) Gradient::new(stops)
} }
}; };
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 settings = GradientSettings {
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, false, Default::default(), transform); spread: convert_gradient_spread(linear.spread_method()),
space: gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma),
..Default::default()
};
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
} }
usvg::Paint::RadialGradient(radial) => { usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform()); let gradient_transform = usvg_transform(radial.transform());
@@ -1006,10 +1011,12 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
Gradient::new(stops) Gradient::new(stops)
} }
}; };
let gradient_spread = convert_gradient_spread(radial.spread_method()); let settings = GradientSettings {
let gradient_space = gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma); spread: convert_gradient_spread(radial.spread_method()),
space: gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma),
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, false, Default::default(), transform); ..Default::default()
};
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
} }
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"), usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
}; };
@@ -1041,6 +1048,31 @@ mod tests {
); );
} }
#[test]
fn graphite_stop_extraction_keeps_real_stops_and_linearizes_their_colors() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art">
<defs>
<linearGradient id="ramp">
<stop stop-color="#000000" graphite:midpoint="0.3" />
<stop offset="0.25" stop-color="#404040" />
<stop offset="0.5" stop-color="#808080" stop-opacity="0.5" graphite:midpoint="0.5" />
<stop offset="1" stop-color="#ffffff" graphite:midpoint="0.5" />
</linearGradient>
</defs>
</svg>"##;
let stops = extract_graphite_gradient_stops(svg);
let gradient = stops.get("ramp").expect("the tagged gradient should be recovered");
// The untagged stop is baked approximation residue, not authored data
assert_eq!(gradient.len(), 3, "only stops tagged with a midpoint should survive");
assert_eq!(gradient.positions(false), vec![0., 0.5, 1.]);
assert_eq!(gradient.midpoints(), vec![0.3, 0.5, 0.5]);
// Hex stop bytes are gamma-encoded, so the recovered color must lift them to linear light
assert_eq!(gradient.color(1), Some(Color::from_gamma_srgb_channels(128. / 255., 128. / 255., 128. / 255., 0.5)));
}
#[test] #[test]
fn color_interpolation_reads_style_blocks_with_selector_specificity() { fn color_interpolation_reads_style_blocks_with_selector_specificity() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"> let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
@@ -18,7 +18,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread, Stroke}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic}; use graphene_std::{Artboard, Color, Graphic};
@@ -432,30 +432,13 @@ 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(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
pub fn fill_gradient_set(
&mut self,
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2,
) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return; return;
}; };
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
let ramp = GradientRamp::from(gradient); let ramp = GradientRamp::from(gradient).with_settings(settings);
let ramp = GradientRamp {
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..ramp
};
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true); self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
// Skip the rerender on all but the last input so the whole update triggers a single graph run // Skip the rerender on all but the last input so the whole update triggers a single graph run
@@ -794,7 +777,9 @@ 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. /// Set the cyclic wrap flag on the chain's gradient value, which is where the ramp carries it. This holds the existing
/// stops in place by reading their positions under the old flag, so batch it before any stops write rather than after one,
/// or it would reinterpret incoming stops already authored under the new flag.
pub fn gradient_cyclic_set(&mut self, gradient_cyclic: bool) { pub fn gradient_cyclic_set(&mut self, gradient_cyclic: bool) {
let Some(output_layer) = self.get_output_layer() else { return }; 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 { let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
@@ -802,7 +787,20 @@ impl<'a> ModifyInputsContext<'a> {
}; };
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return }; let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
let ramp = GradientRamp { gradient_cyclic, ..ramp }; let ramp = ramp.with_cyclic(gradient_cyclic);
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 interpolation on the chain's gradient value, which is where the ramp carries it.
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) {
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_interpolation, ..ramp };
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput); 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); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
} }
@@ -33,8 +33,8 @@ use graphene_std::vector::misc::{
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
}; };
use graphene_std::vector::style::{ use graphene_std::vector::style::{
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap,
build_transform_with_y_preservation, StrokeJoin, build_transform_with_y_preservation,
}; };
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::{NodeParameter, ParameterRef}; use graphene_std::{NodeParameter, ParameterRef};
@@ -306,6 +306,7 @@ pub(crate) fn property_from_type(
Some(x) if id_is::<GradientSpread>(x) => enum_choice::<GradientSpread>().for_socket(default_info).property_row(), Some(x) if id_is::<GradientSpread>(x) => enum_choice::<GradientSpread>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientSpace>(x) => enum_choice::<GradientSpace>().for_socket(default_info).property_row(), Some(x) if id_is::<GradientSpace>(x) => enum_choice::<GradientSpace>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientHueDirection>(x) => enum_choice::<GradientHueDirection>().for_socket(default_info).property_row(), Some(x) if id_is::<GradientHueDirection>(x) => enum_choice::<GradientHueDirection>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientInterpolation>(x) => enum_choice::<GradientInterpolation>().for_socket(default_info).property_row(),
Some(x) if id_is::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(), Some(x) if id_is::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(), Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(), Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
@@ -2406,10 +2407,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Gradient { Gradient {
gradient: Gradient, gradient: Gradient,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, settings: GradientSettings,
gradient_space: GradientSpace,
gradient_cyclic: bool,
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.
transform_is_value: bool, transform_is_value: bool,
@@ -2439,10 +2437,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Some(gradient) => ResolvedFill::Gradient { Some(gradient) => ResolvedFill::Gradient {
gradient: gradient.stops, gradient: gradient.stops,
gradient_form: gradient.gradient_form, gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread, settings: gradient.settings,
gradient_space: gradient.gradient_space,
gradient_cyclic: gradient.gradient_cyclic,
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,
}, },
@@ -2470,33 +2465,15 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}; };
match &fill { match &fill {
ResolvedFill::Gradient { ResolvedFill::Gradient { gradient: stops, settings, .. } => {
gradient: stops,
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..
} => {
let stops = stops.clone(); let stops = stops.clone();
let gradient_spread = *gradient_spread; let settings = *settings;
let gradient_space = *gradient_space;
let gradient_cyclic = *gradient_cyclic;
let gradient_hue_direction = *gradient_hue_direction;
let reverse_button = IconButton::new("Reverse", 24) let reverse_button = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops") .tooltip_label("Reverse Stops")
.tooltip_description("Reverse the gradient color stops.") .tooltip_description("Reverse the gradient color stops.")
.on_update(update_value( .on_update(update_value(
move |_| { move |_| TaggedValue::GradientRamp(GradientRamp::from(stops.reversed(settings.cyclic)).with_settings(settings)),
TaggedValue::GradientRamp(GradientRamp {
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..GradientRamp::from(stops.reversed(gradient_cyclic))
})
},
node_id, node_id,
FillInput, FillInput,
)) ))
@@ -2515,20 +2492,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
FillChoice::<SRGBA8>::None FillChoice::<SRGBA8>::None
} }
} }
ResolvedFill::Gradient { ResolvedFill::Gradient { gradient: stops, settings, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp::from(stops).with_settings(*settings)),
gradient: stops,
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..
} => FillChoice::<SRGBA8>::Gradient(GradientRamp {
gradient_spread: *gradient_spread,
gradient_space: *gradient_space,
gradient_cyclic: *gradient_cyclic,
gradient_hue_direction: *gradient_hue_direction,
..GradientRamp::from(stops)
}),
ResolvedFill::Other => FillChoice::<SRGBA8>::None, ResolvedFill::Other => FillChoice::<SRGBA8>::None,
}; };
@@ -175,10 +175,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::graphic::read_attribute_gradient_spread::IDENTIFIER, node: graphene_std::graphic::read_attribute_gradient_spread::IDENTIFIER,
aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode"], aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode"],
}, },
NodeReplacement {
node: graphene_std::graphic::read_attribute_gradient_space::IDENTIFIER,
aliases: &["graphic_nodes::graphic::ReadAttributeGradientInterpolationNode"],
},
NodeReplacement { NodeReplacement {
node: graphene_std::graphic::remove_at_index::IDENTIFIER, node: graphene_std::graphic::remove_at_index::IDENTIFIER,
aliases: &["graphic_nodes::graphic::OmitElementNode"], aliases: &["graphic_nodes::graphic::OmitElementNode"],
@@ -278,10 +274,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::math_nodes::gradient_form::IDENTIFIER, node: graphene_std::math_nodes::gradient_form::IDENTIFIER,
aliases: &["math_nodes::GradientTypeNode"], aliases: &["math_nodes::GradientTypeNode"],
}, },
NodeReplacement {
node: graphene_std::math_nodes::gradient_space::IDENTIFIER,
aliases: &["math_nodes::GradientInterpolationNode"],
},
NodeReplacement { NodeReplacement {
node: graphene_std::math_nodes::gradient_spread::IDENTIFIER, node: graphene_std::math_nodes::gradient_spread::IDENTIFIER,
aliases: &["math_nodes::SpreadMethodNode"], aliases: &["math_nodes::SpreadMethodNode"],
@@ -14,7 +14,7 @@ use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
use graphene_std::vector::{Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, PointId, SegmentId, VectorModificationType}; use graphene_std::vector::{Gradient, GradientForm, GradientRamp, GradientSettings, PointId, SegmentId, VectorModificationType};
use graphene_std::{NodeParameter, ParameterRef}; use graphene_std::{NodeParameter, ParameterRef};
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -389,7 +389,7 @@ pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &No
} }
/// The ramp held by the 'Gradient Value' node feeding a layer's chain, which carries the whole-ramp settings. /// The ramp held by the 'Gradient Value' node feeding a layer's chain, which carries the whole-ramp settings.
fn get_chain_source_gradient_ramp<'a>(layer: LayerNodeIdentifier, network_interface: &'a NodeNetworkInterface) -> Option<&'a GradientRamp> { fn get_chain_source_gradient_ramp(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<&GradientRamp> {
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else { let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
return None; return None;
@@ -397,24 +397,9 @@ fn get_chain_source_gradient_ramp<'a>(layer: LayerNodeIdentifier, network_interf
Some(ramp) Some(ramp)
} }
/// The spread baked into the 'Gradient Value' node feeding a layer's chain. /// The whole-ramp settings baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpread> { pub fn get_chain_source_gradient_settings(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSettings> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread) Some(get_chain_source_gradient_ramp(layer, network_interface)?.into())
}
/// The space baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_space(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpace> {
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.
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)
} }
/// Get the gradient stops of a layer, if any. /// Get the gradient stops of a layer, if any.
@@ -771,10 +756,7 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
pub struct FillNodeGradient { pub struct FillNodeGradient {
pub stops: Gradient, pub stops: Gradient,
pub gradient_form: GradientForm, pub gradient_form: GradientForm,
pub gradient_spread: GradientSpread, pub settings: GradientSettings,
pub gradient_space: GradientSpace,
pub gradient_cyclic: bool,
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.
pub transform_is_value: bool, pub transform_is_value: bool,
@@ -787,10 +769,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::FillInput)?.as_value()? else { let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::FillInput)?.as_value()? else {
return None; return None;
}; };
let gradient_spread = ramp.gradient_spread; let settings = GradientSettings::from(ramp);
let gradient_space = ramp.gradient_space;
let gradient_cyclic = ramp.gradient_cyclic;
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()) {
Some(&TaggedValue::GradientForm(value)) => value, Some(&TaggedValue::GradientForm(value)) => value,
@@ -807,10 +786,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
Some(FillNodeGradient { Some(FillNodeGradient {
stops, stops,
gradient_form, gradient_form,
gradient_spread, settings,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
transform, transform,
transform_is_value: transform_input.is_some(), transform_is_value: transform_input.is_some(),
}) })
@@ -959,10 +935,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
layer, layer,
gradient: Gradient::from(ramp), gradient: Gradient::from(ramp),
gradient_form, gradient_form,
gradient_spread: ramp.gradient_spread, gradient_settings: ramp.into(),
gradient_space: ramp.gradient_space,
gradient_cyclic: ramp.gradient_cyclic,
gradient_hue_direction: ramp.gradient_hue_direction,
transform, transform,
}); });
} }
@@ -9,15 +9,15 @@ 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_cyclic, get_chain_source_gradient_hue_direction, get_chain_source_gradient_space, get_chain_source_gradient_spread, self, NodeGraphLayer, get_chain_source_gradient_settings, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input,
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, 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;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStop, build_transform_with_y_preservation}; use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSettings, GradientStop, build_transform_with_y_preservation};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct GradientTool { pub struct GradientTool {
@@ -29,10 +29,7 @@ pub struct GradientTool {
#[derive(Default)] #[derive(Default)]
pub struct GradientOptions { pub struct GradientOptions {
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, settings: GradientSettings,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
} }
#[impl_message(Message, ToolMessage, Gradient)] #[impl_message(Message, ToolMessage, Gradient)]
@@ -104,7 +101,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
context, context,
responses, responses,
|_| true, |_| true,
|(gradient, appearance)| *gradient = gradient.reversed(appearance.gradient_cyclic), |(gradient, appearance)| *gradient = gradient.reversed(appearance.settings.cyclic),
); );
} }
GradientOptionsUpdate::ReverseDirection => apply_gradient_update( GradientOptionsUpdate::ReverseDirection => apply_gradient_update(
@@ -146,20 +143,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
} }
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => { ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
let ramp = GradientRamp::from(&ramp); let ramp = GradientRamp::from(&ramp);
self.options.gradient_spread = ramp.gradient_spread; self.options.settings = GradientSettings::from(&ramp);
self.options.gradient_space = ramp.gradient_space; apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), self.options.settings);
self.options.gradient_cyclic = ramp.gradient_cyclic;
self.options.gradient_hue_direction = ramp.gradient_hue_direction;
apply_stops_update(
&mut self.data,
context,
responses,
Gradient::from(&ramp),
ramp.gradient_spread,
ramp.gradient_space,
ramp.gradient_cyclic,
ramp.gradient_hue_direction,
);
} }
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => { ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
if self.data.color_picker_transaction_open { if self.data.color_picker_transaction_open {
@@ -193,20 +178,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
self.options.gradient_form = appearance.gradient_form; self.options.gradient_form = appearance.gradient_form;
needs_refresh = true; needs_refresh = true;
} }
if self.options.gradient_spread != appearance.gradient_spread { if self.options.settings != appearance.settings {
self.options.gradient_spread = appearance.gradient_spread; self.options.settings = appearance.settings;
needs_refresh = true;
}
if self.options.gradient_space != appearance.gradient_space {
self.options.gradient_space = appearance.gradient_space;
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 {
self.options.gradient_hue_direction = appearance.gradient_hue_direction;
needs_refresh = true; needs_refresh = true;
} }
} }
@@ -287,23 +260,17 @@ impl LayoutHolder for GradientTool {
}, },
]) ])
}); });
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp { let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp::from(&stops_value).with_settings(self.options.settings)))
gradient_spread: self.options.gradient_spread, .allow_none(false)
gradient_space: self.options.gradient_space, .narrow(true)
gradient_cyclic: self.options.gradient_cyclic, .tooltip_label("Gradient Stops")
gradient_hue_direction: self.options.gradient_hue_direction, .tooltip_description("Edit the gradient's color stops.")
..GradientRamp::from(&stops_value) .on_update(|input: &ColorInput| {
})) let ramp = input.value.as_gradient().cloned().unwrap_or_default();
.allow_none(false) GradientToolMessage::UpdateRamp { ramp }.into()
.narrow(true) })
.tooltip_label("Gradient Stops") .on_commit(|_| DocumentMessage::AddTransaction.into())
.tooltip_description("Edit the gradient's color stops.") .widget_instance();
.on_update(|input: &ColorInput| {
let ramp = input.value.as_gradient().cloned().unwrap_or_default();
GradientToolMessage::UpdateRamp { ramp }.into()
})
.on_commit(|_| DocumentMessage::AddTransaction.into())
.widget_instance();
let reverse_stops = IconButton::new("Reverse", 24) let reverse_stops = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops") .tooltip_label("Reverse Stops")
@@ -391,10 +358,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
gradient.stops, gradient.stops,
GradientAppearance { GradientAppearance {
gradient_form: gradient.gradient_form, gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread, settings: gradient.settings,
gradient_space: gradient.gradient_space,
gradient_cyclic: gradient.gradient_cyclic,
gradient_hue_direction: gradient.gradient_hue_direction,
transform: gradient.transform, transform: gradient.transform,
}, },
GradientSource::Direct, GradientSource::Direct,
@@ -413,10 +377,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
struct GradientAppearance { struct GradientAppearance {
transform: DAffine2, transform: DAffine2,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, settings: GradientSettings,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
} }
/// Resolve the gradient transform, form, and spread by walking the chain feeding the layer. /// Resolve the gradient transform, form, and spread by walking the chain feeding the layer.
@@ -456,10 +417,7 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod
GradientAppearance { GradientAppearance {
transform: composed_transform, transform: composed_transform,
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(), settings: get_chain_source_gradient_settings(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(),
} }
} }
@@ -504,7 +462,13 @@ fn wrapped_interval_span(gradient: &Gradient) -> (f64, f64) {
/// The gradient's visible midpoint diamonds as `(owning stop index, position along the gradient line)`, omitting intervals /// 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. /// 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)> { fn midpoint_diamonds(gradient: &Gradient, settings: GradientSettings, viewport_line_length: f64) -> Vec<(usize, f64)> {
// A stepped ramp jumps at its stops, so no midpoint has anything to bias
if settings.interpolation == GradientInterpolation::Stepped {
return Vec::new();
}
let gradient_cyclic = settings.cyclic;
let mut diamonds = Vec::with_capacity(gradient.len()); let mut diamonds = Vec::with_capacity(gradient.len());
for index in 0..gradient.len().saturating_sub(1) { for index in 0..gradient.len().saturating_sub(1) {
@@ -585,7 +549,8 @@ struct SelectedGradient {
is_gradient_chain: bool, is_gradient_chain: bool,
} }
fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, gradient_cyclic: bool, mouse: DVec2) -> Option<f64> { fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, settings: GradientSettings, mouse: DVec2) -> Option<f64> {
let gradient_cyclic = settings.cyclic;
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);
@@ -602,7 +567,7 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, gradient_cycl
// 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 (_, midpoint_position) in midpoint_diamonds(stops, gradient_cyclic, line_length) { for (_, midpoint_position) in midpoint_diamonds(stops, settings, line_length) {
let midpoint_viewport = start.lerp(end, midpoint_position); let midpoint_viewport = start.lerp(end, midpoint_position);
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;
@@ -770,7 +735,7 @@ 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 gradient_cyclic = self.appearance.gradient_cyclic; let gradient_cyclic = self.appearance.settings.cyclic;
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_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 has_other_stop_at_one = stop != last_index && !self.gradient.is_empty() && (1. - self.gradient.position(last_index, gradient_cyclic)).abs() < f64::EPSILON * 1000.;
@@ -830,7 +795,7 @@ impl SelectedGradient {
} }
// Convert to a midpoint ratio within the interval owned by the dragged diamond's stop // Convert to a midpoint ratio within the interval owned by the dragged diamond's stop
if let Some(midpoint_ratio) = midpoint_ratio_at(&self.gradient, midpoint_index, self.appearance.gradient_cyclic, full_pos) { if let Some(midpoint_ratio) = midpoint_ratio_at(&self.gradient, midpoint_index, self.appearance.settings.cyclic, full_pos) {
self.gradient.set_midpoint(midpoint_index, midpoint_ratio); self.gradient.set_midpoint(midpoint_index, midpoint_ratio);
} }
} }
@@ -848,10 +813,7 @@ impl SelectedGradient {
layer, layer,
gradient: self.gradient.clone(), gradient: self.gradient.clone(),
gradient_form: self.appearance.gradient_form, gradient_form: self.appearance.gradient_form,
gradient_spread: self.appearance.gradient_spread, gradient_settings: self.appearance.settings,
gradient_space: self.appearance.gradient_space,
gradient_cyclic: self.appearance.gradient_cyclic,
gradient_hue_direction: self.appearance.gradient_hue_direction,
transform: self.appearance.transform, transform: self.appearance.transform,
}); });
} }
@@ -869,10 +831,14 @@ impl SelectedGradient {
/// Send the per-attribute graph operations that mirror the in-memory `Gradient` onto the chain feeding the layer. /// Send the per-attribute graph operations that mirror the in-memory `Gradient` onto the chain feeding the layer.
fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradient, appearance: GradientAppearance, responses: &mut VecDeque<Message>) { fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradient, appearance: GradientAppearance, responses: &mut VecDeque<Message>) {
responses.add(GraphOperationMessage::GradientCyclicSet {
layer,
gradient_cyclic: appearance.settings.cyclic,
});
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(appearance.gradient_cyclic).unwrap_or_default(), positions: gradient.nondefault_positions(appearance.settings.cyclic).unwrap_or_default(),
}); });
responses.add(GraphOperationMessage::GradientMidpointsSet { responses.add(GraphOperationMessage::GradientMidpointsSet {
layer, layer,
@@ -888,19 +854,19 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
}); });
responses.add(GraphOperationMessage::GradientSpreadSet { responses.add(GraphOperationMessage::GradientSpreadSet {
layer, layer,
gradient_spread: appearance.gradient_spread, gradient_spread: appearance.settings.spread,
}); });
responses.add(GraphOperationMessage::GradientSpaceSet { responses.add(GraphOperationMessage::GradientSpaceSet {
layer, layer,
gradient_space: appearance.gradient_space, gradient_space: appearance.settings.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.settings.hue_direction,
});
responses.add(GraphOperationMessage::GradientInterpolationSet {
layer,
gradient_interpolation: appearance.settings.interpolation,
}); });
} }
@@ -1005,7 +971,8 @@ 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 gradient_cyclic = appearance.gradient_cyclic; let settings = appearance.settings;
let gradient_cyclic = settings.cyclic;
let first_at_start = !gradient.is_empty() && gradient.position(0, gradient_cyclic).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.; let last_at_end = !gradient.is_empty() && (1. - gradient.position(gradient.len() - 1, gradient_cyclic)).abs() < f64::EPSILON * 1000.;
@@ -1100,7 +1067,7 @@ 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 (index, midpoint_position) in midpoint_diamonds(gradient, gradient_cyclic, line_length) { for (index, midpoint_position) in midpoint_diamonds(gradient, settings, line_length) {
let midpoint_viewport = start.lerp(end, midpoint_position); let midpoint_viewport = start.lerp(end, midpoint_position);
let emphasis = if dragging == Some(GradientDragTarget::Midpoint(index)) { let emphasis = if dragging == Some(GradientDragTarget::Midpoint(index)) {
@@ -1114,7 +1081,7 @@ impl Fsm for GradientToolFsmState {
} }
if !matches!(self, GradientToolFsmState::Drawing { .. }) if !matches!(self, GradientToolFsmState::Drawing { .. })
&& calculate_insertion(start, end, gradient, gradient_cyclic, mouse).is_some() && calculate_insertion(start, end, gradient, settings, 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();
@@ -1163,7 +1130,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, selected_gradient.appearance.gradient_cyclic); let position = gradient.position(stop_index, selected_gradient.appearance.settings.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();
@@ -1203,7 +1170,7 @@ 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 gradient_cyclic = selected_gradient.appearance.settings.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, gradient_cyclic).abs() < f64::EPSILON * 1000.), GradientDragTarget::Start => (0..gradient.len()).position(|i| gradient.position(i, gradient_cyclic).abs() < f64::EPSILON * 1000.),
@@ -1219,7 +1186,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, selected_gradient.appearance.gradient_cyclic); let stop_pos = selected_gradient.gradient.position(stop_index, selected_gradient.appearance.settings.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();
@@ -1262,7 +1229,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, selected_gradient.appearance.gradient_cyclic).abs() < f64::EPSILON * 1000. { if !selected_gradient.gradient.is_empty() && selected_gradient.gradient.position(0, selected_gradient.appearance.settings.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);
@@ -1272,7 +1239,7 @@ 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() 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. && (1. - selected_gradient.gradient.position(selected_gradient.gradient.len() - 1, selected_gradient.appearance.settings.cyclic)).abs() < f64::EPSILON * 1000.
{ {
let _ = selected_gradient.gradient.pop(); let _ = selected_gradient.gradient.pop();
} else { } else {
@@ -1314,7 +1281,7 @@ impl Fsm for GradientToolFsmState {
} }
// Find the minimum and maximum positions // Find the minimum and maximum positions
let positions = selected_gradient.gradient.positions(selected_gradient.appearance.gradient_cyclic); let positions = selected_gradient.gradient.positions(selected_gradient.appearance.settings.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");
@@ -1350,14 +1317,7 @@ 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( if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.settings) {
&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);
@@ -1410,7 +1370,7 @@ 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 (index, midpoint_position) in midpoint_diamonds(&gradient, appearance.gradient_cyclic, line_length) { for (index, midpoint_position) in midpoint_diamonds(&gradient, appearance.settings, line_length) {
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 {
@@ -1437,14 +1397,14 @@ impl Fsm for GradientToolFsmState {
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 in 0..gradient.len() { for index in 0..gradient.len() {
let pos = start.lerp(end, gradient.position(index, appearance.gradient_cyclic)); let pos = start.lerp(end, gradient.position(index, appearance.settings.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, appearance.gradient_cyclic); let stop_position = gradient.position(index, appearance.settings.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. {
@@ -1499,14 +1459,7 @@ 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( if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.settings) {
&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;
@@ -1584,10 +1537,7 @@ impl Fsm for GradientToolFsmState {
GradientAppearance { GradientAppearance {
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
gradient_form: tool_options.gradient_form, gradient_form: tool_options.gradient_form,
gradient_spread: tool_options.gradient_spread, settings: tool_options.settings,
gradient_space: tool_options.gradient_space,
gradient_cyclic: tool_options.gradient_cyclic,
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
if replaceable_paint_chain(layer, &document.network_interface).is_some() { if replaceable_paint_chain(layer, &document.network_interface).is_some() {
@@ -1685,9 +1635,9 @@ 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, selected.appearance.gradient_cyclic).abs() >= f64::EPSILON * 1000., GradientDragTarget::Start => selected.gradient.is_empty() || selected.gradient.position(0, selected.appearance.settings.cyclic).abs() >= f64::EPSILON * 1000.,
GradientDragTarget::End => { GradientDragTarget::End => {
selected.gradient.is_empty() || (1. - selected.gradient.position(selected.gradient.len() - 1, selected.appearance.gradient_cyclic)).abs() >= f64::EPSILON * 1000. selected.gradient.is_empty() || (1. - selected.gradient.position(selected.gradient.len() - 1, selected.appearance.settings.cyclic)).abs() >= f64::EPSILON * 1000.
} }
_ => false, _ => false,
}) { }) {
@@ -1821,17 +1771,10 @@ impl Fsm for GradientToolFsmState {
} }
} }
fn insert_stop_at_point( fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, settings: GradientSettings) -> Option<usize> {
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_cyclic, gradient_space, gradient_hue_direction)) (0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, settings))
} }
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>) {
@@ -1858,7 +1801,7 @@ 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 (index, midpoint_position) in midpoint_diamonds(&gradient, appearance.gradient_cyclic, line_length) { for (index, midpoint_position) in midpoint_diamonds(&gradient, appearance.settings, line_length) {
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 {
@@ -1887,7 +1830,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, appearance.gradient_cyclic, mouse).is_some() { if calculate_insertion(start, end, &gradient, appearance.settings, mouse).is_some() {
return GradientHoverTarget::InsertionPoint; return GradientHoverTarget::InsertionPoint;
} }
} }
@@ -1947,10 +1890,7 @@ fn apply_gradient_update(
layer, layer,
gradient, gradient,
gradient_form: appearance.gradient_form, gradient_form: appearance.gradient_form,
gradient_spread: appearance.gradient_spread, gradient_settings: appearance.settings,
gradient_space: appearance.gradient_space,
gradient_cyclic: appearance.gradient_cyclic,
gradient_hue_direction: appearance.gradient_hue_direction,
transform: appearance.transform, transform: appearance.transform,
}); });
} }
@@ -1974,17 +1914,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(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient, settings: GradientSettings) {
fn apply_stops_update(
data: &mut GradientToolData,
context: &mut ToolActionMessageContext,
responses: &mut VecDeque<Message>,
new_gradient: Gradient,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
) {
let selected_layers: Vec<_> = context let selected_layers: Vec<_> = context
.document .document
.network_interface .network_interface
@@ -1999,21 +1929,34 @@ fn apply_stops_update(
} }
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() { if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientCyclicSet {
layer,
gradient_cyclic: settings.cyclic,
});
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 {
responses.add(GraphOperationMessage::GradientSpaceSet { layer, gradient_space }); layer,
responses.add(GraphOperationMessage::GradientCyclicSet { layer, gradient_cyclic }); gradient_spread: settings.spread,
responses.add(GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction }); });
responses.add(GraphOperationMessage::GradientSpaceSet {
layer,
gradient_space: settings.space,
});
responses.add(GraphOperationMessage::GradientHueDirectionSet {
layer,
gradient_hue_direction: settings.hue_direction,
});
responses.add(GraphOperationMessage::GradientInterpolationSet {
layer,
gradient_interpolation: settings.interpolation,
});
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) {
responses.add(GraphOperationMessage::FillGradientSet { responses.add(GraphOperationMessage::FillGradientSet {
layer, layer,
gradient: new_gradient.clone(), gradient: new_gradient.clone(),
gradient_form: appearance.gradient_form, gradient_form: appearance.gradient_form,
gradient_spread, gradient_settings: settings,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
transform: appearance.transform, transform: appearance.transform,
}); });
updated_any_layer = true; updated_any_layer = true;
@@ -2022,10 +1965,7 @@ fn apply_stops_update(
if let Some(selected_gradient) = &mut data.selected_gradient { if let Some(selected_gradient) = &mut data.selected_gradient {
selected_gradient.gradient = new_gradient.clone(); selected_gradient.gradient = new_gradient.clone();
selected_gradient.appearance.gradient_spread = gradient_spread; selected_gradient.appearance.settings = settings;
selected_gradient.appearance.gradient_space = gradient_space;
selected_gradient.appearance.gradient_cyclic = gradient_cyclic;
selected_gradient.appearance.gradient_hue_direction = gradient_hue_direction;
} }
// When no selected layer had a gradient to update, the user is editing the tool's default gradient instead. // When no selected layer had a gradient to update, the user is editing the tool's default gradient instead.
@@ -2125,31 +2065,40 @@ mod test_gradient {
use graphene_std::NodeParameter; use graphene_std::NodeParameter;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
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::style::{GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace};
use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill}; use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill};
/// A line long enough that no interval in these tests trips the closely-packed-stops hiding rule. /// A line long enough that no interval in these tests trips the closely-packed-stops hiding rule.
const UNCROWDED_LINE_LENGTH: f64 = 10_000.; const UNCROWDED_LINE_LENGTH: f64 = 10_000.;
const CYCLIC_SETTINGS: GradientSettings = GradientSettings {
spread: GradientSpread::Pad,
cyclic: true,
space: GradientSpace::OkLab,
hue_direction: GradientHueDirection::Shorter,
interpolation: GradientInterpolation::Linear,
};
#[test] #[test]
fn cyclic_adds_a_wrap_diamond_owned_by_the_last_stop() { fn cyclic_adds_a_wrap_diamond_owned_by_the_last_stop() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); 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 // 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, GradientSettings::default(), UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
assert_eq!(midpoint_diamonds(&gradient, true, UNCROWDED_LINE_LENGTH), vec![(0, 0.25), (1, 0.75)]); assert_eq!(midpoint_diamonds(&gradient, CYCLIC_SETTINGS, 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 // 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]); let mut offset = Gradient::from(vec![Color::BLACK, Color::WHITE]);
offset.set_positions(&[0.25, 0.5]); offset.set_positions(&[0.25, 0.5]);
offset.set_midpoints(&[0.5, 0.9]); offset.set_midpoints(&[0.5, 0.9]);
let diamonds = midpoint_diamonds(&offset, true, UNCROWDED_LINE_LENGTH); let diamonds = midpoint_diamonds(&offset, CYCLIC_SETTINGS, UNCROWDED_LINE_LENGTH);
assert_eq!(diamonds[1].0, 1); 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); 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 // 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]); let mut spanning = Gradient::from(vec![Color::BLACK, Color::WHITE]);
spanning.set_positions(&[0., 1.]); spanning.set_positions(&[0., 1.]);
assert_eq!(midpoint_diamonds(&spanning, true, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]); assert_eq!(midpoint_diamonds(&spanning, CYCLIC_SETTINGS, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
} }
#[test] #[test]
@@ -2188,7 +2137,7 @@ mod test_gradient {
fn new(stops: Gradient, appearance: super::GradientAppearance) -> Self { fn new(stops: Gradient, appearance: super::GradientAppearance) -> Self {
Self { Self {
stops, stops,
gradient_spread: appearance.gradient_spread, gradient_spread: appearance.settings.spread,
transform: appearance.transform, transform: appearance.transform,
} }
} }
@@ -2880,7 +2829,7 @@ mod test_gradient {
#[tokio::test] #[tokio::test]
async fn spread_set_from_the_tool_lands_on_the_gradient_value_node() { async fn spread_set_from_the_tool_lands_on_the_gradient_value_node() {
use crate::messages::tool::common_functionality::graph_modification_utils::get_chain_source_gradient_spread; use crate::messages::tool::common_functionality::graph_modification_utils::get_chain_source_gradient_settings;
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
@@ -2898,7 +2847,7 @@ mod test_gradient {
// The Properties panel reads the value node's own ramp, so the spread has to be stored there // The Properties panel reads the value node's own ramp, so the spread has to be stored there
let network_interface = &editor.active_document().network_interface; let network_interface = &editor.active_document().network_interface;
assert_eq!( assert_eq!(
get_chain_source_gradient_spread(layer, network_interface), get_chain_source_gradient_settings(layer, network_interface).map(|settings| settings.spread),
Some(GradientSpread::Reflect), Some(GradientSpread::Reflect),
"the spread should be written into the gradient value's ramp" "the spread should be written into the gradient value's ramp"
); );
@@ -3,7 +3,7 @@
import { preventEscapeClosingParentFloatingMenu } from "/src/components/layout/FloatingMenu.svelte"; import { preventEscapeClosingParentFloatingMenu } from "/src/components/layout/FloatingMenu.svelte";
import LayoutCol from "/src/components/layout/LayoutCol.svelte"; import LayoutCol from "/src/components/layout/LayoutCol.svelte";
import LayoutRow from "/src/components/layout/LayoutRow.svelte"; import LayoutRow from "/src/components/layout/LayoutRow.svelte";
import type { SpectrumInputUpdate, SpectrumMarker } from "/wrapper/pkg/graphite_wasm_wrapper"; import type { GradientInterpolation, SpectrumInputUpdate, SpectrumMarker } from "/wrapper/pkg/graphite_wasm_wrapper";
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const BUTTON_RIGHT = 2; const BUTTON_RIGHT = 2;
@@ -14,6 +14,7 @@
export let trackStartCSS: string; export let trackStartCSS: string;
export let trackEndCSS: string; export let trackEndCSS: string;
export let trackCyclic = false; export let trackCyclic = false;
export let trackInterpolation: GradientInterpolation = "Linear";
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;
@@ -363,8 +364,9 @@
// Map midpoint pairs to absolute track positions for rendering the diamond markers. // Map midpoint pairs to absolute track positions for rendering the diamond markers.
// 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. // 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[] { function diamondPositions(markers: SpectrumMarker[], showMidpoints: boolean, trackCyclic: boolean, trackInterpolation: GradientInterpolation): number[] {
if (!showMidpoints || markers.length < 2) return []; // A stepped ramp jumps at its stops, so no midpoint has anything to bias
if (!showMidpoints || trackInterpolation === "Stepped" || markers.length < 2) return [];
const positions = markers.slice(0, -1).map((marker, i) => marker.position + marker.midpoint * (markers[i + 1].position - marker.position)); 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 // The wrapped interval's diamond may land on either side of the 1|0 boundary
@@ -377,7 +379,7 @@
return positions; return positions;
} }
$: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic); $: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic, trackInterpolation);
onMount(() => { onMount(() => {
document.addEventListener("keydown", deleteShortcut); document.addEventListener("keydown", deleteShortcut);
+1 -1
View File
@@ -555,9 +555,9 @@ tagged_value! {
GradientForm(vector::style::GradientForm), GradientForm(vector::style::GradientForm),
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code #[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
GradientSpread(vector::style::GradientSpread), GradientSpread(vector::style::GradientSpread),
#[serde(alias = "GradientInterpolation")] // TODO: Eventually remove this document upgrade code
GradientSpace(vector::style::GradientSpace), GradientSpace(vector::style::GradientSpace),
GradientHueDirection(vector::style::GradientHueDirection), GradientHueDirection(vector::style::GradientHueDirection),
GradientInterpolation(vector::style::GradientInterpolation),
ReferencePoint(vector::ReferencePoint), ReferencePoint(vector::ReferencePoint),
CentroidType(vector::misc::CentroidType), CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation), BooleanOperation(vector::misc::BooleanOperation),
@@ -24,7 +24,7 @@ use graphene_std::transform::{Footprint, 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, GradientForm, GradientHueDirection, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{DashPattern, GradientForm, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification};
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
use node_registry_macros::async_node; use node_registry_macros::async_node;
@@ -78,6 +78,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpread>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpread>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpace>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpace>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientHueDirection>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientHueDirection>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientInterpolation>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
@@ -114,6 +115,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpread>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpread>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpace>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpace>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientHueDirection>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientHueDirection>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientInterpolation>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Artboard>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Artboard>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Graphic>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Graphic>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Vector>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Vector>]),
@@ -339,6 +341,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
GradientSpread, GradientSpread,
GradientSpace, GradientSpace,
GradientHueDirection, GradientHueDirection,
GradientInterpolation,
DashPattern, DashPattern,
BoxCorners, BoxCorners,
MergeByDistanceAlgorithm, MergeByDistanceAlgorithm,
@@ -429,7 +432,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
GradientForm, GradientForm,
GradientSpread, GradientSpread,
GradientSpace, GradientSpace,
GradientHueDirection GradientHueDirection,
GradientInterpolation
)); ));
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
node_types.extend(list_dyn_rows!(Raster<GPU>)); node_types.extend(list_dyn_rows!(Raster<GPU>));
@@ -547,6 +551,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
attribute_value_node!(Item<GradientSpread>), attribute_value_node!(Item<GradientSpread>),
attribute_value_node!(Item<GradientSpace>), attribute_value_node!(Item<GradientSpace>),
attribute_value_node!(Item<GradientHueDirection>), attribute_value_node!(Item<GradientHueDirection>),
attribute_value_node!(Item<GradientInterpolation>),
attribute_value_node!(Item<NodeIdPath>), attribute_value_node!(Item<NodeIdPath>),
attribute_value_node!(List<String>), attribute_value_node!(List<String>),
attribute_value_node!(List<Color>), attribute_value_node!(List<Color>),
+2 -2
View File
@@ -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_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING,
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, ATTR_LETTER_TILT, 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,6 +65,9 @@ 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 `GradientInterpolation` (`Stepped`, `Linear`, or `Smooth`), the path its stops interpolate along
/// and thus whether the ramp jumps, turns corners, or flows smoothly through them.
pub const ATTR_GRADIENT_INTERPOLATION: &str = "gradient_interpolation";
/// Gradient's `bool` (implicit default `false`) for treating the stop list as a cycle, where a wrapped interval /// 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. /// interpolates from the last stop through the 1|0 boundary back to the first.
pub const ATTR_GRADIENT_CYCLIC: &str = "gradient_cyclic"; pub const ATTR_GRADIENT_CYCLIC: &str = "gradient_cyclic";
@@ -1,16 +1,16 @@
use crate::renderer::{ClearGuardPlacement, RenderParams, format_transform_matrix, gradient_placement, spread_adjusted_samples, transform_is_invertible}; use crate::renderer::{ClearGuardPlacement, RenderParams, format_transform_matrix, gradient_placement, gradient_settings_at, spread_adjusted_samples, transform_is_invertible};
use crate::{Render, RenderSvgSegmentList, SvgRender}; 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_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_TRANSFORM, Color}; use core_types::{ATTR_GRADIENT_FORM, 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;
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write; use std::fmt::Write;
use vector_types::Gradient; use vector_types::Gradient;
use vector_types::gradient::{GradientHueDirection, GradientSpace, GradientSpread}; use vector_types::gradient::GradientSpread;
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
pub enum PaintTarget { pub enum PaintTarget {
@@ -95,20 +95,9 @@ impl RenderExt for List<Gradient> {
let Some(stops) = self.element(0) else { return 0 }; let Some(stops) = self.element(0) else { return 0 };
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0); let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
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 settings = gradient_settings_at(self, 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 (samples, _) = spread_adjusted_samples( let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
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");
@@ -147,10 +136,10 @@ impl RenderExt for List<Gradient> {
format!(r#" gradientTransform="{gradient_transform}""#) format!(r#" gradientTransform="{gradient_transform}""#)
}; };
let gradient_spread = if matches!(gradient_spread, GradientSpread::Pad | GradientSpread::Clear) { let gradient_spread = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) {
String::new() String::new()
} else { } else {
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name()) format!(r#" spreadMethod="{}""#, settings.spread.svg_name())
}; };
let gradient_id = generate_uuid(); let gradient_id = generate_uuid();
+61 -83
View File
@@ -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_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_FONT_SIZE, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING,
ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, 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};
@@ -39,7 +39,7 @@ use std::fmt::Write;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use vector_types::gradient::{GradientHueDirection, GradientSpace, GradientSpread}; use vector_types::gradient::{GradientSettings, GradientSpread};
use vello::*; use vello::*;
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@@ -416,17 +416,9 @@ pub(crate) enum ClearGuardPlacement {
/// The `Clear` spread brackets the samples with transparent guard stops placed per `guards`: the pad extension then /// The `Clear` spread brackets the samples with transparent guard stops placed per `guards`: the pad extension then
/// paints transparency outward while hard stops cut the paint off exactly at the unit range's boundaries. A radial /// paints transparency outward while hard stops cut the paint off exactly at the unit range's boundaries. A radial
/// gradient's span still starts at zero, since its sampling distance never goes below the center. /// gradient's span still starts at zero, since its sampling distance never goes below the center.
pub(crate) fn spread_adjusted_samples( pub(crate) fn spread_adjusted_samples(gradient: &Gradient, settings: GradientSettings, gradient_form: GradientForm, guards: ClearGuardPlacement) -> (GradientSamples, (f64, f64)) {
gradient: &Gradient, let samples = gradient.interpolated_samples(settings);
gradient_spread: GradientSpread, if settings.spread != GradientSpread::Clear {
gradient_form: GradientForm,
gradient_cyclic: bool,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
guards: ClearGuardPlacement,
) -> (GradientSamples, (f64, f64)) {
let samples = gradient.interpolated_samples(gradient_cyclic, gradient_space, gradient_hue_direction);
if gradient_spread != GradientSpread::Clear {
return (samples, (0., 1.)); return (samples, (0., 1.));
} }
@@ -504,25 +496,25 @@ fn peniko_extend(gradient_spread: GradientSpread) -> peniko::Extend {
} }
} }
/// The whole-ramp settings attributes carried by the gradient at `index` of the list.
pub(crate) fn gradient_settings_at(list: &List<Gradient>, index: usize) -> GradientSettings {
GradientSettings {
spread: list.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, index),
cyclic: list.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC, index),
space: list.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index),
hue_direction: list.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index),
interpolation: list.attribute_cloned_or_default(ATTR_GRADIENT_INTERPOLATION, index),
}
}
fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?; let stops = gradient_list.element(0)?;
let gradient_form: GradientForm = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0); let gradient_form: GradientForm = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
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 settings = gradient_settings_at(gradient_list, 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 (samples, span) = spread_adjusted_samples( let (samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);
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);
@@ -544,7 +536,7 @@ fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_trans
} }
.into(), .into(),
}, },
extend: peniko_extend(gradient_spread), extend: peniko_extend(settings.spread),
stops: peniko_stops, stops: peniko_stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default() ..Default::default()
@@ -2201,11 +2193,8 @@ impl Render for List<Gradient> {
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
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 settings = gradient_settings_at(self, 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 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| {
if let Some((min, size)) = thumbnail_rect { if let Some((min, size)) = thumbnail_rect {
@@ -2221,15 +2210,7 @@ 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( let (samples, _) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
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 {
@@ -2253,10 +2234,10 @@ impl Render for List<Gradient> {
}; };
let gradient_id = generate_uuid(); let gradient_id = generate_uuid();
let gradient_spread_attribute = if matches!(gradient_spread, GradientSpread::Pad | GradientSpread::Clear) { let gradient_spread_attribute = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) {
String::new() String::new()
} else { } else {
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name()) format!(r#" spreadMethod="{}""#, settings.spread.svg_name())
}; };
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied // The unit gradient line is the +X unit vector in local space, before the item's transform is applied
@@ -2296,12 +2277,7 @@ impl Render for List<Gradient> {
return; return;
} }
for (((index, gradient), gradient_spread), gradient_form) in self for ((index, gradient), gradient_form) in self.iter_element_values().enumerate().zip(self.iter_attribute_values_or_default::<GradientForm>(ATTR_GRADIENT_FORM)) {
.iter_element_values()
.enumerate()
.zip(self.iter_attribute_values_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD))
.zip(self.iter_attribute_values_or_default::<GradientForm>(ATTR_GRADIENT_FORM))
{
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
@@ -2311,22 +2287,12 @@ impl Render for List<Gradient> {
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
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 settings = gradient_settings_at(self, index);
let gradient_cyclic: bool = self.attribute_cloned_or_default(ATTR_GRADIENT_CYCLIC, index); let (samples, span) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);
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_cyclic,
gradient_space,
gradient_hue_direction,
ClearGuardPlacement::VelloRampTexels,
);
let stops = peniko_color_stops(&samples); let stops = peniko_color_stops(&samples);
let extend = peniko_extend(gradient_spread); let extend = peniko_extend(settings.spread);
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied. // The unit gradient line is the +X unit vector in local space, before the item's transform is applied.
// For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies. // For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies.
@@ -2866,6 +2832,7 @@ impl SvgRenderAttrs<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use vector_types::gradient::GradientSpace;
#[test] #[test]
fn spread_adjusted_samples_wraps_clear_in_transparent_guards() { fn spread_adjusted_samples_wraps_clear_in_transparent_guards() {
@@ -2873,24 +2840,32 @@ mod tests {
let (samples, span) = spread_adjusted_samples( let (samples, span) = spread_adjusted_samples(
&gradient, &gradient,
GradientSpread::Repeat, GradientSettings {
spread: GradientSpread::Repeat,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear, GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder, ClearGuardPlacement::SvgStopOrder,
); );
assert_eq!(span, (0., 1.)); assert_eq!(span, (0., 1.));
assert_eq!(samples, gradient.interpolated_samples(false, GradientSpace::RgbGamma, Default::default())); assert_eq!(
samples,
gradient.interpolated_samples(GradientSettings {
space: 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, GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear, GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder, ClearGuardPlacement::SvgStopOrder,
); );
assert_eq!(span, (0., 1.)); assert_eq!(span, (0., 1.));
@@ -2903,11 +2878,12 @@ mod tests {
let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.); let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.);
let (samples, span) = spread_adjusted_samples( let (samples, span) = spread_adjusted_samples(
&gradient, &gradient,
GradientSpread::Clear, GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear, GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::VelloRampTexels, ClearGuardPlacement::VelloRampTexels,
); );
assert_eq!( assert_eq!(
@@ -2924,11 +2900,12 @@ mod tests {
// A radial keeps its stops and span anchored at zero, with no guard below the center // A radial keeps its stops and span anchored at zero, with no guard below the center
let (samples, span) = spread_adjusted_samples( let (samples, span) = spread_adjusted_samples(
&gradient, &gradient,
GradientSpread::Clear, GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Radial, GradientForm::Radial,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::VelloRampTexels, ClearGuardPlacement::VelloRampTexels,
); );
assert_eq!(span.0, 0.); assert_eq!(span.0, 0.);
@@ -2940,11 +2917,12 @@ mod tests {
fn spread_adjusted_samples_keeps_a_stopless_clear_gradient_black_inside_the_range() { fn spread_adjusted_samples_keeps_a_stopless_clear_gradient_black_inside_the_range() {
let (samples, _) = spread_adjusted_samples( let (samples, _) = spread_adjusted_samples(
&Gradient::from(Vec::new()), &Gradient::from(Vec::new()),
GradientSpread::Clear, GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear, GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder, ClearGuardPlacement::SvgStopOrder,
); );
let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect(); let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect();
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ pub mod vector;
// Re-export commonly used types at the crate root // Re-export commonly used types at the crate root
pub use core_types as gcore; pub use core_types as gcore;
pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStop}; pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStop};
pub use math::{QuadExt, RectExt}; pub use math::{QuadExt, RectExt};
pub use subpath::Subpath; pub use subpath::Subpath;
pub use vector::Vector; pub use vector::Vector;
@@ -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_cyclic, ramp.gradient_space, ramp.gradient_hue_direction)), Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.into())),
} }
} }
} }
@@ -361,19 +361,6 @@ impl Stroke {
self self
} }
pub fn with_dash_lengths(mut self, dash_lengths: &str) -> Option<Self> {
dash_lengths
.split(&[',', ' '])
.filter(|x| !x.is_empty())
.map(str::parse::<f64>)
.collect::<Result<Vec<_>, _>>()
.ok()
.map(|lengths| {
self.dash_lengths = lengths;
self
})
}
pub fn with_dash_offset(mut self, dash_offset: f64) -> Self { pub fn with_dash_offset(mut self, dash_offset: f64) -> Self {
self.dash_offset = dash_offset; self.dash_offset = dash_offset;
self self
+18 -1
View File
@@ -9,7 +9,7 @@ use rand::SeedableRng;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
use std::cmp::Ordering; use std::cmp::Ordering;
use vector_types::gradient::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread}; use vector_types::gradient::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread};
use vector_types::{Gradient, ReferencePoint}; use vector_types::{Gradient, ReferencePoint};
/// Returns the list with the item at the specified index removed. /// Returns the list with the item at the specified index removed.
@@ -764,6 +764,23 @@ fn read_attribute_gradient_space(
result result
} }
/// Reads a named `GradientInterpolation` attribute from the input list, outputting each value as an element of a new `GradientInterpolation[]`.
#[node_macro::node(category("Attributes: Read"))]
fn read_attribute_gradient_interpolation(
_: impl Ctx,
content: ListDyn,
/// The attribute name (key) to read.
name: Item<String>,
) -> List<GradientInterpolation> {
let name = name.into_element();
let mut result = List::with_capacity(content.len());
for index in 0..content.len() {
let Some(value) = content.attribute::<GradientInterpolation>(&name, index) else { continue };
result.push(Item::new_from_element(*value));
}
result
}
/// Reads a named `GradientHueDirection` attribute from the input list, outputting each value as an element of a new `GradientHueDirection[]`. /// Reads a named `GradientHueDirection` attribute from the input list, outputting each value as an element of a new `GradientHueDirection[]`.
#[node_macro::node(category("Attributes: Read"))] #[node_macro::node(category("Attributes: Read"))]
fn read_attribute_gradient_hue_direction( fn read_attribute_gradient_hue_direction(
+10 -7
View File
@@ -1401,6 +1401,14 @@ fn gradient_space(_: impl Ctx, gradient: Item<Gradient>, space: Item<vector_type
gradient gradient
} }
/// Sets the path each gradient in the input list interpolates along, deciding whether it jumps, turns corners, or flows smoothly through its stops.
#[node_macro::node(category("Gradient"))]
fn gradient_interpolation(_: impl Ctx, gradient: Item<Gradient>, interpolation: Item<vector_types::GradientInterpolation>) -> Item<Gradient> {
let mut gradient = gradient;
gradient.set_attribute(core_types::ATTR_GRADIENT_INTERPOLATION, *interpolation.element());
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. /// 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"))] #[node_macro::node(category("Gradient"))]
fn gradient_cyclic(_: impl Ctx, gradient: Item<Gradient>, cyclic: Item<bool>) -> Item<Gradient> { fn gradient_cyclic(_: impl Ctx, gradient: Item<Gradient>, cyclic: Item<bool>) -> Item<Gradient> {
@@ -1444,13 +1452,8 @@ fn gradient_midpoints(_: impl Ctx, gradient: Item<Gradient>, midpoints: List<f64
/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear. Colors between stops interpolate in the gradient's `gradient_space` color space. /// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear. Colors between stops interpolate in the gradient's `gradient_space` color space.
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
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 settings = vector_types::GradientSettings::from(&gradient);
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE); let color = gradient.element().evaluate(*position.element(), settings);
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 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)
} }
@@ -48,10 +48,10 @@ mod blend_std {
let mut combined_stops = self.positions(false).into_iter().chain(under.positions(false)).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 over_evaluator = self.evaluator(Default::default());
let under_evaluator = under.evaluator(Default::default());
let stops = combined_stops.into_iter().map(|position| { let stops = combined_stops.into_iter().map(|position| {
let over_color = self.evaluate(position, Default::default(), false, Default::default(), Default::default()); let color = blend_fn(over_evaluator.evaluate(position), under_evaluator.evaluate(position));
let under_color = under.evaluate(position, Default::default(), false, Default::default(), Default::default());
let color = blend_fn(over_color, under_color);
GradientStop { position, midpoint: 0.5, color } GradientStop { position, midpoint: 0.5, color }
}); });
+3 -6
View File
@@ -22,17 +22,14 @@ async fn gradient_map<T: Adjust<Color> + Send>(
reverse: Item<bool>, reverse: Item<bool>,
) -> Item<T> { ) -> Item<T> {
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 settings = vector_types::GradientSettings::from(&gradient);
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE); let evaluator = gradient.into_element().evaluator(settings);
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 = gradient.into_element();
let reverse = reverse.into_element(); let reverse = reverse.into_element();
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_cyclic, gradient_space, gradient_hue_direction) evaluator.evaluate(intensity as f64)
}); });
image image
+10 -8
View File
@@ -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_CYCLIC, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath}; use core_types::list::{ATTR_FILL, 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;
@@ -32,7 +32,7 @@ use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles, bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
}; };
use vector_types::vector::style::{DashPattern, Gradient, GradientHueDirection, GradientSpace, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{DashPattern, Gradient, GradientSettings, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::vector::{PointDomain, RegionDomain}; use vector_types::vector::{PointDomain, RegionDomain};
@@ -141,11 +141,14 @@ 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); // The factor spans 0..=1, so the spread deliberately stays Pad (Repeat would wrap the final element onto the first stop's color)
let gradient_cyclic = gradient.attribute_cloned_or_default::<bool>(ATTR_GRADIENT_CYCLIC); let settings = GradientSettings {
let gradient_hue_direction = gradient.attribute_cloned_or_default::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION); spread: Default::default(),
..GradientSettings::from(&gradient)
};
let element = gradient.into_element(); let element = gradient.into_element();
let gradient = if reverse { element.reversed(gradient_cyclic) } else { element }; let gradient = if reverse { element.reversed(settings.cyclic) } else { element };
let evaluator = gradient.evaluator(settings);
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -161,8 +164,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) let color = evaluator.evaluate(factor);
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 {