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 Dennis Kobert
parent c00eb21a7f
commit 9181a62e99
24 changed files with 1552 additions and 750 deletions

View File

@@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
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.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -55,6 +55,8 @@ pub enum ColorPickerMessage {
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.
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).
StartTransaction,

View File

@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::color::SRGBA8;
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).
const MIN_MIDPOINT: f64 = 0.01;
@@ -33,6 +33,7 @@ pub struct ColorPickerMessageHandler {
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
gradient_interpolation: GradientInterpolation,
active_marker_index: Option<u32>,
active_marker_is_midpoint: bool,
@@ -58,6 +59,7 @@ impl Default for ColorPickerMessageHandler {
gradient_space: GradientSpace::default(),
gradient_cyclic: false,
gradient_hue_direction: GradientHueDirection::default(),
gradient_interpolation: GradientInterpolation::default(),
active_marker_index: None,
active_marker_is_midpoint: false,
allow_none: true,
@@ -82,6 +84,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.gradient_space = GradientSpace::default();
self.gradient_cyclic = false;
self.gradient_hue_direction = GradientHueDirection::default();
self.gradient_interpolation = GradientInterpolation::default();
self.active_marker_index = None;
self.active_marker_is_midpoint = false;
}
@@ -91,6 +94,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.gradient_space = GradientSpace::default();
self.gradient_cyclic = false;
self.gradient_hue_direction = GradientHueDirection::default();
self.gradient_interpolation = GradientInterpolation::default();
self.active_marker_index = None;
self.active_marker_is_midpoint = false;
self.adopt_color(color);
@@ -102,6 +106,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.gradient_space = ramp.gradient_space;
self.gradient_cyclic = ramp.gradient_cyclic;
self.gradient_hue_direction = ramp.gradient_hue_direction;
self.gradient_interpolation = ramp.gradient_interpolation;
let gradient = Gradient::from(ramp);
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
self.gradient = Some(gradient);
@@ -212,28 +217,20 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_spread = gradient_spread;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
});
self.send_layouts(responses);
}
ColorPickerMessage::SetGradientCyclic { gradient_cyclic } => {
let Some(gradient) = &self.gradient else { return };
let Some(gradient) = &mut self.gradient else { return };
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 {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
});
self.send_layouts(responses);
}
@@ -242,13 +239,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_space = gradient_space;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
});
self.send_layouts(responses);
}
@@ -257,13 +248,16 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_hue_direction = gradient_hue_direction;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_space: self.gradient_space,
gradient_cyclic: self.gradient_cyclic,
gradient_hue_direction,
..GradientRamp::from(gradient)
}),
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
});
self.send_layouts(responses);
}
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
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);
}
@@ -315,6 +309,17 @@ impl ColorPickerMessageHandler {
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) {
self.old_hue = self.hue;
self.old_saturation = self.saturation;
@@ -350,14 +355,9 @@ impl ColorPickerMessageHandler {
&& (active_index as usize) < gradient.len()
{
gradient.set_color(active_index as usize, color);
let ramp = GradientRamp::from(&*gradient);
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
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)
}),
value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
});
} else {
responses.add(FrontendMessage::ColorPickerColorChanged {
@@ -414,7 +414,7 @@ impl ColorPickerMessageHandler {
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
}
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_is_midpoint = false;
if let Some(color) = gradient.color(new_index) {
@@ -487,13 +487,7 @@ impl ColorPickerMessageHandler {
}
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
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)
}),
value: FillChoice::Gradient(GradientRamp::from(&gradient).with_settings(self.gradient_settings())),
});
self.gradient = Some(gradient);
self.send_layouts(responses);
@@ -527,6 +521,7 @@ impl ColorPickerMessageHandler {
.track_space(self.gradient_space)
.track_cyclic(self.gradient_cyclic)
.track_hue_direction(self.gradient_hue_direction)
.track_interpolation(self.gradient_interpolation)
.markers(markers)
.active_marker_index(self.active_marker_index)
.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)
if self.gradient.is_some() {
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![
TextLabel::new("Space").tooltip_label("Gradient Space").tooltip_description(SPACE_DESCRIPTION).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)
.selected_index(Some(self.gradient_hue_direction as u32))
.disabled(self.disabled)
.tooltip_label("Gradient Hue Direction")
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
.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 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 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`).
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.

View File

@@ -531,13 +531,19 @@ fn populate_computed_display_fields(layout: &mut Layout) {
color_input.chosen_gradient = color_input.value.to_css_background_image();
}
Widget::SpectrumInput(spectrum_input) => {
spectrum_input.track_css = spectrum_input
.track
.to_css_linear_gradient(spectrum_input.track_cyclic, spectrum_input.track_space, spectrum_input.track_hue_direction);
// The track strip spans exactly 0 to 1, which no spread affects, so the widget carries no spread of its own
let settings = graphene_std::vector::style::GradientSettings {
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'
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 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()
};
spectrum_input.track_start_css = cap(0.);

View File

@@ -7,7 +7,7 @@ use derivative::*;
use graphene_std::Color;
use graphene_std::color::SRGBA8;
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;
#[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.
#[serde(skip)]
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.
#[serde(rename = "trackCSS")]
#[widget_builder(skip)]

View File

@@ -23,7 +23,9 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
use graphene_std::vector::misc::{
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::{Artboard, Color, Graphic};
use std::any::Any;
@@ -220,6 +222,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
List<GradientSpread>,
List<GradientSpace>,
List<GradientHueDirection>,
List<GradientInterpolation>,
List<DashPattern>,
List<BoxCorners>,
List<StrokeJoin>,
@@ -1014,6 +1017,7 @@ impl_table_item_layout_for_choice_enum!(
GradientSpread,
GradientSpace,
GradientHueDirection,
GradientInterpolation,
StrokeJoin,
StrokeAlign,
StrokeCap,
@@ -1228,6 +1232,7 @@ macro_rules! known_item_types {
GradientSpread,
GradientSpace,
GradientHueDirection,
GradientInterpolation,
StrokeJoin,
StrokeAlign,
StrokeCap,

View File

@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
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};
#[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -29,10 +29,7 @@ pub enum GraphOperationMessage {
#[serde(skip)]
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
gradient_settings: GradientSettings,
transform: DAffine2,
},
BlendingFillSet {
@@ -76,6 +73,10 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
gradient_hue_direction: GradientHueDirection,
},
GradientInterpolationSet {
layer: LayerNodeIdentifier,
gradient_interpolation: GradientInterpolation,
},
OpacitySet {
layer: LayerNodeIdentifier,
opacity: f64,

View File

@@ -14,7 +14,7 @@ use graph_craft::document::{NodeId, NodeInput};
use graphene_std::list::List;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
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};
#[derive(ExtractField)]
@@ -49,14 +49,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
layer,
gradient,
gradient_form,
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
gradient_settings,
transform,
} => {
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 } => {
@@ -109,6 +106,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
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 } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.opacity_set(opacity);
@@ -728,7 +730,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 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.;
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.
@@ -981,10 +983,13 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
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
let gradient_space = gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, false, Default::default(), transform);
let settings = GradientSettings {
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) => {
let gradient_transform = usvg_transform(radial.transform());
@@ -1007,10 +1012,12 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
Gradient::new(stops)
}
};
let gradient_spread = convert_gradient_spread(radial.spread_method());
let gradient_space = gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, false, Default::default(), transform);
let settings = GradientSettings {
spread: convert_gradient_spread(radial.spread_method()),
space: gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma),
..Default::default()
};
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
}
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
};
@@ -1042,6 +1049,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]
fn color_interpolation_reads_style_blocks_with_selector_specificity() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">

View File

@@ -19,7 +19,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
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::{Artboard, Color, Graphic};
@@ -433,30 +433,13 @@ impl<'a> ModifyInputsContext<'a> {
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,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2,
) {
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
let ramp = GradientRamp::from(gradient);
let ramp = GradientRamp {
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..ramp
};
let ramp = GradientRamp::from(gradient).with_settings(settings);
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
@@ -795,7 +778,9 @@ impl<'a> ModifyInputsContext<'a> {
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) {
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 {
@@ -803,7 +788,20 @@ impl<'a> ModifyInputsContext<'a> {
};
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);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
}

View File

@@ -34,8 +34,8 @@ use graphene_std::vector::misc::{
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin,
build_transform_with_y_preservation,
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap,
StrokeJoin, build_transform_with_y_preservation,
};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::{NodeParameter, ParameterRef};
@@ -297,6 +297,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::<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::<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::<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(),
@@ -2408,10 +2409,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Gradient {
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
settings: GradientSettings,
transform: DAffine2,
/// 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,
@@ -2441,10 +2439,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Some(gradient) => ResolvedFill::Gradient {
gradient: gradient.stops,
gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread,
gradient_space: gradient.gradient_space,
gradient_cyclic: gradient.gradient_cyclic,
gradient_hue_direction: gradient.gradient_hue_direction,
settings: gradient.settings,
transform: gradient.transform,
transform_is_value: gradient.transform_is_value,
},
@@ -2472,33 +2467,15 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
};
match &fill {
ResolvedFill::Gradient {
gradient: stops,
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..
} => {
ResolvedFill::Gradient { gradient: stops, settings, .. } => {
let stops = stops.clone();
let gradient_spread = *gradient_spread;
let gradient_space = *gradient_space;
let gradient_cyclic = *gradient_cyclic;
let gradient_hue_direction = *gradient_hue_direction;
let settings = *settings;
let reverse_button = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops")
.tooltip_description("Reverse the gradient color stops.")
.on_update(update_value(
move |_| {
TaggedValue::GradientRamp(GradientRamp {
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
..GradientRamp::from(stops.reversed(gradient_cyclic))
})
},
move |_| TaggedValue::GradientRamp(GradientRamp::from(stops.reversed(settings.cyclic)).with_settings(settings)),
node_id,
FillInput,
))
@@ -2517,20 +2494,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
FillChoice::<SRGBA8>::None
}
}
ResolvedFill::Gradient {
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::Gradient { gradient: stops, settings, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp::from(stops).with_settings(*settings)),
ResolvedFill::Other => FillChoice::<SRGBA8>::None,
};

View File

@@ -280,10 +280,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::math_nodes::gradient_form::IDENTIFIER,
aliases: &["math_nodes::GradientTypeNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::gradient_space::IDENTIFIER,
aliases: &["math_nodes::GradientInterpolationNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::gradient_spread::IDENTIFIER,
aliases: &["math_nodes::SpreadMethodNode"],

View File

@@ -14,7 +14,7 @@ use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
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::{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 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.
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 TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
return None;
@@ -397,24 +397,9 @@ fn get_chain_source_gradient_ramp<'a>(layer: LayerNodeIdentifier, network_interf
Some(ramp)
}
/// The spread 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> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread)
}
/// 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)
/// The whole-ramp settings baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_settings(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSettings> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.into())
}
/// 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 stops: Gradient,
pub gradient_form: GradientForm,
pub gradient_spread: GradientSpread,
pub gradient_space: GradientSpace,
pub gradient_cyclic: bool,
pub gradient_hue_direction: GradientHueDirection,
pub settings: GradientSettings,
pub transform: DAffine2,
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
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 {
return None;
};
let gradient_spread = ramp.gradient_spread;
let gradient_space = ramp.gradient_space;
let gradient_cyclic = ramp.gradient_cyclic;
let gradient_hue_direction = ramp.gradient_hue_direction;
let settings = GradientSettings::from(ramp);
let stops = Gradient::from(ramp);
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_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 {
stops,
gradient_form,
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
settings,
transform,
transform_is_value: transform_input.is_some(),
})
@@ -959,10 +935,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
layer,
gradient: Gradient::from(ramp),
gradient_form,
gradient_spread: ramp.gradient_spread,
gradient_space: ramp.gradient_space,
gradient_cyclic: ramp.gradient_cyclic,
gradient_hue_direction: ramp.gradient_hue_direction,
gradient_settings: ramp.into(),
transform,
});
}

View File

@@ -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::tool::common_functionality::auto_panning::AutoPanning;
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,
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,
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,
replaceable_paint_chain, reverse_direction_tooltip_description,
};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use glam::DMat2;
use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8;
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)]
pub struct GradientTool {
@@ -29,10 +29,7 @@ pub struct GradientTool {
#[derive(Default)]
pub struct GradientOptions {
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
settings: GradientSettings,
}
#[impl_message(Message, ToolMessage, Gradient)]
@@ -104,7 +101,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
context,
responses,
|_| true,
|(gradient, appearance)| *gradient = gradient.reversed(appearance.gradient_cyclic),
|(gradient, appearance)| *gradient = gradient.reversed(appearance.settings.cyclic),
);
}
GradientOptionsUpdate::ReverseDirection => apply_gradient_update(
@@ -146,20 +143,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
}
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
let ramp = GradientRamp::from(&ramp);
self.options.gradient_spread = ramp.gradient_spread;
self.options.gradient_space = ramp.gradient_space;
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,
);
self.options.settings = GradientSettings::from(&ramp);
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), self.options.settings);
}
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
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;
needs_refresh = true;
}
if self.options.gradient_spread != appearance.gradient_spread {
self.options.gradient_spread = appearance.gradient_spread;
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;
if self.options.settings != appearance.settings {
self.options.settings = appearance.settings;
needs_refresh = true;
}
}
@@ -287,23 +260,17 @@ impl LayoutHolder for GradientTool {
},
])
});
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
gradient_spread: self.options.gradient_spread,
gradient_space: self.options.gradient_space,
gradient_cyclic: self.options.gradient_cyclic,
gradient_hue_direction: self.options.gradient_hue_direction,
..GradientRamp::from(&stops_value)
}))
.allow_none(false)
.narrow(true)
.tooltip_label("Gradient Stops")
.tooltip_description("Edit the gradient's color stops.")
.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 stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp::from(&stops_value).with_settings(self.options.settings)))
.allow_none(false)
.narrow(true)
.tooltip_label("Gradient Stops")
.tooltip_description("Edit the gradient's color stops.")
.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)
.tooltip_label("Reverse Stops")
@@ -391,10 +358,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
gradient.stops,
GradientAppearance {
gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread,
gradient_space: gradient.gradient_space,
gradient_cyclic: gradient.gradient_cyclic,
gradient_hue_direction: gradient.gradient_hue_direction,
settings: gradient.settings,
transform: gradient.transform,
},
GradientSource::Direct,
@@ -413,10 +377,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
struct GradientAppearance {
transform: DAffine2,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_cyclic: bool,
gradient_hue_direction: GradientHueDirection,
settings: GradientSettings,
}
/// 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 {
transform: composed_transform,
gradient_form: gradient_form.unwrap_or_default(),
gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(),
gradient_space: get_chain_source_gradient_space(layer, network_interface).unwrap_or_default(),
gradient_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(),
settings: get_chain_source_gradient_settings(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
/// 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());
for index in 0..gradient.len().saturating_sub(1) {
@@ -585,7 +549,8 @@ struct SelectedGradient {
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 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
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);
if midpoint_viewport.distance_squared(mouse) < GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2) {
return None;
@@ -770,7 +735,7 @@ impl SelectedGradient {
let min_gap = GRADIENT_STOP_MIN_VIEWPORT_GAP / line_length;
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_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
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);
}
}
@@ -848,10 +813,7 @@ impl SelectedGradient {
layer,
gradient: self.gradient.clone(),
gradient_form: self.appearance.gradient_form,
gradient_spread: self.appearance.gradient_spread,
gradient_space: self.appearance.gradient_space,
gradient_cyclic: self.appearance.gradient_cyclic,
gradient_hue_direction: self.appearance.gradient_hue_direction,
gradient_settings: self.appearance.settings,
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.
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::GradientPositionsSet {
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 {
layer,
@@ -888,19 +854,19 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
});
responses.add(GraphOperationMessage::GradientSpreadSet {
layer,
gradient_spread: appearance.gradient_spread,
gradient_spread: appearance.settings.spread,
});
responses.add(GraphOperationMessage::GradientSpaceSet {
layer,
gradient_space: appearance.gradient_space,
});
responses.add(GraphOperationMessage::GradientCyclicSet {
layer,
gradient_cyclic: appearance.gradient_cyclic,
gradient_space: appearance.settings.space,
});
responses.add(GraphOperationMessage::GradientHueDirectionSet {
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));
// 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 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_length = start.distance(end);
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 emphasis = if dragging == Some(GradientDragTarget::Midpoint(index)) {
@@ -1114,7 +1081,7 @@ impl Fsm for GradientToolFsmState {
}
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 perp = dir.perp();
@@ -1163,7 +1130,7 @@ impl Fsm for GradientToolFsmState {
let gradient = &selected_gradient.gradient;
if stop_index < gradient.len() {
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 end = transform.transform_point2(DVec2::X);
let position = start.lerp(end, position).into();
@@ -1203,7 +1170,7 @@ impl Fsm for GradientToolFsmState {
GradientDragTarget::Start | GradientDragTarget::End | GradientDragTarget::Stop(_) => {
// Find the stop index from the drag target
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 {
GradientDragTarget::Stop(i) => Some(i),
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;
}
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 viewport_pos = start.lerp(end, stop_pos);
let position = viewport_pos.into();
@@ -1262,7 +1229,7 @@ impl Fsm for GradientToolFsmState {
match selected_gradient.dragging {
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)
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);
} else {
responses.add(DocumentMessage::AbortTransaction);
@@ -1272,7 +1239,7 @@ impl Fsm for GradientToolFsmState {
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)
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();
} else {
@@ -1314,7 +1281,7 @@ impl Fsm for GradientToolFsmState {
}
// 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 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 distance < (SELECTION_THRESHOLD * 2.) {
// Try and insert the new stop
if let Some(index) = insert_stop_at_point(
&mut gradient,
mouse,
unit_to_viewport,
appearance.gradient_cyclic,
appearance.gradient_space,
appearance.gradient_hue_direction,
) {
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.settings) {
responses.add(DocumentMessage::StartTransaction);
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
@@ -1410,7 +1370,7 @@ impl Fsm for GradientToolFsmState {
if drag_hint.is_none() {
let line_length = start.distance(end);
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);
if midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
@@ -1437,14 +1397,14 @@ impl Fsm for GradientToolFsmState {
if drag_hint.is_none() {
let mut best: Option<(f64, usize)> = None;
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);
if dist_sq < tolerance && best.as_ref().is_none_or(|&(best_dist, _)| dist_sq < best_dist) {
best = Some((dist_sq, index));
}
}
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
// gradient line endpoint geometry (start/end) instead of stop position
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) {
let mut new_gradient = gradient.clone();
if let Some(index) = insert_stop_at_point(
&mut new_gradient,
mouse,
unit_to_viewport,
appearance.gradient_cyclic,
appearance.gradient_space,
appearance.gradient_hue_direction,
) {
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.settings) {
responses.add(DocumentMessage::StartTransaction);
transaction_started = true;
@@ -1584,10 +1537,7 @@ impl Fsm for GradientToolFsmState {
GradientAppearance {
transform: DAffine2::IDENTITY,
gradient_form: tool_options.gradient_form,
gradient_spread: tool_options.gradient_spread,
gradient_space: tool_options.gradient_space,
gradient_cyclic: tool_options.gradient_cyclic,
gradient_hue_direction: tool_options.gradient_hue_direction,
settings: tool_options.settings,
},
// 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() {
@@ -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
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 => {
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,
}) {
@@ -1821,17 +1771,10 @@ impl Fsm for GradientToolFsmState {
}
}
fn insert_stop_at_point(
gradient: &mut Gradient,
point: DVec2,
unit_to_viewport: DAffine2,
gradient_cyclic: bool,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
) -> Option<usize> {
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, settings: GradientSettings) -> Option<usize> {
let (start, end) = gradient_handle_positions(unit_to_viewport);
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>) {
@@ -1858,7 +1801,7 @@ fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> Gradi
let line_length = start.distance(end);
// 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);
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
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;
}
}
@@ -1947,10 +1890,7 @@ fn apply_gradient_update(
layer,
gradient,
gradient_form: appearance.gradient_form,
gradient_spread: appearance.gradient_spread,
gradient_space: appearance.gradient_space,
gradient_cyclic: appearance.gradient_cyclic,
gradient_hue_direction: appearance.gradient_hue_direction,
gradient_settings: appearance.settings,
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
/// 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.
#[allow(clippy::too_many_arguments)]
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,
) {
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient, settings: GradientSettings) {
let selected_layers: Vec<_> = context
.document
.network_interface
@@ -1999,21 +1929,34 @@ fn apply_stops_update(
}
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::GradientSpreadSet { layer, gradient_spread });
responses.add(GraphOperationMessage::GradientSpaceSet { layer, gradient_space });
responses.add(GraphOperationMessage::GradientCyclicSet { layer, gradient_cyclic });
responses.add(GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction });
responses.add(GraphOperationMessage::GradientSpreadSet {
layer,
gradient_spread: settings.spread,
});
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;
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
responses.add(GraphOperationMessage::FillGradientSet {
layer,
gradient: new_gradient.clone(),
gradient_form: appearance.gradient_form,
gradient_spread,
gradient_space,
gradient_cyclic,
gradient_hue_direction,
gradient_settings: settings,
transform: appearance.transform,
});
updated_any_layer = true;
@@ -2022,10 +1965,7 @@ fn apply_stops_update(
if let Some(selected_gradient) = &mut data.selected_gradient {
selected_gradient.gradient = new_gradient.clone();
selected_gradient.appearance.gradient_spread = gradient_spread;
selected_gradient.appearance.gradient_space = gradient_space;
selected_gradient.appearance.gradient_cyclic = gradient_cyclic;
selected_gradient.appearance.gradient_hue_direction = gradient_hue_direction;
selected_gradient.appearance.settings = settings;
}
// 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::color::SRGBA8;
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};
/// 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 CYCLIC_SETTINGS: GradientSettings = GradientSettings {
spread: GradientSpread::Pad,
cyclic: true,
space: GradientSpace::OkLab,
hue_direction: GradientHueDirection::Shorter,
interpolation: GradientInterpolation::Linear,
};
#[test]
fn cyclic_adds_a_wrap_diamond_owned_by_the_last_stop() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
// The elided cyclic stops sit at 0 and 0.5, so the wrapped interval spans the other half and centers its diamond at 0.75
assert_eq!(midpoint_diamonds(&gradient, false, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
assert_eq!(midpoint_diamonds(&gradient, true, UNCROWDED_LINE_LENGTH), vec![(0, 0.25), (1, 0.75)]);
assert_eq!(midpoint_diamonds(&gradient, GradientSettings::default(), UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
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
let mut offset = Gradient::from(vec![Color::BLACK, Color::WHITE]);
offset.set_positions(&[0.25, 0.5]);
offset.set_midpoints(&[0.5, 0.9]);
let diamonds = midpoint_diamonds(&offset, true, UNCROWDED_LINE_LENGTH);
let diamonds = midpoint_diamonds(&offset, CYCLIC_SETTINGS, UNCROWDED_LINE_LENGTH);
assert_eq!(diamonds[1].0, 1);
assert!((diamonds[1].1 - 0.175).abs() < 1e-9, "the late wrap midpoint should land past the boundary, got {}", diamonds[1].1);
// Stops pinned to both ends leave the wrapped interval no width, so it contributes no diamond
let mut spanning = Gradient::from(vec![Color::BLACK, Color::WHITE]);
spanning.set_positions(&[0., 1.]);
assert_eq!(midpoint_diamonds(&spanning, true, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
assert_eq!(midpoint_diamonds(&spanning, CYCLIC_SETTINGS, UNCROWDED_LINE_LENGTH), vec![(0, 0.5)]);
}
#[test]
@@ -2188,7 +2137,7 @@ mod test_gradient {
fn new(stops: Gradient, appearance: super::GradientAppearance) -> Self {
Self {
stops,
gradient_spread: appearance.gradient_spread,
gradient_spread: appearance.settings.spread,
transform: appearance.transform,
}
}
@@ -2878,7 +2827,7 @@ mod test_gradient {
#[tokio::test]
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();
editor.new_document().await;
@@ -2896,7 +2845,7 @@ mod test_gradient {
// 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;
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),
"the spread should be written into the gradient value's ramp"
);

View File

@@ -3,7 +3,7 @@
import { preventEscapeClosingParentFloatingMenu } from "/src/components/layout/FloatingMenu.svelte";
import LayoutCol from "/src/components/layout/LayoutCol.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_RIGHT = 2;
@@ -14,6 +14,7 @@
export let trackStartCSS: string;
export let trackEndCSS: string;
export let trackCyclic = false;
export let trackInterpolation: GradientInterpolation = "Linear";
export let markers: SpectrumMarker[];
export let activeMarkerIndex: number | undefined = 0;
export let activeMarkerIsMidpoint = false;
@@ -363,8 +364,9 @@
// 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.
function diamondPositions(markers: SpectrumMarker[], showMidpoints: boolean, trackCyclic: boolean): number[] {
if (!showMidpoints || markers.length < 2) return [];
function diamondPositions(markers: SpectrumMarker[], showMidpoints: boolean, trackCyclic: boolean, trackInterpolation: GradientInterpolation): number[] {
// 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));
// The wrapped interval's diamond may land on either side of the 1|0 boundary
@@ -377,7 +379,7 @@
return positions;
}
$: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic);
$: midpointPositions = diamondPositions(markers, showMidpoints, trackCyclic, trackInterpolation);
onMount(() => {
document.addEventListener("keydown", deleteShortcut);

View File

@@ -614,9 +614,9 @@ tagged_value! {
GradientForm(vector::style::GradientForm),
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
GradientSpread(vector::style::GradientSpread),
#[serde(alias = "GradientInterpolation")] // TODO: Eventually remove this document upgrade code
GradientSpace(vector::style::GradientSpace),
GradientHueDirection(vector::style::GradientHueDirection),
GradientInterpolation(vector::style::GradientInterpolation),
ReferencePoint(vector::ReferencePoint),
CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation),

View File

@@ -1,4 +1,4 @@
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, lane_gradient_settings, spread_adjusted_samples, transform_is_invertible};
use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::Color;
use core_types::attribute::Transform;
@@ -14,7 +14,7 @@ use graphic_types::vector_types::markers::{
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write;
use vector_types::Gradient;
use vector_types::gradient::{GradientHueDirection, GradientSpace, GradientSpread};
use vector_types::gradient::GradientSpread;
#[derive(Copy, Clone, PartialEq)]
pub enum PaintTarget {
@@ -112,20 +112,9 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
let Some(stops) = source.element(0) else { return 0 };
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(0);
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0);
let gradient_space: GradientSpace = source.attr::<GradientSpaceAttr>(0);
let gradient_hue_direction: GradientHueDirection = source.attr::<GradientHueDirectionAttr>(0);
let gradient_cyclic: bool = source.attr::<GradientCyclicAttr>(0);
let settings = lane_gradient_settings(source, 0);
let (samples, _) = spread_adjusted_samples(
stops,
gradient_spread,
gradient_form,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
ClearGuardPlacement::SvgStopOrder,
);
let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
for (position, color, original_midpoint) in samples {
stop.push_str("<stop");
@@ -164,10 +153,10 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
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()
} else {
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name())
format!(r#" spreadMethod="{}""#, settings.spread.svg_name())
};
let gradient_id = generate_uuid();

View File

@@ -26,7 +26,8 @@ use graphene_resource::Resource;
use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientHueDirection, GradientSpace};
use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientSettings};
use graphic_types::vector_types::markers::GradientInterpolation as GradientInterpolationAttr;
use graphic_types::vector_types::markers::{
GradientCyclic as GradientCyclicAttr, GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
};
@@ -424,17 +425,9 @@ pub(crate) enum ClearGuardPlacement {
/// 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
/// gradient's span still starts at zero, since its sampling distance never goes below the center.
pub(crate) fn spread_adjusted_samples(
gradient: &Gradient,
gradient_spread: GradientSpread,
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 {
pub(crate) fn spread_adjusted_samples(gradient: &Gradient, settings: GradientSettings, gradient_form: GradientForm, guards: ClearGuardPlacement) -> (GradientSamples, (f64, f64)) {
let samples = gradient.interpolated_samples(settings);
if settings.spread != GradientSpread::Clear {
return (samples, (0., 1.));
}
@@ -512,25 +505,25 @@ fn peniko_extend(gradient_spread: GradientSpread) -> peniko::Extend {
}
}
/// The gradient's whole-ramp settings from its lane attributes.
pub(crate) fn lane_gradient_settings<S: LaneSource<Element = Gradient>>(source: &S, index: usize) -> GradientSettings {
GradientSettings {
spread: source.attr::<GradientSpreadAttr>(index),
cyclic: source.attr::<GradientCyclicAttr>(index),
space: source.attr::<GradientSpaceAttr>(index),
hue_direction: source.attr::<GradientHueDirectionAttr>(index),
interpolation: source.attr::<GradientInterpolationAttr>(index),
}
}
fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?;
let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0);
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0);
let gradient_space: GradientSpace = gradient_list.attr::<GradientSpaceAttr>(0);
let gradient_hue_direction: GradientHueDirection = gradient_list.attr::<GradientHueDirectionAttr>(0);
let gradient_cyclic: bool = gradient_list.attr::<GradientCyclicAttr>(0);
let settings = lane_gradient_settings(gradient_list, 0);
let (samples, span) = spread_adjusted_samples(
stops,
gradient_spread,
gradient_form,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
ClearGuardPlacement::VelloRampTexels,
);
let (samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);
let peniko_stops = peniko_color_stops(&samples);
@@ -552,7 +545,7 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
}
.into(),
},
extend: peniko_extend(gradient_spread),
extend: peniko_extend(settings.spread),
stops: peniko_stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()
@@ -2443,11 +2436,8 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
let blend_mode: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
let settings = lane_gradient_settings(source, index);
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
let gradient_space: GradientSpace = source.attr::<GradientSpaceAttr>(index);
let gradient_hue_direction: GradientHueDirection = source.attr::<GradientHueDirectionAttr>(index);
let gradient_cyclic: bool = source.attr::<GradientCyclicAttr>(index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect {
@@ -2463,15 +2453,7 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
}
let (samples, _) = spread_adjusted_samples(
gradient,
gradient_spread,
gradient_form,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
ClearGuardPlacement::SvgStopOrder,
);
let (samples, _) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
let mut stop_string = String::new();
for (position, color, original_midpoint) in samples {
@@ -2495,10 +2477,10 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
};
let gradient_id = generate_uuid();
let spread_method_attribute = if matches!(gradient_spread, GradientSpread::Pad | GradientSpread::Clear) {
let spread_method_attribute = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) {
String::new()
} 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
@@ -2540,11 +2522,8 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
for index in 0..source.lane_count() {
let Some(gradient) = source.element(index) else { continue };
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
let settings = lane_gradient_settings(source, index);
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
let gradient_space: GradientSpace = source.attr::<GradientSpaceAttr>(index);
let gradient_hue_direction: GradientHueDirection = source.attr::<GradientHueDirectionAttr>(index);
let gradient_cyclic: bool = source.attr::<GradientCyclicAttr>(index);
let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
@@ -2554,18 +2533,10 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
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 (samples, span) = spread_adjusted_samples(
gradient,
gradient_spread,
gradient_form,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
ClearGuardPlacement::VelloRampTexels,
);
let (samples, span) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);
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.
// For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies.
@@ -3369,30 +3340,39 @@ mod group_walk_tests {
#[cfg(test)]
mod spread_tests {
use super::*;
use graphic_types::vector_types::gradient::{GradientHueDirection, GradientInterpolation, GradientSpace};
#[test]
fn spread_adjusted_samples_wraps_clear_in_transparent_guards() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
let (samples, span) = spread_adjusted_samples(
&gradient,
GradientSpread::Repeat,
GradientSettings {
spread: GradientSpread::Repeat,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder,
);
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
let (samples, span) = spread_adjusted_samples(
&gradient,
GradientSpread::Clear,
GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder,
);
assert_eq!(span, (0., 1.));
@@ -3405,11 +3385,12 @@ mod spread_tests {
let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.);
let (samples, span) = spread_adjusted_samples(
&gradient,
GradientSpread::Clear,
GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::VelloRampTexels,
);
assert_eq!(
@@ -3426,11 +3407,12 @@ mod spread_tests {
// A radial keeps its stops and span anchored at zero, with no guard below the center
let (samples, span) = spread_adjusted_samples(
&gradient,
GradientSpread::Clear,
GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Radial,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::VelloRampTexels,
);
assert_eq!(span.0, 0.);
@@ -3442,11 +3424,12 @@ mod spread_tests {
fn spread_adjusted_samples_keeps_a_stopless_clear_gradient_black_inside_the_range() {
let (samples, _) = spread_adjusted_samples(
&Gradient::from(Vec::new()),
GradientSpread::Clear,
GradientSettings {
spread: GradientSpread::Clear,
space: GradientSpace::RgbGamma,
..Default::default()
},
GradientForm::Linear,
false,
GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder,
);
let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect();

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ pub mod vector;
// Re-export commonly used types at the crate root
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 markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_FORM, ATTR_GRADIENT_SPREAD};
pub use math::{QuadExt, RectExt};
pub use subpath::Subpath;

View File

@@ -13,6 +13,8 @@ core_types::attribute! {
/// Gradient's `bool` (implicit default `false`) for treating the stop list as a cycle, where a wrapped interval
/// interpolates from the last stop through the 1|0 boundary back to the first.
pub GradientCyclic("gradient_cyclic"): bool;
/// Gradient's `GradientInterpolation` (`Stepped`, `Linear`, or `Smooth`), how the color progresses across each interval.
pub GradientInterpolation("gradient_interpolation"): crate::gradient::GradientInterpolation;
/// Gradient's shape (`Linear` or `Radial`).
pub GradientForm("gradient_form"): crate::gradient::GradientForm;
/// Optional `Vector` that overrides the item's own geometry for click-target generation.
@@ -29,6 +31,7 @@ core_types::named_value! {
for crate::gradient::GradientForm;
for crate::gradient::GradientSpace;
for crate::gradient::GradientHueDirection;
for crate::gradient::GradientInterpolation;
}
pub const ATTR_GRADIENT_SPREAD: &str = GradientSpread::NAME;
@@ -36,6 +39,7 @@ pub const ATTR_GRADIENT_FORM: &str = GradientForm::NAME;
pub const ATTR_GRADIENT_CYCLIC: &str = GradientCyclic::NAME;
pub const ATTR_GRADIENT_SPACE: &str = GradientSpace::NAME;
pub const ATTR_GRADIENT_HUE_DIRECTION: &str = GradientHueDirection::NAME;
pub const ATTR_GRADIENT_INTERPOLATION: &str = GradientInterpolation::NAME;
pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME;
#[cfg(test)]

View File

@@ -76,7 +76,7 @@ impl FillChoice<SRGBA8> {
let hex = srgba.to_rgba_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
}
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 {
self.dash_offset = dash_offset;
self

View File

@@ -12,7 +12,8 @@ use rand::{Rng, SeedableRng};
use std::ops::{Add, Mul, Rem, Sub};
use vector_types::Gradient;
use vector_types::markers::{
GradientCyclic as GradientCyclicAttr, GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
GradientCyclic as GradientCyclicAttr, GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientInterpolation as GradientInterpolationAttr,
GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
};
/// The struct that stores the context for the maths parser.
@@ -1228,6 +1229,12 @@ fn gradient_hue_direction(_: impl Ctx, gradient: Gradient, gradient_hue_directio
(gradient, Attr(gradient_hue_direction))
}
/// Sets how the color progresses across each interval between gradient stops: stepped, linear, or smoothstep.
#[node_macro::node(category("Gradient"))]
fn gradient_interpolation(_: impl Ctx, gradient: Gradient, gradient_interpolation: vector_types::GradientInterpolation) -> (Gradient, Attr<GradientInterpolationAttr>) {
(gradient, Attr(gradient_interpolation))
}
/// Sets the position of each of a gradient's stops, a factor from 0 to 1 along the gradient.
///
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each stop to its default evenly spaced position.
@@ -1263,11 +1270,14 @@ fn sample_gradient(
return Err(GraphError::past_end().into());
}
let gradient_spread = gradient.lane(0).attr::<GradientSpreadAttr>();
let gradient_space = gradient.lane(0).attr::<GradientSpaceAttr>();
let gradient_hue_direction = gradient.lane(0).attr::<GradientHueDirectionAttr>();
let gradient_cyclic = gradient.lane(0).attr::<GradientCyclicAttr>();
Ok(gradient.element_ref(0).evaluate(position, gradient_spread, gradient_cyclic, gradient_space, gradient_hue_direction))
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<GradientSpreadAttr>(),
cyclic: gradient.lane(0).attr::<GradientCyclicAttr>(),
space: gradient.lane(0).attr::<GradientSpaceAttr>(),
hue_direction: gradient.lane(0).attr::<GradientHueDirectionAttr>(),
interpolation: gradient.lane(0).attr::<GradientInterpolationAttr>(),
};
Ok(gradient.element_ref(0).evaluate(position, settings))
}
/// Constructs a footprint value which may be set to any transformation of a unit square describing a render area, and a render resolution at least 1x1 integer pixels.

View File

@@ -44,10 +44,10 @@ mod blend_std {
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.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 over_color = self.evaluate(position, Default::default(), false, Default::default(), Default::default());
let under_color = under.evaluate(position, Default::default(), false, Default::default(), Default::default());
let color = blend_fn(over_color, under_color);
let color = blend_fn(over_evaluator.evaluate(position), under_evaluator.evaluate(position));
GradientStop { position, midpoint: 0.5, color }
});

View File

@@ -23,16 +23,19 @@ fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash +
if gradient.is_empty() {
return image;
}
let gradient_spread = gradient.lane(0).attr::<vector_types::markers::GradientSpread>();
let gradient_space = gradient.lane(0).attr::<vector_types::markers::GradientSpace>();
let gradient_hue_direction = gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>();
let gradient_cyclic = gradient.lane(0).attr::<vector_types::markers::GradientCyclic>();
let gradient = gradient.element_ref(0);
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
cyclic: gradient.lane(0).attr::<vector_types::markers::GradientCyclic>(),
space: gradient.lane(0).attr::<vector_types::markers::GradientSpace>(),
hue_direction: gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>(),
interpolation: gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(),
};
let evaluator = gradient.element_ref(0).evaluator(settings);
image.adjust(|color| {
let intensity = color.luminance_rec_709();
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

View File

@@ -45,17 +45,7 @@ use vector_types::vector::{PointDomain, RegionDomain};
/// The gradient color for one assign-colors position, replaying the
/// randomized draws up to it.
fn assign_color_at(
gradient: &Gradient,
gradient_cyclic: bool,
gradient_space: vector_types::GradientSpace,
gradient_hue_direction: vector_types::GradientHueDirection,
position: usize,
length: usize,
randomize: bool,
seed: SeedValue,
repeat_every: u32,
) -> Color {
fn assign_color_at(gradient: &Gradient, settings: vector_types::GradientSettings, position: usize, length: usize, randomize: bool, seed: SeedValue, repeat_every: u32) -> Color {
let factor = match randomize {
true => {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -71,7 +61,7 @@ fn assign_color_at(
_ => position as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
};
gradient.evaluate(factor, Default::default(), gradient_cyclic, gradient_space, gradient_hue_direction)
gradient.evaluate(factor, settings)
}
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
@@ -114,30 +104,24 @@ fn assign_colors<'e>(
if gradient.is_empty() {
return Ok((content.lane(lane).map_element(element), Attr(existing_fill), Attr(existing_stroke)));
}
let gradient_cyclic = gradient.lane(0).attr::<vector_types::markers::GradientCyclic>();
let gradient_space = gradient.lane(0).attr::<vector_types::markers::GradientSpace>();
let gradient_hue_direction = gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>();
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
cyclic: gradient.lane(0).attr::<vector_types::markers::GradientCyclic>(),
space: gradient.lane(0).attr::<vector_types::markers::GradientSpace>(),
hue_direction: gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>(),
interpolation: gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(),
};
let gradient_element = gradient.element_ref(0);
let reversed;
let gradient_element = match reverse {
true => {
reversed = gradient_element.reversed();
reversed = gradient_element.reversed(settings.cyclic);
&reversed
}
false => gradient_element,
};
let color = assign_color_at(
gradient_element,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
lane,
content.len(),
randomize,
seed,
repeat_every,
);
let color = assign_color_at(gradient_element, settings, lane, content.len(), randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list();
let parked = park_paint(ctx.arena(), paint)?;
@@ -195,14 +179,18 @@ fn assign_colors_graphic<'e>(
if gradient.is_empty() {
return Ok(content.lane(lane).map_element(original.clone()));
}
let gradient_cyclic = gradient.lane(0).attr::<vector_types::markers::GradientCyclic>();
let gradient_space = gradient.lane(0).attr::<vector_types::markers::GradientSpace>();
let gradient_hue_direction = gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>();
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
cyclic: gradient.lane(0).attr::<vector_types::markers::GradientCyclic>(),
space: gradient.lane(0).attr::<vector_types::markers::GradientSpace>(),
hue_direction: gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>(),
interpolation: gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(),
};
let gradient_element = gradient.element_ref(0);
let reversed;
let gradient_element = match reverse {
true => {
reversed = gradient_element.reversed();
reversed = gradient_element.reversed(settings.cyclic);
&reversed
}
false => gradient_element,
@@ -243,17 +231,7 @@ fn assign_colors_graphic<'e>(
Some(mut rows) => {
for row in 0..rows.len() {
let has_stroke = rows.element(row).is_some_and(|vector| vector.stroke.is_some());
let color = assign_color_at(
gradient_element,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
position + row,
length,
randomize,
seed,
repeat_every,
);
let color = assign_color_at(gradient_element, settings, position + row, length, randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list();
if fill {
set_paint_attribute_at(&mut rows, row, ATTR_FILL, paint.clone());