Add the "Perceptual" and "Classic" families of gradient spaces with OkLab as the new default (#4415)

* Add an OkLab gradient interpolation space and make it the new default

* Fix the Properties panel fill row resetting the gradient interpolation space to the default

* Add OkLch, Lab, LCh, and HSL gradient interpolation spaces

* Add a hue direction attribute and picker choice for polar gradient interpolation spaces

* Add an HSV gradient interpolation space

* Rename the sRGB Linear and sRGB Gamma gradient spaces to RGB Linear and RGB Gamma

* Divide the gradient interpolation dropdown between absolute and relative color spaces

* Rename the OkLch gradient interpolation space to OkLCh for channel-notation capitalization

* Shorten the color picker popover's hue direction row label to Arc

* Call the Gradient Interpolation node's parameter Space and extend the picker tooltip title to match

* Rename the gradient interpolation attribute and type family to gradient space

* Rename the gradient tool's gradient space transform to gradient to viewport transform

* Add serde aliases and a node replacement covering the gradient space renames

* Give the gradient space choices artist-facing labels and reorder the dropdown sections

* Add a Gradient Hue Direction node and its attribute read node

* Say interpolate instead of blend for gradient stop color traversal in docs and tooltips

* Label the polar perceptual gradient spaces as Perceptual Hue and group the dropdown sections by geometry

* Add a migration alias covering the gradient space attribute reader rename

* Carry the gradient hue direction attribute through boolean operations

* Register GradientHueDirection wire types in the node registry
This commit is contained in:
Keavon Chambers
2026-09-14 12:58:40 +02:00
committed by Dennis Kobert
parent 11dfc27645
commit a35143586a
29 changed files with 886 additions and 410 deletions
Generated
+1
View File
@@ -6601,6 +6601,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"bytemuck", "bytemuck",
"color",
"core-types", "core-types",
"dyn-any", "dyn-any",
"fixedbitset", "fixedbitset",
@@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate}; use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_std::vector::style::{FillChoice, GradientInterpolation, GradientSpread}; use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientSpace, GradientSpread};
/// Identifies which RGB channel a numeric input change targets. /// Identifies which RGB channel a numeric input change targets.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -49,8 +49,10 @@ pub enum ColorPickerMessage {
GradientUpdate { update: SpectrumInputUpdate }, GradientUpdate { update: SpectrumInputUpdate },
/// Gradient spread choice from the gradient "Ends" selection. /// Gradient spread choice from the gradient "Ends" selection.
SetGradientSpread { gradient_spread: GradientSpread }, SetGradientSpread { gradient_spread: GradientSpread },
/// Gradient interpolation choice: the color space the stops blend in, from the "Space" dropdown. /// Gradient space choice: the color space the stops interpolate in, from the "Space" dropdown.
SetGradientInterpolation { gradient_interpolation: GradientInterpolation }, 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 },
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent). /// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
StartTransaction, StartTransaction,
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::core_types::misc::parse_css_color; use graphene_std::core_types::misc::parse_css_color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientInterpolation, GradientRamp, GradientSpread, GradientStops}; use graphene_std::vector::style::{FillChoice, Gradient, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops};
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops). /// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
const MIN_MIDPOINT: f64 = 0.01; const MIN_MIDPOINT: f64 = 0.01;
@@ -30,7 +30,8 @@ pub struct ColorPickerMessageHandler {
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color. // When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
gradient: Option<Gradient>, gradient: Option<Gradient>,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
active_marker_index: Option<u32>, active_marker_index: Option<u32>,
active_marker_is_midpoint: bool, active_marker_is_midpoint: bool,
@@ -53,7 +54,8 @@ impl Default for ColorPickerMessageHandler {
old_is_none: true, old_is_none: true,
gradient: None, gradient: None,
gradient_spread: GradientSpread::default(), gradient_spread: GradientSpread::default(),
gradient_interpolation: GradientInterpolation::default(), gradient_space: GradientSpace::default(),
gradient_hue_direction: GradientHueDirection::default(),
active_marker_index: None, active_marker_index: None,
active_marker_is_midpoint: false, active_marker_is_midpoint: false,
allow_none: true, allow_none: true,
@@ -75,14 +77,16 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.set_new_hsva(0., 0., 0., 1., true); self.set_new_hsva(0., 0., 0., 1., true);
self.gradient = None; self.gradient = None;
self.gradient_spread = GradientSpread::default(); self.gradient_spread = GradientSpread::default();
self.gradient_interpolation = GradientInterpolation::default(); self.gradient_space = GradientSpace::default();
self.gradient_hue_direction = GradientHueDirection::default();
self.active_marker_index = None; self.active_marker_index = None;
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
} }
FillChoice::Solid(color) => { FillChoice::Solid(color) => {
self.gradient = None; self.gradient = None;
self.gradient_spread = GradientSpread::default(); self.gradient_spread = GradientSpread::default();
self.gradient_interpolation = GradientInterpolation::default(); self.gradient_space = GradientSpace::default();
self.gradient_hue_direction = GradientHueDirection::default();
self.active_marker_index = None; self.active_marker_index = None;
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
self.adopt_color(color); self.adopt_color(color);
@@ -91,7 +95,8 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.active_marker_index = Some(0); self.active_marker_index = Some(0);
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
self.gradient_spread = ramp.gradient_spread; self.gradient_spread = ramp.gradient_spread;
self.gradient_interpolation = ramp.gradient_interpolation; self.gradient_space = ramp.gradient_space;
self.gradient_hue_direction = ramp.gradient_hue_direction;
let gradient = Gradient::from(ramp); let gradient = Gradient::from(ramp);
let first_color = gradient.color(0).unwrap_or(Color::BLACK); let first_color = gradient.color(0).unwrap_or(Color::BLACK);
self.gradient = Some(gradient); self.gradient = Some(gradient);
@@ -204,20 +209,36 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp {
gradient_spread, gradient_spread,
gradient_interpolation: self.gradient_interpolation, gradient_space: self.gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient) ..GradientRamp::from(gradient)
}), }),
}); });
self.send_layouts(responses); self.send_layouts(responses);
} }
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => { ColorPickerMessage::SetGradientSpace { gradient_space } => {
let Some(gradient) = &self.gradient else { return }; let Some(gradient) = &self.gradient else { return };
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction); responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_interpolation = gradient_interpolation; self.gradient_space = gradient_space;
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread, gradient_spread: self.gradient_spread,
gradient_interpolation, gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
});
self.send_layouts(responses);
}
ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction } => {
let Some(gradient) = &self.gradient else { return };
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_hue_direction,
..GradientRamp::from(gradient) ..GradientRamp::from(gradient)
}), }),
}); });
@@ -309,7 +330,8 @@ impl ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread, gradient_spread: self.gradient_spread,
gradient_interpolation: self.gradient_interpolation, gradient_space: self.gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(&*gradient) ..GradientRamp::from(&*gradient)
}), }),
}); });
@@ -368,7 +390,7 @@ impl ColorPickerMessageHandler {
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT)); gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
} }
SpectrumInputUpdate::InsertMarker { position } => { SpectrumInputUpdate::InsertMarker { position } => {
let new_index = gradient.insert_stop(position, self.gradient_interpolation); let new_index = gradient.insert_stop(position, self.gradient_space, self.gradient_hue_direction);
self.active_marker_index = Some(new_index as u32); self.active_marker_index = Some(new_index as u32);
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
if let Some(color) = gradient.color(new_index) { if let Some(color) = gradient.color(new_index) {
@@ -440,7 +462,8 @@ impl ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp { value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread, gradient_spread: self.gradient_spread,
gradient_interpolation: self.gradient_interpolation, gradient_space: self.gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(&gradient) ..GradientRamp::from(&gradient)
}), }),
}); });
@@ -471,7 +494,8 @@ impl ColorPickerMessageHandler {
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect(); let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
let mut row_widgets = vec![ let mut row_widgets = vec![
SpectrumInput::new(GradientStops::from(gradient)) SpectrumInput::new(GradientStops::from(gradient))
.track_interpolation(self.gradient_interpolation) .track_space(self.gradient_space)
.track_hue_direction(self.gradient_hue_direction)
.markers(markers) .markers(markers)
.active_marker_index(self.active_marker_index) .active_marker_index(self.active_marker_index)
.active_marker_is_midpoint(self.active_marker_is_midpoint) .active_marker_is_midpoint(self.active_marker_is_midpoint)
@@ -654,15 +678,29 @@ impl ColorPickerMessageHandler {
])); ]));
} }
// Gradient interpolation color space (only present when the picker is in gradient mode) // Gradient color space (only present when the picker is in gradient mode)
if self.gradient.is_some() { if self.gradient.is_some() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into()); let entries = MenuListEntry::sections_from_choice_type(|gradient_space| ColorPickerMessage::SetGradientSpace { gradient_space }.into());
groups.push(LayoutGroup::row(vec![ groups.push(LayoutGroup::row(vec![
TextLabel::new("Space").tooltip_label("Gradient Interpolation").tooltip_description(SPACE_DESCRIPTION).widget_instance(), TextLabel::new("Space").tooltip_label("Gradient Space").tooltip_description(SPACE_DESCRIPTION).widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries).selected_index(Some(self.gradient_space as u32)).disabled(self.disabled).widget_instance(),
]));
}
// Gradient hue direction (only present when the chosen space is polar)
if self.gradient.is_some() && self.gradient_space.is_polar() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_hue_direction| ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction }.into());
groups.push(LayoutGroup::row(vec![
TextLabel::new("Arc")
.tooltip_label("Gradient Hue Direction")
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries) DropdownInput::new(entries)
.selected_index(Some(self.gradient_interpolation as u32)) .selected_index(Some(self.gradient_hue_direction as u32))
.disabled(self.disabled) .disabled(self.disabled)
.widget_instance(), .widget_instance(),
])); ]));
@@ -722,7 +760,8 @@ const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color
const VALUE_DESCRIPTION: &str = "The brightness from black to full color."; const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%)."; const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends."; const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends.";
const SPACE_DESCRIPTION: &str = "The color space where stops blend into their neighbors."; const SPACE_DESCRIPTION: &str = "The color space where stops interpolate toward their neighbors.";
const HUE_DIRECTION_DESCRIPTION: &str = "Which way around the hue wheel the stops interpolate.";
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`). /// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background. /// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
@@ -531,7 +531,7 @@ fn populate_computed_display_fields(layout: &mut Layout) {
color_input.chosen_gradient = color_input.value.to_css_background_image(); color_input.chosen_gradient = color_input.value.to_css_background_image();
} }
Widget::SpectrumInput(spectrum_input) => { Widget::SpectrumInput(spectrum_input) => {
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_interpolation); spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_space, spectrum_input.track_hue_direction);
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string()); spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string()); spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
} }
@@ -7,7 +7,7 @@ use derivative::*;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::transform::ReferencePoint; use graphene_std::transform::ReferencePoint;
use graphene_std::vector::style::{FillChoice, GradientInterpolation, GradientStops}; use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientSpace, GradientStops};
use graphite_proc_macros::WidgetBuilder; use graphite_proc_macros::WidgetBuilder;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -586,9 +586,12 @@ pub struct SpectrumInput {
/// The colored gradient drawn behind the markers (display-only, caller-owned). /// The colored gradient drawn behind the markers (display-only, caller-owned).
#[widget_builder(constructor)] #[widget_builder(constructor)]
pub track: GradientStops<SRGBA8>, pub track: GradientStops<SRGBA8>,
/// The interpolation color space the track's stops blend in, used to compute `track_css`. Not sent to the frontend. /// The color space the track's stops interpolate in, used to compute `track_css`. Not sent to the frontend.
#[serde(skip)] #[serde(skip)]
pub track_interpolation: GradientInterpolation, pub track_space: GradientSpace,
/// 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,
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time. /// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
#[serde(rename = "trackCSS")] #[serde(rename = "trackCSS")]
#[widget_builder(skip)] #[widget_builder(skip)]
@@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
use graphene_std::vector::misc::{ use graphene_std::vector::misc::{
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
}; };
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientRamp, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
use graphene_std::{Artboard, Color, Graphic}; use graphene_std::{Artboard, Color, Graphic};
use std::any::Any; use std::any::Any;
@@ -218,7 +218,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
List<BlendMode>, List<BlendMode>,
List<GradientForm>, List<GradientForm>,
List<GradientSpread>, List<GradientSpread>,
List<GradientInterpolation>, List<GradientSpace>,
List<DashPattern>, List<DashPattern>,
List<BoxCorners>, List<BoxCorners>,
List<StrokeJoin>, List<StrokeJoin>,
@@ -271,7 +271,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
DAffine2, DAffine2,
BlendMode, BlendMode,
GradientForm, GradientForm,
GradientInterpolation, GradientSpace,
GradientSpread, GradientSpread,
DashPattern, DashPattern,
BoxCorners, BoxCorners,
@@ -1011,7 +1011,7 @@ impl_table_item_layout_for_choice_enum!(
BlendMode, BlendMode,
GradientForm, GradientForm,
GradientSpread, GradientSpread,
GradientInterpolation, GradientSpace,
StrokeJoin, StrokeJoin,
StrokeAlign, StrokeAlign,
StrokeCap, StrokeCap,
@@ -1224,7 +1224,7 @@ macro_rules! known_item_types {
BlendMode, BlendMode,
GradientForm, GradientForm,
GradientSpread, GradientSpread,
GradientInterpolation, GradientSpace,
StrokeJoin, StrokeJoin,
StrokeAlign, StrokeAlign,
StrokeCap, StrokeCap,
@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, PointId, VectorModificationType}; use graphene_std::vector::{Gradient, PointId, VectorModificationType};
#[impl_message(Message, DocumentMessage, GraphOperation)] #[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -30,7 +30,8 @@ pub enum GraphOperationMessage {
gradient: Gradient, gradient: Gradient,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2, transform: DAffine2,
}, },
BlendingFillSet { BlendingFillSet {
@@ -62,9 +63,13 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
}, },
GradientInterpolationSet { GradientSpaceSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
},
GradientHueDirectionSet {
layer: LayerNodeIdentifier,
gradient_hue_direction: GradientHueDirection,
}, },
OpacitySet { OpacitySet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
@@ -14,7 +14,7 @@ use graph_craft::document::{NodeId, NodeInput};
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Gradient, GradientForm, GradientInterpolation, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{Gradient, GradientForm, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color}; use graphene_std::{Artboard, Color};
#[derive(ExtractField)] #[derive(ExtractField)]
@@ -50,11 +50,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
gradient, gradient,
gradient_form, gradient_form,
gradient_spread, gradient_spread,
gradient_interpolation, gradient_space,
gradient_hue_direction,
transform, transform,
} => { } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform); modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, gradient_hue_direction, transform);
} }
} }
GraphOperationMessage::BlendingFillSet { layer, fill } => { GraphOperationMessage::BlendingFillSet { layer, fill } => {
@@ -92,9 +93,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.gradient_spread_set(gradient_spread); modify_inputs.gradient_spread_set(gradient_spread);
} }
} }
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => { GraphOperationMessage::GradientSpaceSet { layer, gradient_space } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_interpolation_set(gradient_interpolation); modify_inputs.gradient_space_set(gradient_space);
}
}
GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
} }
} }
GraphOperationMessage::OpacitySet { layer, opacity } => { GraphOperationMessage::OpacitySet { layer, opacity } => {
@@ -489,7 +495,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
let gradient_info = SvgGradientInfo { let gradient_info = SvgGradientInfo {
graphite_stops: extract_graphite_gradient_stops(&svg), graphite_stops: extract_graphite_gradient_stops(&svg),
interpolations: extract_gradient_interpolations(&svg), spaces: extract_gradient_spaces(&svg),
}; };
// Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`. // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
@@ -530,14 +536,14 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
struct SvgGradientInfo { struct SvgGradientInfo {
/// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data. /// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data.
graphite_stops: HashMap<String, Gradient>, graphite_stops: HashMap<String, Gradient>,
/// Interpolation spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property. /// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
interpolations: HashMap<String, GradientInterpolation>, spaces: HashMap<String, GradientSpace>,
} }
/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's /// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's
/// tree does not carry. Only `linearRGB` selects linear interpolation; `auto` and `sRGB` (browsers treat the /// tree does not carry. Only `linearRGB` selects the linear space; `auto` and `sRGB` (browsers treat the
/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value. /// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value.
fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpolation> { fn extract_gradient_spaces(svg: &str) -> HashMap<String, GradientSpace> {
let mut result = HashMap::new(); let mut result = HashMap::new();
// Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default // Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default
@@ -568,9 +574,9 @@ fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpo
} }
if let Some(gradient_id) = node.attribute("id") if let Some(gradient_id) = node.attribute("id")
&& let Some(gradient_interpolation) = resolve_color_interpolation(node, &stylesheet) && let Some(gradient_space) = resolve_color_interpolation(node, &stylesheet)
{ {
result.insert(gradient_id.to_string(), gradient_interpolation); result.insert(gradient_id.to_string(), gradient_space);
} }
} }
@@ -579,15 +585,15 @@ fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpo
/// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each /// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each
/// element's own winning declaration per [`declared_color_interpolation`]'s cascade order. /// element's own winning declaration per [`declared_color_interpolation`]'s cascade order.
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientInterpolation> { fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientSpace> {
let mut next = Some(element); let mut next = Some(element);
while let Some(element) = next { while let Some(element) = next {
match declared_color_interpolation(element, stylesheet) { match declared_color_interpolation(element, stylesheet) {
Some("linearRGB") => return Some(GradientInterpolation::SrgbLinear), Some("linearRGB") => return Some(GradientSpace::RgbLinear),
// `inherit` defers to the ancestors like an undeclared element // `inherit` defers to the ancestors like an undeclared element
Some("inherit") | None => {} Some("inherit") | None => {}
Some(_) => return Some(GradientInterpolation::SrgbGamma), Some(_) => return Some(GradientSpace::RgbGamma),
} }
next = element.parent_element(); next = element.parent_element();
@@ -971,8 +977,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
}; };
let gradient_spread = convert_gradient_spread(linear.spread_method()); let gradient_spread = convert_gradient_spread(linear.spread_method());
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default // SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
let gradient_interpolation = gradient_info.interpolations.get(linear.id()).copied().unwrap_or(GradientInterpolation::SrgbGamma); 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_interpolation, transform); modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, Default::default(), transform);
} }
usvg::Paint::RadialGradient(radial) => { usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform()); let gradient_transform = usvg_transform(radial.transform());
@@ -996,9 +1002,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
} }
}; };
let gradient_spread = convert_gradient_spread(radial.spread_method()); let gradient_spread = convert_gradient_spread(radial.spread_method());
let gradient_interpolation = gradient_info.interpolations.get(radial.id()).copied().unwrap_or(GradientInterpolation::SrgbGamma); 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_interpolation, transform); modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, Default::default(), transform);
} }
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"), usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
}; };
@@ -1019,25 +1025,13 @@ mod tests {
</defs> </defs>
</svg>"##; </svg>"##;
let interpolations = extract_gradient_interpolations(svg); let spaces = extract_gradient_spaces(svg);
assert_eq!(spaces.get("inherited"), Some(&GradientSpace::RgbLinear), "an undeclared gradient should inherit from its ancestors");
assert_eq!(spaces.get("attribute"), Some(&GradientSpace::RgbGamma), "an sRGB declaration should beat the inherited linearRGB");
assert_eq!(spaces.get("styled"), Some(&GradientSpace::RgbLinear), "the inline style should beat the presentation attribute");
assert_eq!( assert_eq!(
interpolations.get("inherited"), spaces.get("auto"),
Some(&GradientInterpolation::SrgbLinear), Some(&GradientSpace::RgbGamma),
"an undeclared gradient should inherit from its ancestors"
);
assert_eq!(
interpolations.get("attribute"),
Some(&GradientInterpolation::SrgbGamma),
"an sRGB declaration should beat the inherited linearRGB"
);
assert_eq!(
interpolations.get("styled"),
Some(&GradientInterpolation::SrgbLinear),
"the inline style should beat the presentation attribute"
);
assert_eq!(
interpolations.get("auto"),
Some(&GradientInterpolation::SrgbGamma),
"auto should mean gamma like browsers treat it, not defer to ancestors" "auto should mean gamma like browsers treat it, not defer to ancestors"
); );
} }
@@ -1059,26 +1053,18 @@ mod tests {
</defs> </defs>
</svg>"##; </svg>"##;
let interpolations = extract_gradient_interpolations(svg); let spaces = extract_gradient_spaces(svg);
assert_eq!(spaces.get("from-type-rule"), Some(&GradientSpace::RgbLinear), "a type rule in a style block should reach the gradient");
assert_eq!( assert_eq!(
interpolations.get("from-type-rule"), spaces.get("from-class-rule"),
Some(&GradientInterpolation::SrgbLinear), Some(&GradientSpace::RgbGamma),
"a type rule in a style block should reach the gradient"
);
assert_eq!(
interpolations.get("from-class-rule"),
Some(&GradientInterpolation::SrgbGamma),
"the class rule should outrank the type rule by specificity" "the class rule should outrank the type rule by specificity"
); );
assert_eq!(interpolations.get("exact"), Some(&GradientInterpolation::SrgbLinear), "the ID rule should outrank the class rule"); assert_eq!(spaces.get("exact"), Some(&GradientSpace::RgbLinear), "the ID rule should outrank the class rule");
assert_eq!(spaces.get("inline-beats-rules"), Some(&GradientSpace::RgbLinear), "the inline style should beat every style block rule");
assert_eq!( assert_eq!(
interpolations.get("inline-beats-rules"), spaces.get("rule-beats-attribute"),
Some(&GradientInterpolation::SrgbLinear), Some(&GradientSpace::RgbGamma),
"the inline style should beat every style block rule"
);
assert_eq!(
interpolations.get("rule-beats-attribute"),
Some(&GradientInterpolation::SrgbGamma),
"a style block rule should beat the presentation attribute" "a style block rule should beat the presentation attribute"
); );
} }
@@ -1092,20 +1078,20 @@ mod tests {
<linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/> <linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/>
</svg>"##; </svg>"##;
let interpolations = extract_gradient_interpolations(svg); let spaces = extract_gradient_spaces(svg);
assert_eq!( assert_eq!(
interpolations.get("last-declaration-wins"), spaces.get("last-declaration-wins"),
Some(&GradientInterpolation::SrgbLinear), Some(&GradientSpace::RgbLinear),
"the last of repeated inline declarations should win" "the last of repeated inline declarations should win"
); );
assert_eq!( assert_eq!(
interpolations.get("important-beats-later"), spaces.get("important-beats-later"),
Some(&GradientInterpolation::SrgbLinear), Some(&GradientSpace::RgbLinear),
"an `!important` declaration should beat a later normal one" "an `!important` declaration should beat a later normal one"
); );
assert_eq!( assert_eq!(
interpolations.get("important-rule-beats-inline"), spaces.get("important-rule-beats-inline"),
Some(&GradientInterpolation::SrgbLinear), Some(&GradientSpace::RgbLinear),
"an `!important` style block rule should beat the inline style" "an `!important` style block rule should beat the inline style"
); );
} }
@@ -1115,7 +1101,7 @@ mod tests {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##; let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##;
assert!( assert!(
extract_gradient_interpolations(svg).is_empty(), extract_gradient_spaces(svg).is_empty(),
"gradients without any declaration should fall back to the caller's gamma default" "gradients without any declaration should fall back to the caller's gamma default"
); );
} }
@@ -19,7 +19,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic}; use graphene_std::{Artboard, Color, Graphic};
@@ -433,7 +433,15 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false); self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
} }
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, gradient_spread: GradientSpread, gradient_interpolation: GradientInterpolation, transform: DAffine2) { pub fn fill_gradient_set(
&mut self,
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2,
) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return; return;
}; };
@@ -442,7 +450,8 @@ impl<'a> ModifyInputsContext<'a> {
let ramp = GradientRamp::from(gradient); let ramp = GradientRamp::from(gradient);
let ramp = GradientRamp { let ramp = GradientRamp {
gradient_spread, gradient_spread,
gradient_interpolation, gradient_space,
gradient_hue_direction,
..ramp ..ramp
}; };
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true); self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
@@ -769,16 +778,29 @@ impl<'a> ModifyInputsContext<'a> {
Some(ramp.clone()) Some(ramp.clone())
} }
/// Set the interpolation on the chain's gradient value, which is where the ramp carries it. Never touches a /// Set the space on the chain's gradient value, which is where the ramp carries it. Never touches a
/// 'Gradient Interpolation' node: that one is a user-authored procedural override, not something the tools manage. /// 'Gradient Space' node: that one is a user-authored procedural override, not something the tools manage.
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) { pub fn gradient_space_set(&mut self, gradient_space: GradientSpace) {
let Some(output_layer) = self.get_output_layer() else { return }; let Some(output_layer) = self.get_output_layer() else { return };
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else { let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
return; return;
}; };
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return }; let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
let ramp = GradientRamp { gradient_interpolation, ..ramp }; let ramp = GradientRamp { gradient_space, ..ramp };
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
}
/// Set the hue direction on the chain's gradient value, which is where the ramp carries it.
pub fn gradient_hue_direction_set(&mut self, gradient_hue_direction: GradientHueDirection) {
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_hue_direction, ..ramp };
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput); let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
} }
@@ -34,7 +34,8 @@ use graphene_std::vector::misc::{
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
}; };
use graphene_std::vector::style::{ use graphene_std::vector::style::{
FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation, FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin,
build_transform_with_y_preservation,
}; };
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::{NodeParameter, ParameterRef}; use graphene_std::{NodeParameter, ParameterRef};
@@ -294,7 +295,8 @@ pub(crate) fn property_from_type(
// ========================= // =========================
Some(x) if id_is::<GradientForm>(x) => enum_choice::<GradientForm>().for_socket(default_info).property_row(), Some(x) if id_is::<GradientForm>(x) => enum_choice::<GradientForm>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientSpread>(x) => enum_choice::<GradientSpread>().for_socket(default_info).property_row(), Some(x) if id_is::<GradientSpread>(x) => enum_choice::<GradientSpread>().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::<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::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(), Some(x) if id_is::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(), Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(), Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
@@ -1390,7 +1392,7 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
// Build the shared spectrum widget (placed on the first non-exposed row) // Build the shared spectrum widget (placed on the first non-exposed row)
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| { let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
SpectrumInput::new(GradientStops::from(&bw_track())) SpectrumInput::new(GradientStops::from(&bw_track()))
.track_interpolation(GradientInterpolation::SrgbGamma) .track_space(GradientSpace::RgbGamma)
.markers(spectrum_markers) .markers(spectrum_markers)
.show_midpoints(false) .show_midpoints(false)
.allow_insert(false) .allow_insert(false)
@@ -1563,7 +1565,7 @@ fn spectrum_slider_row(
let position_to_value = move |position: f64| value_min + position * value_range; let position_to_value = move |position: f64| value_min + position * value_range;
row.push( row.push(
SpectrumInput::new(GradientStops::from(&track)) SpectrumInput::new(GradientStops::from(&track))
.track_interpolation(GradientInterpolation::SrgbGamma) .track_space(GradientSpace::RgbGamma)
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)]) .markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
.show_midpoints(false) .show_midpoints(false)
.allow_insert(false) .allow_insert(false)
@@ -2392,7 +2394,7 @@ fn root_layer_for_chain_node(node_id: NodeId, context: &mut NodePropertiesContex
/// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically. /// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically.
fn gradient_orientation_in_fill_node(node_id: NodeId, gradient_transform: DAffine2, context: &mut NodePropertiesContext) -> Option<bool> { fn gradient_orientation_in_fill_node(node_id: NodeId, gradient_transform: DAffine2, context: &mut NodePropertiesContext) -> Option<bool> {
let layer = root_layer_for_chain_node(node_id, context)?; let layer = root_layer_for_chain_node(node_id, context)?;
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface); let transform = graph_modification_utils::gradient_to_viewport_transform(layer, context.network_interface);
Some(graph_modification_utils::gradient_orientation_rightward(transform * gradient_transform)) Some(graph_modification_utils::gradient_orientation_rightward(transform * gradient_transform))
} }
@@ -2407,6 +2409,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
gradient: Gradient, gradient: Gradient,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2, transform: DAffine2,
/// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire. /// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire.
transform_is_value: bool, transform_is_value: bool,
@@ -2437,6 +2441,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
gradient: gradient.stops, gradient: gradient.stops,
gradient_form: gradient.gradient_form, gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread, gradient_spread: gradient.gradient_spread,
gradient_space: gradient.gradient_space,
gradient_hue_direction: gradient.gradient_hue_direction,
transform: gradient.transform, transform: gradient.transform,
transform_is_value: gradient.transform_is_value, transform_is_value: gradient.transform_is_value,
}, },
@@ -2464,9 +2470,17 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}; };
match &fill { match &fill {
ResolvedFill::Gradient { gradient: stops, gradient_spread, .. } => { ResolvedFill::Gradient {
gradient: stops,
gradient_spread,
gradient_space,
gradient_hue_direction,
..
} => {
let stops = stops.clone(); let stops = stops.clone();
let gradient_spread = *gradient_spread; let gradient_spread = *gradient_spread;
let gradient_space = *gradient_space;
let gradient_hue_direction = *gradient_hue_direction;
let reverse_button = IconButton::new("Reverse", 24) let reverse_button = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops") .tooltip_label("Reverse Stops")
@@ -2475,6 +2489,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
move |_| { move |_| {
TaggedValue::GradientRamp(GradientRamp { TaggedValue::GradientRamp(GradientRamp {
gradient_spread, gradient_spread,
gradient_space,
gradient_hue_direction,
..GradientRamp::from(stops.reversed()) ..GradientRamp::from(stops.reversed())
}) })
}, },
@@ -2496,8 +2512,16 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
FillChoice::<SRGBA8>::None FillChoice::<SRGBA8>::None
} }
} }
ResolvedFill::Gradient { gradient: stops, gradient_spread, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp { ResolvedFill::Gradient {
gradient: stops,
gradient_spread,
gradient_space,
gradient_hue_direction,
..
} => FillChoice::<SRGBA8>::Gradient(GradientRamp {
gradient_spread: *gradient_spread, gradient_spread: *gradient_spread,
gradient_space: *gradient_space,
gradient_hue_direction: *gradient_hue_direction,
..GradientRamp::from(stops) ..GradientRamp::from(stops)
}), }),
ResolvedFill::Other => FillChoice::<SRGBA8>::None, ResolvedFill::Other => FillChoice::<SRGBA8>::None,
@@ -808,8 +808,8 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}"); panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}");
}; };
assert_eq!( assert_eq!(
ramp.gradient_interpolation, ramp.gradient_space,
graphene_std::vector::style::GradientInterpolation::SrgbGamma, graphene_std::vector::style::GradientSpace::RgbGamma,
"a legacy document's ramps should deserialize with the explicit gamma interpolation" "a legacy document's ramps should deserialize with the explicit gamma interpolation"
); );
let stops = graphene_std::vector::Gradient::from(ramp); let stops = graphene_std::vector::Gradient::from(ramp);
@@ -850,8 +850,8 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
}; };
assert_eq!(ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the fill ramp"); assert_eq!(ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the fill ramp");
assert_eq!( assert_eq!(
ramp.gradient_interpolation, ramp.gradient_space,
graphene_std::vector::style::GradientInterpolation::SrgbGamma, graphene_std::vector::style::GradientSpace::RgbGamma,
"a legacy document's ramps should deserialize with the explicit gamma interpolation" "a legacy document's ramps should deserialize with the explicit gamma interpolation"
); );
@@ -861,8 +861,8 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
}; };
assert_eq!(backup_ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the backup ramp"); assert_eq!(backup_ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the backup ramp");
assert_eq!( assert_eq!(
backup_ramp.gradient_interpolation, backup_ramp.gradient_space,
graphene_std::vector::style::GradientInterpolation::SrgbGamma, graphene_std::vector::style::GradientSpace::RgbGamma,
"the backup ramp should carry the explicit gamma interpolation too" "the backup ramp should carry the explicit gamma interpolation too"
); );
@@ -172,6 +172,13 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::graphic::read_gradient_spread_attribute::IDENTIFIER, node: graphene_std::graphic::read_gradient_spread_attribute::IDENTIFIER,
aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode", "graphic_nodes::graphic::ReadSpreadMethodAttributeNode"], aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode", "graphic_nodes::graphic::ReadSpreadMethodAttributeNode"],
}, },
NodeReplacement {
node: graphene_std::graphic::read_gradient_space_attribute::IDENTIFIER,
aliases: &[
"graphic_nodes::graphic::ReadAttributeGradientInterpolationNode",
"graphic_nodes::graphic::ReadGradientInterpolationAttributeNode",
],
},
NodeReplacement { NodeReplacement {
node: graphene_std::list::remove_at_index::IDENTIFIER, node: graphene_std::list::remove_at_index::IDENTIFIER,
aliases: &["graphic_nodes::graphic::OmitElementNode", "graphic_nodes::graphic::RemoveAtIndexNode"], aliases: &["graphic_nodes::graphic::OmitElementNode", "graphic_nodes::graphic::RemoveAtIndexNode"],
@@ -273,6 +280,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::math_nodes::gradient_form::IDENTIFIER, node: graphene_std::math_nodes::gradient_form::IDENTIFIER,
aliases: &["math_nodes::GradientTypeNode"], aliases: &["math_nodes::GradientTypeNode"],
}, },
NodeReplacement {
node: graphene_std::math_nodes::gradient_space::IDENTIFIER,
aliases: &["math_nodes::GradientInterpolationNode"],
},
NodeReplacement { NodeReplacement {
node: graphene_std::math_nodes::gradient_spread::IDENTIFIER, node: graphene_std::math_nodes::gradient_spread::IDENTIFIER,
aliases: &["math_nodes::SpreadMethodNode"], aliases: &["math_nodes::SpreadMethodNode"],
@@ -14,7 +14,7 @@ use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
use graphene_std::vector::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PointId, SegmentId, VectorModificationType}; use graphene_std::vector::{Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, PointId, SegmentId, VectorModificationType};
use graphene_std::{NodeParameter, ParameterRef}; use graphene_std::{NodeParameter, ParameterRef};
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -402,9 +402,14 @@ pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_inte
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread) Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread)
} }
/// The interpolation baked into the 'Gradient Value' node feeding a layer's chain. /// The space baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_interpolation(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientInterpolation> { pub fn get_chain_source_gradient_space(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpace> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_interpolation) Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_space)
}
/// The hue direction baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_hue_direction(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientHueDirection> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_hue_direction)
} }
/// Get the gradient stops of a layer, if any. /// Get the gradient stops of a layer, if any.
@@ -466,7 +471,7 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<Gradient>` /// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<Gradient>`
/// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport /// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport
/// transform with the [0,1]² → bounding-box mapping. /// transform with the [0,1]² → bounding-box mapping.
pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 { pub fn gradient_to_viewport_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
let metadata = network_interface.document_metadata(); let metadata = network_interface.document_metadata();
@@ -762,7 +767,8 @@ pub struct FillNodeGradient {
pub stops: Gradient, pub stops: Gradient,
pub gradient_form: GradientForm, pub gradient_form: GradientForm,
pub gradient_spread: GradientSpread, pub gradient_spread: GradientSpread,
pub gradient_interpolation: GradientInterpolation, pub gradient_space: GradientSpace,
pub gradient_hue_direction: GradientHueDirection,
pub transform: DAffine2, pub transform: DAffine2,
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire. /// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
pub transform_is_value: bool, pub transform_is_value: bool,
@@ -776,7 +782,8 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
return None; return None;
}; };
let gradient_spread = ramp.gradient_spread; let gradient_spread = ramp.gradient_spread;
let gradient_interpolation = ramp.gradient_interpolation; let gradient_space = ramp.gradient_space;
let gradient_hue_direction = ramp.gradient_hue_direction;
let stops = Gradient::from(ramp); let stops = Gradient::from(ramp);
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) { let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientForm(value)) => value, Some(&TaggedValue::GradientForm(value)) => value,
@@ -794,7 +801,8 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
stops, stops,
gradient_form, gradient_form,
gradient_spread, gradient_spread,
gradient_interpolation, gradient_space,
gradient_hue_direction,
transform, transform,
transform_is_value: transform_input.is_some(), transform_is_value: transform_input.is_some(),
}) })
@@ -944,7 +952,8 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
gradient: Gradient::from(ramp), gradient: Gradient::from(ramp),
gradient_form, gradient_form,
gradient_spread: ramp.gradient_spread, gradient_spread: ramp.gradient_spread,
gradient_interpolation: ramp.gradient_interpolation, gradient_space: ramp.gradient_space,
gradient_hue_direction: ramp.gradient_hue_direction,
transform, transform,
}); });
} }
@@ -9,15 +9,15 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{ use crate::messages::tool::common_functionality::graph_modification_utils::{
self, NodeGraphLayer, get_chain_source_gradient_interpolation, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, self, NodeGraphLayer, get_chain_source_gradient_hue_direction, get_chain_source_gradient_space, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops,
gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description,
}; };
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use glam::DMat2; use glam::DMat2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop, build_transform_with_y_preservation}; use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStop, build_transform_with_y_preservation};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct GradientTool { pub struct GradientTool {
@@ -30,7 +30,8 @@ pub struct GradientTool {
pub struct GradientOptions { pub struct GradientOptions {
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
} }
#[impl_message(Message, ToolMessage, Gradient)] #[impl_message(Message, ToolMessage, Gradient)]
@@ -139,8 +140,17 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => { ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
let ramp = GradientRamp::from(&ramp); let ramp = GradientRamp::from(&ramp);
self.options.gradient_spread = ramp.gradient_spread; self.options.gradient_spread = ramp.gradient_spread;
self.options.gradient_interpolation = ramp.gradient_interpolation; self.options.gradient_space = ramp.gradient_space;
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.gradient_spread, ramp.gradient_interpolation); 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_hue_direction,
);
} }
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => { ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
if self.data.color_picker_transaction_open { if self.data.color_picker_transaction_open {
@@ -178,8 +188,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
self.options.gradient_spread = appearance.gradient_spread; self.options.gradient_spread = appearance.gradient_spread;
needs_refresh = true; needs_refresh = true;
} }
if self.options.gradient_interpolation != appearance.gradient_interpolation { if self.options.gradient_space != appearance.gradient_space {
self.options.gradient_interpolation = appearance.gradient_interpolation; self.options.gradient_space = appearance.gradient_space;
needs_refresh = true;
}
if self.options.gradient_hue_direction != appearance.gradient_hue_direction {
self.options.gradient_hue_direction = appearance.gradient_hue_direction;
needs_refresh = true; needs_refresh = true;
} }
} }
@@ -198,7 +212,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
let new_orientation = match (current_layer, &current_gradient) { let new_orientation = match (current_layer, &current_gradient) {
(Some(layer), Some((_gradient, appearance))) => { (Some(layer), Some((_gradient, appearance))) => {
let transform = gradient_space_transform(layer, context.document) * appearance.transform; let transform = gradient_to_viewport_transform(layer, context.document) * appearance.transform;
graph_modification_utils::gradient_orientation_rightward(transform) graph_modification_utils::gradient_orientation_rightward(transform)
} }
_ => true, _ => true,
@@ -262,7 +276,8 @@ impl LayoutHolder for GradientTool {
}); });
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp { let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
gradient_spread: self.options.gradient_spread, gradient_spread: self.options.gradient_spread,
gradient_interpolation: self.options.gradient_interpolation, gradient_space: self.options.gradient_space,
gradient_hue_direction: self.options.gradient_hue_direction,
..GradientRamp::from(&stops_value) ..GradientRamp::from(&stops_value)
})) }))
.allow_none(false) .allow_none(false)
@@ -335,8 +350,8 @@ impl Default for GradientToolFsmState {
} }
/// Computes the transform from gradient space to viewport space. /// Computes the transform from gradient space to viewport space.
fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 { fn gradient_to_viewport_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
graph_modification_utils::gradient_space_transform(layer, &document.network_interface) graph_modification_utils::gradient_to_viewport_transform(layer, &document.network_interface)
} }
/// Viewport positions of the gradient's start (unit param 0) and end (unit param 1) handles. /// Viewport positions of the gradient's start (unit param 0) and end (unit param 1) handles.
@@ -363,7 +378,8 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
GradientAppearance { GradientAppearance {
gradient_form: gradient.gradient_form, gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread, gradient_spread: gradient.gradient_spread,
gradient_interpolation: gradient.gradient_interpolation, gradient_space: gradient.gradient_space,
gradient_hue_direction: gradient.gradient_hue_direction,
transform: gradient.transform, transform: gradient.transform,
}, },
GradientSource::Direct, GradientSource::Direct,
@@ -383,7 +399,8 @@ struct GradientAppearance {
transform: DAffine2, transform: DAffine2,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
} }
/// Resolve the gradient transform, form, and spread by walking the chain feeding the layer. /// Resolve the gradient transform, form, and spread by walking the chain feeding the layer.
@@ -424,7 +441,8 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod
transform: composed_transform, transform: composed_transform,
gradient_form: gradient_form.unwrap_or_default(), gradient_form: gradient_form.unwrap_or_default(),
gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(), gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(),
gradient_interpolation: get_chain_source_gradient_interpolation(layer, network_interface).unwrap_or_default(), gradient_space: get_chain_source_gradient_space(layer, network_interface).unwrap_or_default(),
gradient_hue_direction: get_chain_source_gradient_hue_direction(layer, network_interface).unwrap_or_default(),
} }
} }
@@ -477,7 +495,7 @@ struct SelectedGradient {
layer: Option<LayerNodeIdentifier>, layer: Option<LayerNodeIdentifier>,
dragging: GradientDragTarget, dragging: GradientDragTarget,
/// Transform from the geometry's local gradient space to viewport space. /// Transform from the geometry's local gradient space to viewport space.
gradient_space_transform: DAffine2, gradient_to_viewport_transform: DAffine2,
gradient: Gradient, gradient: Gradient,
appearance: GradientAppearance, appearance: GradientAppearance,
initial_gradient: Gradient, initial_gradient: Gradient,
@@ -526,10 +544,10 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, mouse: DVec2)
impl SelectedGradient { impl SelectedGradient {
pub fn new(gradient: Gradient, appearance: GradientAppearance, source: GradientSource, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self { pub fn new(gradient: Gradient, appearance: GradientAppearance, source: GradientSource, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
let gradient_space_transform = gradient_space_transform(layer, document); let gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
Self { Self {
layer: Some(layer), layer: Some(layer),
gradient_space_transform, gradient_to_viewport_transform,
gradient: gradient.clone(), gradient: gradient.clone(),
appearance, appearance,
dragging: GradientDragTarget::End, dragging: GradientDragTarget::End,
@@ -615,7 +633,7 @@ impl SelectedGradient {
snap_manager.update_indicator(snapped); snap_manager.update_indicator(snapped);
} }
let local_mouse = self.gradient_space_transform.inverse().transform_point2(mouse); let local_mouse = self.gradient_to_viewport_transform.inverse().transform_point2(mouse);
let local_start = self.appearance.transform.transform_point2(DVec2::ZERO); let local_start = self.appearance.transform.transform_point2(DVec2::ZERO);
let local_end = self.appearance.transform.transform_point2(DVec2::X); let local_end = self.appearance.transform.transform_point2(DVec2::X);
@@ -630,7 +648,7 @@ impl SelectedGradient {
self.appearance.transform = create_new_gradient_transform(local_start, local_mouse); self.appearance.transform = create_new_gradient_transform(local_start, local_mouse);
} }
GradientDragTarget::New => { GradientDragTarget::New => {
self.appearance.transform = create_new_gradient_transform(self.gradient_space_transform.inverse().transform_point2(drag_start), local_mouse); self.appearance.transform = create_new_gradient_transform(self.gradient_to_viewport_transform.inverse().transform_point2(drag_start), local_mouse);
} }
GradientDragTarget::Stop(stop) => { GradientDragTarget::Stop(stop) => {
let document_to_viewport = snap_data.document.metadata().document_to_viewport; let document_to_viewport = snap_data.document.metadata().document_to_viewport;
@@ -761,7 +779,8 @@ impl SelectedGradient {
gradient: self.gradient.clone(), gradient: self.gradient.clone(),
gradient_form: self.appearance.gradient_form, gradient_form: self.appearance.gradient_form,
gradient_spread: self.appearance.gradient_spread, gradient_spread: self.appearance.gradient_spread,
gradient_interpolation: self.appearance.gradient_interpolation, gradient_space: self.appearance.gradient_space,
gradient_hue_direction: self.appearance.gradient_hue_direction,
transform: self.appearance.transform, transform: self.appearance.transform,
}); });
} }
@@ -769,7 +788,7 @@ impl SelectedGradient {
} }
fn unit_to_viewport_transform(&self) -> DAffine2 { fn unit_to_viewport_transform(&self) -> DAffine2 {
self.gradient_space_transform * self.appearance.transform self.gradient_to_viewport_transform * self.appearance.transform
} }
fn viewport_handle_positions(&self) -> (DVec2, DVec2) { fn viewport_handle_positions(&self) -> (DVec2, DVec2) {
@@ -800,9 +819,13 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
layer, layer,
gradient_spread: appearance.gradient_spread, gradient_spread: appearance.gradient_spread,
}); });
responses.add(GraphOperationMessage::GradientInterpolationSet { responses.add(GraphOperationMessage::GradientSpaceSet {
layer, layer,
gradient_interpolation: appearance.gradient_interpolation, gradient_space: appearance.gradient_space,
});
responses.add(GraphOperationMessage::GradientHueDirectionSet {
layer,
gradient_hue_direction: appearance.gradient_hue_direction,
}); });
} }
@@ -883,7 +906,7 @@ impl Fsm for GradientToolFsmState {
let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) else { let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) else {
continue; continue;
}; };
let unit_to_viewport = gradient_space_transform(layer, document) * appearance.transform; let unit_to_viewport = gradient_to_viewport_transform(layer, document) * appearance.transform;
let dragging = selected let dragging = selected
.filter(|selected| selected.layer.is_some_and(|selected_layer| selected_layer == layer)) .filter(|selected| selected.layer.is_some_and(|selected_layer| selected_layer == layer))
.map(|selected| selected.dragging); .map(|selected| selected.dragging);
@@ -1065,8 +1088,8 @@ impl Fsm for GradientToolFsmState {
&& let Some(selected_gradient) = tool_data.selected_gradient.as_ref() && let Some(selected_gradient) = tool_data.selected_gradient.as_ref()
&& let Some(layer) = selected_gradient.layer && let Some(layer) = selected_gradient.layer
{ {
// The gradient space transform has be recalculated as the saved transform in SelectedGradient may become stale by panning/zooming during the rendering of the overlay. // The gradient-to-viewport transform has to be recalculated as the saved transform in SelectedGradient may become stale by panning/zooming during the rendering of the overlay.
let transform = gradient_space_transform(layer, document) * selected_gradient.appearance.transform; let transform = gradient_to_viewport_transform(layer, document) * selected_gradient.appearance.transform;
let gradient = &selected_gradient.gradient; let gradient = &selected_gradient.gradient;
if stop_index < gradient.len() { if stop_index < gradient.len() {
let color = gradient.color(stop_index).unwrap_or(Color::BLACK); let color = gradient.color(stop_index).unwrap_or(Color::BLACK);
@@ -1244,7 +1267,7 @@ impl Fsm for GradientToolFsmState {
continue; continue;
}; };
// TODO: This transform is incorrect. I think this is since it is based on the Footprint which has not been updated yet // TODO: This transform is incorrect. I think this is since it is based on the Footprint which has not been updated yet
let unit_to_viewport = gradient_space_transform(layer, document) * appearance.transform; let unit_to_viewport = gradient_to_viewport_transform(layer, document) * appearance.transform;
let mouse = input.mouse.position; let mouse = input.mouse.position;
let (start, end) = gradient_handle_positions(unit_to_viewport); let (start, end) = gradient_handle_positions(unit_to_viewport);
@@ -1254,7 +1277,7 @@ impl Fsm for GradientToolFsmState {
// If click is on the line then insert point // If click is on the line then insert point
if distance < (SELECTION_THRESHOLD * 2.) { if distance < (SELECTION_THRESHOLD * 2.) {
// Try and insert the new stop // Try and insert the new stop
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.gradient_interpolation) { if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.gradient_space, appearance.gradient_hue_direction) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document); let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
@@ -1298,8 +1321,8 @@ impl Fsm for GradientToolFsmState {
let Some((gradient, appearance, source)) = resolve_gradient(layer, &document.network_interface) else { let Some((gradient, appearance, source)) = resolve_gradient(layer, &document.network_interface) else {
continue; continue;
}; };
let gradient_space_transform = gradient_space_transform(layer, document); let gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
let unit_to_viewport = gradient_space_transform * appearance.transform; let unit_to_viewport = gradient_to_viewport_transform * appearance.transform;
let is_gradient_chain = source == GradientSource::Chain; let is_gradient_chain = source == GradientSource::Chain;
let (start, end) = gradient_handle_positions(unit_to_viewport); let (start, end) = gradient_handle_positions(unit_to_viewport);
@@ -1324,7 +1347,7 @@ impl Fsm for GradientToolFsmState {
tool_data.selected_gradient = Some(SelectedGradient { tool_data.selected_gradient = Some(SelectedGradient {
layer: Some(layer), layer: Some(layer),
gradient_space_transform, gradient_to_viewport_transform,
gradient: gradient.clone(), gradient: gradient.clone(),
appearance, appearance,
initial_gradient_transform: appearance.transform, initial_gradient_transform: appearance.transform,
@@ -1368,7 +1391,7 @@ impl Fsm for GradientToolFsmState {
tool_data.selected_gradient = Some(SelectedGradient { tool_data.selected_gradient = Some(SelectedGradient {
layer: Some(layer), layer: Some(layer),
dragging: drag_target, dragging: drag_target,
gradient_space_transform, gradient_to_viewport_transform,
gradient: gradient.clone(), gradient: gradient.clone(),
appearance, appearance,
initial_gradient: gradient.clone(), initial_gradient: gradient.clone(),
@@ -1386,7 +1409,7 @@ impl Fsm for GradientToolFsmState {
tool_data.selected_gradient = Some(SelectedGradient { tool_data.selected_gradient = Some(SelectedGradient {
layer: Some(layer), layer: Some(layer),
dragging: dragging_target, dragging: dragging_target,
gradient_space_transform, gradient_to_viewport_transform,
gradient: gradient.clone(), gradient: gradient.clone(),
appearance, appearance,
initial_gradient: gradient.clone(), initial_gradient: gradient.clone(),
@@ -1404,7 +1427,7 @@ impl Fsm for GradientToolFsmState {
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
let mut new_gradient = gradient.clone(); let mut new_gradient = gradient.clone();
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.gradient_interpolation) { if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.gradient_space, appearance.gradient_hue_direction) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
transaction_started = true; transaction_started = true;
@@ -1483,7 +1506,8 @@ impl Fsm for GradientToolFsmState {
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
gradient_form: tool_options.gradient_form, gradient_form: tool_options.gradient_form,
gradient_spread: tool_options.gradient_spread, gradient_spread: tool_options.gradient_spread,
gradient_interpolation: tool_options.gradient_interpolation, gradient_space: tool_options.gradient_space,
gradient_hue_direction: tool_options.gradient_hue_direction,
}, },
// A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted // A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted
if replaceable_paint_chain(layer, &document.network_interface).is_some() { if replaceable_paint_chain(layer, &document.network_interface).is_some() {
@@ -1524,8 +1548,8 @@ impl Fsm for GradientToolFsmState {
// Recompute the gradient-to-viewport transform fresh each frame so zoom/pan mid-drag works correctly // Recompute the gradient-to-viewport transform fresh each frame so zoom/pan mid-drag works correctly
if let Some(layer) = selected_gradient.layer { if let Some(layer) = selected_gradient.layer {
selected_gradient.gradient_space_transform = gradient_space_transform(layer, document); selected_gradient.gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
selected_gradient.gradient_space_transform.translation += tool_data.auto_pan_shift; selected_gradient.gradient_to_viewport_transform.translation += tool_data.auto_pan_shift;
} }
// Convert drag_start from document space to effective viewport space // Convert drag_start from document space to effective viewport space
@@ -1715,10 +1739,10 @@ impl Fsm for GradientToolFsmState {
} }
} }
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, gradient_interpolation: GradientInterpolation) -> Option<usize> { fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Option<usize> {
let (start, end) = gradient_handle_positions(unit_to_viewport); let (start, end) = gradient_handle_positions(unit_to_viewport);
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end); let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_interpolation)) (0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_space, gradient_hue_direction))
} }
fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) { fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) {
@@ -1739,8 +1763,8 @@ fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> Gradi
let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) else { let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) else {
continue; continue;
}; };
let gradient_space_transform = gradient_space_transform(layer, document); let gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
let unit_to_viewport = gradient_space_transform * appearance.transform; let unit_to_viewport = gradient_to_viewport_transform * appearance.transform;
let (start, end) = gradient_handle_positions(unit_to_viewport); let (start, end) = gradient_handle_positions(unit_to_viewport);
let line_length = start.distance(end); let line_length = start.distance(end);
@@ -1842,7 +1866,8 @@ fn apply_gradient_update(
gradient, gradient,
gradient_form: appearance.gradient_form, gradient_form: appearance.gradient_form,
gradient_spread: appearance.gradient_spread, gradient_spread: appearance.gradient_spread,
gradient_interpolation: appearance.gradient_interpolation, gradient_space: appearance.gradient_space,
gradient_hue_direction: appearance.gradient_hue_direction,
transform: appearance.transform, transform: appearance.transform,
}); });
} }
@@ -1872,7 +1897,8 @@ fn apply_stops_update(
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
new_gradient: Gradient, new_gradient: Gradient,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
) { ) {
let selected_layers: Vec<_> = context let selected_layers: Vec<_> = context
.document .document
@@ -1890,7 +1916,8 @@ fn apply_stops_update(
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() { if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() }); responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
responses.add(GraphOperationMessage::GradientSpreadSet { layer, gradient_spread }); responses.add(GraphOperationMessage::GradientSpreadSet { layer, gradient_spread });
responses.add(GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation }); responses.add(GraphOperationMessage::GradientSpaceSet { layer, gradient_space });
responses.add(GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction });
updated_any_layer = true; updated_any_layer = true;
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) { } else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
responses.add(GraphOperationMessage::FillGradientSet { responses.add(GraphOperationMessage::FillGradientSet {
@@ -1898,7 +1925,8 @@ fn apply_stops_update(
gradient: new_gradient.clone(), gradient: new_gradient.clone(),
gradient_form: appearance.gradient_form, gradient_form: appearance.gradient_form,
gradient_spread, gradient_spread,
gradient_interpolation, gradient_space,
gradient_hue_direction,
transform: appearance.transform, transform: appearance.transform,
}); });
updated_any_layer = true; updated_any_layer = true;
@@ -1908,7 +1936,8 @@ fn apply_stops_update(
if let Some(selected_gradient) = &mut data.selected_gradient { if let Some(selected_gradient) = &mut data.selected_gradient {
selected_gradient.gradient = new_gradient.clone(); selected_gradient.gradient = new_gradient.clone();
selected_gradient.appearance.gradient_spread = gradient_spread; selected_gradient.appearance.gradient_spread = gradient_spread;
selected_gradient.appearance.gradient_interpolation = gradient_interpolation; selected_gradient.appearance.gradient_space = gradient_space;
selected_gradient.appearance.gradient_hue_direction = gradient_hue_direction;
} }
// When no selected layer had a gradient to update, the user is editing the tool's default gradient instead. // When no selected layer had a gradient to update, the user is editing the tool's default gradient instead.
@@ -2008,7 +2037,7 @@ mod test_gradient {
use graphene_std::vector::style::{GradientForm, GradientSpread, build_transform_with_y_preservation}; use graphene_std::vector::style::{GradientForm, GradientSpread, build_transform_with_y_preservation};
use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill}; use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill};
use super::gradient_space_transform; use super::gradient_to_viewport_transform;
struct ResolvedGradient { struct ResolvedGradient {
stops: Gradient, stops: Gradient,
@@ -2065,7 +2094,7 @@ mod test_gradient {
transform: local_transform, transform: local_transform,
}; };
let transform = gradient_space_transform(layer, document); let transform = gradient_to_viewport_transform(layer, document);
Some((gradient, transform)) Some((gradient, transform))
}) })
.collect() .collect()
@@ -2082,7 +2111,7 @@ mod test_gradient {
let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface)?; let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface)?;
let gradient = ResolvedGradient::new(gradient, appearance); let gradient = ResolvedGradient::new(gradient, appearance);
let transform = gradient_space_transform(layer, document); let transform = gradient_to_viewport_transform(layer, document);
Some((gradient, transform)) Some((gradient, transform))
}) })
.collect() .collect()
@@ -2770,11 +2799,11 @@ mod test_gradient {
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
let document = editor.active_document(); let document = editor.active_document();
let space_transform = gradient_space_transform(layer, document); let to_viewport_transform = gradient_to_viewport_transform(layer, document);
let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface).unwrap(); let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface).unwrap();
let gradient = ResolvedGradient::new(gradient, appearance); let gradient = ResolvedGradient::new(gradient, appearance);
let viewport_start = space_transform.transform_point2(gradient.start()); let viewport_start = to_viewport_transform.transform_point2(gradient.start());
let viewport_end = space_transform.transform_point2(gradient.end()); let viewport_end = to_viewport_transform.transform_point2(gradient.end());
// Drag target of the end point, move 80px down // Drag target of the end point, move 80px down
let new_viewport_end = viewport_end + DVec2::new(0., 80.); let new_viewport_end = viewport_end + DVec2::new(0., 80.);
@@ -2797,9 +2826,9 @@ mod test_gradient {
let document = editor.active_document(); let document = editor.active_document();
let (updated, appearance, _) = super::resolve_gradient(layer, &document.network_interface).expect("Gradient should exist after drag"); let (updated, appearance, _) = super::resolve_gradient(layer, &document.network_interface).expect("Gradient should exist after drag");
let updated = ResolvedGradient::new(updated, appearance); let updated = ResolvedGradient::new(updated, appearance);
let updated_space_transform = gradient_space_transform(layer, document); let updated_to_viewport_transform = gradient_to_viewport_transform(layer, document);
let updated_viewport_start = updated_space_transform.transform_point2(updated.start()); let updated_viewport_start = updated_to_viewport_transform.transform_point2(updated.start());
let updated_viewport_end = updated_space_transform.transform_point2(updated.end()); let updated_viewport_end = updated_to_viewport_transform.transform_point2(updated.end());
assert!( assert!(
updated_viewport_start.abs_diff_eq(viewport_start, 1.), updated_viewport_start.abs_diff_eq(viewport_start, 1.),
@@ -2848,10 +2877,10 @@ mod test_gradient {
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
let document = editor.active_document(); let document = editor.active_document();
let space_transform = gradient_space_transform(layer, document); let to_viewport_transform = gradient_to_viewport_transform(layer, document);
let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface).unwrap(); let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface).unwrap();
let gradient = ResolvedGradient::new(gradient, appearance); let gradient = ResolvedGradient::new(gradient, appearance);
let viewport_end = space_transform.transform_point2(gradient.end()); let viewport_end = to_viewport_transform.transform_point2(gradient.end());
// Drag the end point 80px down // Drag the end point 80px down
let new_viewport_end = viewport_end + DVec2::new(0., 80.); let new_viewport_end = viewport_end + DVec2::new(0., 80.);
+13 -15
View File
@@ -614,7 +614,9 @@ tagged_value! {
GradientForm(vector::style::GradientForm), GradientForm(vector::style::GradientForm),
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code #[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
GradientSpread(vector::style::GradientSpread), GradientSpread(vector::style::GradientSpread),
GradientInterpolation(vector::style::GradientInterpolation), #[serde(alias = "GradientInterpolation")] // TODO: Eventually remove this document upgrade code
GradientSpace(vector::style::GradientSpace),
GradientHueDirection(vector::style::GradientHueDirection),
ReferencePoint(vector::ReferencePoint), ReferencePoint(vector::ReferencePoint),
CentroidType(vector::misc::CentroidType), CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation), BooleanOperation(vector::misc::BooleanOperation),
@@ -868,7 +870,7 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
&& array.is_empty() && array.is_empty()
{ {
let ramp = GradientRamp { let ramp = GradientRamp {
gradient_interpolation: vector::style::GradientInterpolation::SrgbGamma, gradient_space: vector::style::GradientSpace::RgbGamma,
..Default::default() ..Default::default()
}; };
return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp))); return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp)));
@@ -1093,7 +1095,7 @@ mod paint_default_parsing {
#[cfg(test)] #[cfg(test)]
mod gradient_shape_migration { mod gradient_shape_migration {
use graphic_types::vector_types::{GradientInterpolation, GradientSpread}; use graphic_types::vector_types::{GradientSpace, GradientSpread};
use super::*; use super::*;
@@ -1121,26 +1123,22 @@ mod gradient_shape_migration {
let json = serde_json::to_value(&value).unwrap(); let json = serde_json::to_value(&value).unwrap();
assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}"); assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}");
assert_eq!( assert_eq!(
json.get("GradientRamp").and_then(|payload| payload.get("gradient_interpolation")), json.get("GradientRamp").and_then(|payload| payload.get("gradient_space")),
Some(&serde_json::json!("SrgbLinear")), Some(&serde_json::json!("OkLab")),
"the interpolation should serialize even at its default, marking the ramp as post-legacy: {json}" "the space should serialize even at its default, marking the ramp as post-legacy: {json}"
); );
assert_eq!(load(json), value); assert_eq!(load(json), value);
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
#[test] #[test]
fn ramp_without_interpolation_field_reads_as_legacy_gamma() { fn ramp_without_space_field_reads_as_legacy_gamma() {
let json = serde_json::json!({ "GradientRamp": { "stops": { "color": [white(), white()] } } }); let json = serde_json::json!({ "GradientRamp": { "stops": { "color": [white(), white()] } } });
let TaggedValue::GradientRamp(ramp) = load(json) else { let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the ramp payload should become a gradient ramp value") panic!("the ramp payload should become a gradient ramp value")
}; };
assert_eq!( assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "a ramp saved before the field existed should read as gamma");
ramp.gradient_interpolation,
GradientInterpolation::SrgbGamma,
"a ramp saved before the field existed should read as gamma"
);
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
@@ -1150,7 +1148,7 @@ mod gradient_shape_migration {
let TaggedValue::GradientRamp(ramp) = load(json) else { let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the flat stops should become a gradient ramp value") panic!("the flat stops should become a gradient ramp value")
}; };
assert_eq!(ramp.gradient_interpolation, GradientInterpolation::SrgbGamma, "the pre-ramp flat form should carry the era's gamma"); assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp flat form should carry the era's gamma");
let gradient = Gradient::from(ramp); let gradient = Gradient::from(ramp);
assert_eq!(gradient.positions(), vec![0., 0.25]); assert_eq!(gradient.positions(), vec![0., 0.25]);
@@ -1164,7 +1162,7 @@ mod gradient_shape_migration {
let TaggedValue::GradientRamp(ramp) = load(json) else { let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the tuple stops should become a gradient ramp value") panic!("the tuple stops should become a gradient ramp value")
}; };
assert_eq!(ramp.gradient_interpolation, GradientInterpolation::SrgbGamma, "the pre-ramp tuple form should carry the era's gamma"); assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp tuple form should carry the era's gamma");
let gradient = Gradient::from(ramp); let gradient = Gradient::from(ramp);
assert_eq!(gradient.positions(), vec![0., 1.]); assert_eq!(gradient.positions(), vec![0., 1.]);
@@ -1176,7 +1174,7 @@ mod gradient_shape_migration {
fn empty_legacy_gradient_table_degrades_to_the_default() { fn empty_legacy_gradient_table_degrades_to_the_default() {
let json = serde_json::json!({ "GradientTable": { "element": [] } }); let json = serde_json::json!({ "GradientTable": { "element": [] } });
let expected = GradientRamp { let expected = GradientRamp {
gradient_interpolation: GradientInterpolation::SrgbGamma, gradient_space: GradientSpace::RgbGamma,
..Default::default() ..Default::default()
}; };
assert_eq!(load(json), TaggedValue::GradientRamp(expected)); assert_eq!(load(json), TaggedValue::GradientRamp(expected));
@@ -17,7 +17,7 @@ pub mod migrations {
use crate::Vector; use crate::Vector;
use core_types::Color; use core_types::Color;
use vector_types::gradient::GradientStops; use vector_types::gradient::GradientStops;
use vector_types::{Gradient, GradientInterpolation, GradientRamp}; use vector_types::{Gradient, GradientRamp, GradientSpace};
// Storing legacy structs that are only used in document migration. // Storing legacy structs that are only used in document migration.
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
@@ -152,7 +152,7 @@ pub mod migrations {
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
/// Recovers a [`GradientRamp`] from any of its on-disk shapes: the current nested form, the flat stops struct /// Recovers a [`GradientRamp`] from any of its on-disk shapes: the current nested form, the flat stops struct
/// that preceded it, or the ancient position-color tuple list (whose even positions elide back to absence). /// that preceded it, or the ancient position-color tuple list (whose even positions elide back to absence).
/// The pre-ramp shapes come from documents that rendered in gamma, so they carry that interpolation explicitly. /// The pre-ramp shapes come from documents that rendered in gamma, so they carry that space explicitly.
pub fn migrate_to_gradient_ramp<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientRamp, D::Error> { pub fn migrate_to_gradient_ramp<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientRamp, D::Error> {
use serde::Deserialize; use serde::Deserialize;
@@ -167,7 +167,7 @@ pub mod migrations {
Ok(match GradientRampFormat::deserialize(deserializer)? { Ok(match GradientRampFormat::deserialize(deserializer)? {
GradientRampFormat::Ramp(ramp) => ramp, GradientRampFormat::Ramp(ramp) => ramp,
GradientRampFormat::FlatStops(stops) => GradientRamp { GradientRampFormat::FlatStops(stops) => GradientRamp {
gradient_interpolation: GradientInterpolation::SrgbGamma, gradient_space: GradientSpace::RgbGamma,
..GradientRamp::from(stops) ..GradientRamp::from(stops)
}, },
GradientRampFormat::Tuples(stops) => { GradientRampFormat::Tuples(stops) => {
@@ -177,7 +177,7 @@ pub mod migrations {
gradient.elide_default_attributes(); gradient.elide_default_attributes();
GradientRamp { GradientRamp {
gradient_interpolation: GradientInterpolation::SrgbGamma, gradient_space: GradientSpace::RgbGamma,
..GradientRamp::from(gradient) ..GradientRamp::from(gradient)
} }
} }
@@ -8,11 +8,13 @@ use core_types::uuid::generate_uuid;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::Graphic; use graphic_types::Graphic;
use graphic_types::vector_types::gradient::GradientForm; use graphic_types::vector_types::gradient::GradientForm;
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpread as GradientSpreadAttr}; use graphic_types::vector_types::markers::{
GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
};
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write; use std::fmt::Write;
use vector_types::Gradient; use vector_types::Gradient;
use vector_types::gradient::{GradientInterpolation, GradientSpread}; use vector_types::gradient::{GradientHueDirection, GradientSpace, GradientSpread};
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
pub enum PaintTarget { pub enum PaintTarget {
@@ -111,9 +113,10 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(0); let gradient_form: GradientForm = source.attr::<GradientFormAttr>(0);
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0); let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0); let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0);
let gradient_interpolation: GradientInterpolation = source.attr::<GradientInterpolationAttr>(0); let gradient_space: GradientSpace = source.attr::<GradientSpaceAttr>(0);
let gradient_hue_direction: GradientHueDirection = source.attr::<GradientHueDirectionAttr>(0);
let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::SvgStopOrder); let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::SvgStopOrder);
for (position, color, original_midpoint) in samples { for (position, color, original_midpoint) in samples {
stop.push_str("<stop"); stop.push_str("<stop");
+97 -73
View File
@@ -26,8 +26,10 @@ 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::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::markers::{EditorMergedLayers, Fill, Stroke};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientInterpolation}; use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientHueDirection, GradientSpace};
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpread as GradientSpreadAttr}; use graphic_types::vector_types::markers::{
GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
};
use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
@@ -426,10 +428,11 @@ pub(crate) fn spread_adjusted_samples(
gradient: &Gradient, gradient: &Gradient,
gradient_spread: GradientSpread, gradient_spread: GradientSpread,
gradient_form: GradientForm, gradient_form: GradientForm,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
guards: ClearGuardPlacement, guards: ClearGuardPlacement,
) -> (GradientSamples, (f64, f64)) { ) -> (GradientSamples, (f64, f64)) {
let samples = gradient.interpolated_samples(gradient_interpolation); let samples = gradient.interpolated_samples(gradient_space, gradient_hue_direction);
if gradient_spread != GradientSpread::Clear { if gradient_spread != GradientSpread::Clear {
return (samples, (0., 1.)); return (samples, (0., 1.));
} }
@@ -514,9 +517,10 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0); let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0);
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0); let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0); let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0);
let gradient_interpolation: GradientInterpolation = gradient_list.attr::<GradientInterpolationAttr>(0); let gradient_space: GradientSpace = gradient_list.attr::<GradientSpaceAttr>(0);
let gradient_hue_direction: GradientHueDirection = gradient_list.attr::<GradientHueDirectionAttr>(0);
let (samples, span) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::VelloRampTexels); let (samples, span) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::VelloRampTexels);
let peniko_stops = peniko_color_stops(&samples); let peniko_stops = peniko_color_stops(&samples);
@@ -772,7 +776,7 @@ fn collect_element_metadata<'a>(
Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::Color(_) => {} Graphic::Color(_) => {}
Graphic::Gradient(_) => {} Graphic::Gradient(gradient) => collect_gradient_metadata(&Single(gradient), metadata, element_id),
Graphic::Text(text) => collect_text_metadata(&Single(text), metadata, footprint, element_id), Graphic::Text(text) => collect_text_metadata(&Single(text), metadata, footprint, element_id),
Graphic::Group(group) => collect_group_metadata(group, reach, metadata, footprint, element_id), Graphic::Group(group) => collect_group_metadata(group, reach, metadata, footprint, element_id),
} }
@@ -814,7 +818,8 @@ fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReac
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets), Graphic::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets),
Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), click_targets), Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), click_targets),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets),
Graphic::Color(_) | Graphic::Gradient(_) => {} Graphic::Color(_) => {}
Graphic::Gradient(gradient) => click_targets.extend(gradient_control_targets(&Single(gradient), |transform| transform, true)),
Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets), Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets),
Graphic::Group(group) => add_group_upstream_click_targets(group, reach, click_targets), Graphic::Group(group) => add_group_upstream_click_targets(group, reach, click_targets),
} }
@@ -827,7 +832,8 @@ fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintRe
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines), Graphic::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines),
Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), outlines), Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), outlines),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines),
Graphic::Color(_) | Graphic::Gradient(_) => {} Graphic::Color(_) => {}
Graphic::Gradient(gradient) => outlines.extend(gradient_control_targets(&Single(gradient), |transform| transform, false)),
Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines), Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines),
Graphic::Group(group) => add_group_upstream_outline_targets(group, reach, outlines), Graphic::Group(group) => add_group_upstream_outline_targets(group, reach, outlines),
} }
@@ -894,7 +900,9 @@ fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata:
collect_raster_metadata(&run, metadata, footprint, element_id) collect_raster_metadata(&run, metadata, footprint, element_id)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) { } else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
collect_raster_metadata(&run, metadata, footprint, element_id) collect_raster_metadata(&run, metadata, footprint, element_id)
} else if item.typed_lanes::<Color>().is_some() || item.typed_lanes::<Gradient>().is_some() { } else if let Some(run) = RunView::<Gradient>::new(item) {
collect_gradient_metadata(&run, metadata, element_id)
} else if item.typed_lanes::<Color>().is_some() {
} else if let Some(run) = RunView::<String>::new(item) { } else if let Some(run) = RunView::<String>::new(item) {
collect_text_metadata(&run, metadata, footprint, element_id) collect_text_metadata(&run, metadata, footprint, element_id)
} }
@@ -911,6 +919,8 @@ fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>,
} }
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() { } else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(click_targets) add_raster_upstream_click_targets(click_targets)
} else if let Some(run) = RunView::<Gradient>::new(item) {
click_targets.extend(gradient_control_targets(&run, |transform| transform, true))
} else if let Some(run) = RunView::<String>::new(item) { } else if let Some(run) = RunView::<String>::new(item) {
add_text_upstream_click_targets(&run, click_targets) add_text_upstream_click_targets(&run, click_targets)
} }
@@ -927,6 +937,8 @@ fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a
} }
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() { } else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(outlines) add_raster_upstream_click_targets(outlines)
} else if let Some(run) = RunView::<Gradient>::new(item) {
outlines.extend(gradient_control_targets(&run, |transform| transform, false))
} else if let Some(run) = RunView::<String>::new(item) { } else if let Some(run) = RunView::<String>::new(item) {
add_text_upstream_click_targets(&run, outlines) add_text_upstream_click_targets(&run, outlines)
} }
@@ -2381,7 +2393,6 @@ fn render_color_vello<S: LaneSource<Element = Color>>(source: &S, scene: &mut Sc
} }
} }
} }
/// A gradient's control geometry in its local space: the unit circle a radial gradient's transform carries to its drawn ellipse, or the (0,0) to (1,0) gradient line for a linear one. /// A gradient's control geometry in its local space: the unit circle a radial gradient's transform carries to its drawn ellipse, or the (0,0) to (1,0) gradient line for a linear one.
fn gradient_control_outline(gradient_form: GradientForm) -> Subpath<graphic_types::vector_types::vector::PointId> { fn gradient_control_outline(gradient_form: GradientForm) -> Subpath<graphic_types::vector_types::vector::PointId> {
match gradient_form { match gradient_form {
@@ -2424,7 +2435,8 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index); let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index); let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
let gradient_interpolation: GradientInterpolation = source.attr::<GradientInterpolationAttr>(index); let gradient_space: GradientSpace = source.attr::<GradientSpaceAttr>(index);
let gradient_hue_direction: GradientHueDirection = source.attr::<GradientHueDirectionAttr>(index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
render.leaf_tag(tag, |attributes| { render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect { if let Some((min, size)) = thumbnail_rect {
@@ -2440,7 +2452,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}")); attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
} }
let (samples, _) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::SvgStopOrder); let (samples, _) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::SvgStopOrder);
let mut stop_string = String::new(); let mut stop_string = String::new();
for (position, color, original_midpoint) in samples { for (position, color, original_midpoint) in samples {
@@ -2511,7 +2523,8 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
let Some(gradient) = source.element(index) else { continue }; let Some(gradient) = source.element(index) else { continue };
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index); let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index); let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
let gradient_interpolation: GradientInterpolation = source.attr::<GradientInterpolationAttr>(index); let gradient_space: GradientSpace = source.attr::<GradientSpaceAttr>(index);
let gradient_hue_direction: GradientHueDirection = source.attr::<GradientHueDirectionAttr>(index);
let transform: DAffine2 = source.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
@@ -2521,7 +2534,7 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::VelloRampTexels); let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::VelloRampTexels);
let stops = peniko_color_stops(&samples); let stops = peniko_color_stops(&samples);
let extend = peniko_extend(gradient_spread); let extend = peniko_extend(gradient_spread);
@@ -2576,6 +2589,49 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
} }
} }
/// The control geometry of each gradient lane, transformed by `lane_transform`.
fn gradient_control_targets<S: LaneSource>(source: &S, lane_transform: impl Fn(DAffine2) -> DAffine2, clickable_only: bool) -> Vec<ClickTarget> {
(0..source.lane_count())
.filter_map(|index| {
let gradient_form = source.attr::<GradientFormAttr>(index);
if clickable_only && !gradient_control_interior_is_clickable(gradient_form) {
return None;
}
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
target.apply_transform(lane_transform(source.attr::<Transform>(index)));
Some(target)
})
.collect()
}
fn collect_gradient_metadata<S: LaneSource>(source: &S, metadata: &mut RenderMetadata, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return };
if source.lane_count() == 0 {
return;
}
// Targets are baked relative to lane 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`
let lane_zero_transform = source.attr::<Transform>(0);
let lane_zero_inverse = if transform_is_invertible(lane_zero_transform) {
lane_zero_transform.inverse()
} else {
DAffine2::IDENTITY
};
let outline_targets: Vec<Arc<ClickTarget>> = gradient_control_targets(source, |transform| lane_zero_inverse * transform, false).into_iter().map(Arc::new).collect();
let click_targets: Vec<Arc<ClickTarget>> = outline_targets
.iter()
.enumerate()
.filter(|(index, _)| gradient_control_interior_is_clickable(source.attr::<GradientFormAttr>(*index)))
.map(|(_, target)| target.clone())
.collect();
metadata.outlines.insert(element_id, outline_targets);
if !click_targets.is_empty() {
metadata.click_targets.insert(element_id, click_targets);
}
}
impl Render for List<Gradient> { impl Render for List<Gradient> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_gradient_svg(self, render, render_params) render_gradient_svg(self, render, render_params)
@@ -2586,64 +2642,15 @@ impl Render for List<Gradient> {
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return }; collect_gradient_metadata(self, metadata, element_id)
if self.is_empty() {
return;
}
// Targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`
let item_zero_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
item_zero_transform.inverse()
} else {
DAffine2::IDENTITY
};
let mut outline_targets = Vec::new();
let mut click_targets = Vec::new();
for index in 0..self.len() {
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
target.apply_transform(item_zero_inverse * item_transform);
let target = Arc::new(target);
if gradient_control_interior_is_clickable(gradient_form) {
click_targets.push(target.clone());
}
outline_targets.push(target);
}
metadata.outlines.insert(element_id, outline_targets);
if !click_targets.is_empty() {
metadata.click_targets.insert(element_id, click_targets);
}
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { click_targets.extend(gradient_control_targets(self, |transform| transform, true));
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
if !gradient_control_interior_is_clickable(gradient_form) {
continue;
}
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
target.apply_transform(transform);
click_targets.push(target);
}
} }
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) { fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
for index in 0..self.len() { outlines.extend(gradient_control_targets(self, |transform| transform, false));
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
target.apply_transform(transform);
outlines.push(target);
}
} }
} }
@@ -3113,6 +3120,18 @@ impl Render for RunView<'_, Gradient> {
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_gradient_vello(self, scene, parent_transform, render_params) render_gradient_vello(self, scene, parent_transform, render_params)
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option<NodeId>) {
collect_gradient_metadata(self, metadata, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
click_targets.extend(gradient_control_targets(self, |transform| transform, true));
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
outlines.extend(gradient_control_targets(self, |transform| transform, false));
}
} }
impl Render for RunView<'_, Artboard<'_>> { impl Render for RunView<'_, Artboard<'_>> {
@@ -3330,18 +3349,20 @@ mod spread_tests {
&gradient, &gradient,
GradientSpread::Repeat, GradientSpread::Repeat,
GradientForm::Linear, GradientForm::Linear,
GradientInterpolation::SrgbGamma, GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder, ClearGuardPlacement::SvgStopOrder,
); );
assert_eq!(span, (0., 1.)); assert_eq!(span, (0., 1.));
assert_eq!(samples, gradient.interpolated_samples(GradientInterpolation::SrgbGamma)); assert_eq!(samples, gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default()));
// SVG guards share the range ends' exact offsets, ordered so the pad extension resolves to the transparent outer stops // SVG guards share the range ends' exact offsets, ordered so the pad extension resolves to the transparent outer stops
let (samples, span) = spread_adjusted_samples( let (samples, span) = spread_adjusted_samples(
&gradient, &gradient,
GradientSpread::Clear, GradientSpread::Clear,
GradientForm::Linear, GradientForm::Linear,
GradientInterpolation::SrgbGamma, GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder, ClearGuardPlacement::SvgStopOrder,
); );
assert_eq!(span, (0., 1.)); assert_eq!(span, (0., 1.));
@@ -3356,7 +3377,8 @@ mod spread_tests {
&gradient, &gradient,
GradientSpread::Clear, GradientSpread::Clear,
GradientForm::Linear, GradientForm::Linear,
GradientInterpolation::SrgbGamma, GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::VelloRampTexels, ClearGuardPlacement::VelloRampTexels,
); );
assert_eq!( assert_eq!(
@@ -3375,7 +3397,8 @@ mod spread_tests {
&gradient, &gradient,
GradientSpread::Clear, GradientSpread::Clear,
GradientForm::Radial, GradientForm::Radial,
GradientInterpolation::SrgbGamma, GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::VelloRampTexels, ClearGuardPlacement::VelloRampTexels,
); );
assert_eq!(span.0, 0.); assert_eq!(span.0, 0.);
@@ -3389,7 +3412,8 @@ mod spread_tests {
&Gradient::from(Vec::new()), &Gradient::from(Vec::new()),
GradientSpread::Clear, GradientSpread::Clear,
GradientForm::Linear, GradientForm::Linear,
GradientInterpolation::SrgbGamma, GradientSpace::RgbGamma,
Default::default(),
ClearGuardPlacement::SvgStopOrder, ClearGuardPlacement::SvgStopOrder,
); );
let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect(); let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect();
@@ -20,6 +20,7 @@ node-macro = { workspace = true }
# Workspace dependencies # Workspace dependencies
bitflags = { workspace = true } bitflags = { workspace = true }
bytemuck = { workspace = true } bytemuck = { workspace = true }
color = { workspace = true }
num-traits = { workspace = true } num-traits = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
kurbo = { workspace = true } kurbo = { workspace = true }
+390 -100
View File
@@ -1,4 +1,4 @@
use crate::markers::{ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD}; use crate::markers::{ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD};
use core_types::Color; use core_types::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION, Item, List}; use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION, Item, List};
@@ -94,13 +94,13 @@ impl From<&GradientStops<SRGBA8>> for Gradient {
impl GradientStops<SRGBA8> { impl GradientStops<SRGBA8> {
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes). /// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
pub fn to_css_linear_gradient(&self, gradient_interpolation: GradientInterpolation) -> String { pub fn to_css_linear_gradient(&self, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> String {
Gradient::from(self).to_css_linear_gradient(gradient_interpolation) Gradient::from(self).to_css_linear_gradient(gradient_space, gradient_hue_direction)
} }
} }
/// The serialized exchange form of a gradient: its stops, with whole-ramp settings as sibling fields serialized /// The serialized exchange form of a gradient: its stops, with whole-ramp settings as sibling fields serialized
/// only when non-default. The interpolation is the exception: it always serializes, so its absence marks a ramp /// only when non-default. The space is the exception: it always serializes, so its absence marks a ramp
/// from before the field existed, which deserializes as the gamma those documents rendered with. /// from before the field existed, which deserializes as the gamma those documents rendered with.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)] #[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)]
@@ -110,9 +110,12 @@ pub struct GradientRamp<C = Color> {
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "GradientSpread::is_default"))] #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "GradientSpread::is_default"))]
#[cfg_attr(feature = "wasm", tsify(optional))] #[cfg_attr(feature = "wasm", tsify(optional))]
pub gradient_spread: GradientSpread, pub gradient_spread: GradientSpread,
// TODO: Elide the default again (removing `legacy_gamma`) when switching to the new document format and Ctrl-C node serialization format // TODO: Elide the default again (removing `legacy_gamma` and the serde aliases) when switching to the new document format and Ctrl-C node serialization format
#[cfg_attr(feature = "serde", serde(default = "GradientInterpolation::legacy_gamma"))] #[cfg_attr(feature = "serde", serde(default = "GradientSpace::legacy_gamma", alias = "gradient_interpolation"))]
pub gradient_interpolation: GradientInterpolation, pub gradient_space: GradientSpace,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "GradientHueDirection::is_default"))]
#[cfg_attr(feature = "wasm", tsify(optional))]
pub gradient_hue_direction: GradientHueDirection,
} }
unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> { unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> {
@@ -124,7 +127,8 @@ impl<C> From<GradientStops<C>> for GradientRamp<C> {
Self { Self {
stops, stops,
gradient_spread: Default::default(), gradient_spread: Default::default(),
gradient_interpolation: Default::default(), gradient_space: Default::default(),
gradient_hue_direction: Default::default(),
} }
} }
} }
@@ -134,7 +138,8 @@ impl From<&Gradient> for GradientRamp {
Self { Self {
stops: gradient.into(), stops: gradient.into(),
gradient_spread: Default::default(), gradient_spread: Default::default(),
gradient_interpolation: Default::default(), gradient_space: Default::default(),
gradient_hue_direction: Default::default(),
} }
} }
} }
@@ -165,8 +170,11 @@ impl From<GradientRamp> for Item<Gradient> {
if !ramp.gradient_spread.is_default() { if !ramp.gradient_spread.is_default() {
item.set_attribute(ATTR_GRADIENT_SPREAD, ramp.gradient_spread); item.set_attribute(ATTR_GRADIENT_SPREAD, ramp.gradient_spread);
} }
if !ramp.gradient_interpolation.is_default() { if !ramp.gradient_space.is_default() {
item.set_attribute(ATTR_GRADIENT_INTERPOLATION, ramp.gradient_interpolation); item.set_attribute(ATTR_GRADIENT_SPACE, ramp.gradient_space);
}
if !ramp.gradient_hue_direction.is_default() {
item.set_attribute(ATTR_GRADIENT_HUE_DIRECTION, ramp.gradient_hue_direction);
} }
item item
} }
@@ -177,7 +185,8 @@ impl From<&Item<Gradient>> for GradientRamp {
Self { Self {
stops: item.element().into(), stops: item.element().into(),
gradient_spread: item.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD), gradient_spread: item.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD),
gradient_interpolation: item.attribute_cloned_or_default(ATTR_GRADIENT_INTERPOLATION), gradient_space: item.attribute_cloned_or_default(ATTR_GRADIENT_SPACE),
gradient_hue_direction: item.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION),
} }
} }
} }
@@ -204,7 +213,8 @@ impl From<&GradientRamp> for GradientRamp<SRGBA8> {
Self { Self {
stops: ramp.into(), stops: ramp.into(),
gradient_spread: ramp.gradient_spread, gradient_spread: ramp.gradient_spread,
gradient_interpolation: ramp.gradient_interpolation, gradient_space: ramp.gradient_space,
gradient_hue_direction: ramp.gradient_hue_direction,
} }
} }
} }
@@ -214,7 +224,8 @@ impl From<&Gradient> for GradientRamp<SRGBA8> {
Self { Self {
stops: gradient.into(), stops: gradient.into(),
gradient_spread: Default::default(), gradient_spread: Default::default(),
gradient_interpolation: Default::default(), gradient_space: Default::default(),
gradient_hue_direction: Default::default(),
} }
} }
} }
@@ -223,7 +234,8 @@ impl From<&GradientRamp<SRGBA8>> for GradientRamp {
fn from(ramp: &GradientRamp<SRGBA8>) -> Self { fn from(ramp: &GradientRamp<SRGBA8>) -> Self {
Self { Self {
gradient_spread: ramp.gradient_spread, gradient_spread: ramp.gradient_spread,
gradient_interpolation: ramp.gradient_interpolation, gradient_space: ramp.gradient_space,
gradient_hue_direction: ramp.gradient_hue_direction,
..Self::from(&ramp.stops) ..Self::from(&ramp.stops)
} }
} }
@@ -274,11 +286,131 @@ fn apply_midpoint(t: f64, midpoint: f64) -> f64 {
} }
} }
/// Interpolates between two adjacent stops' colors at `t` across their interval, in the gradient's interpolation color space. /// Interpolates between two adjacent stops' colors at `t` across their interval, in the gradient's chosen color space.
pub fn interpolate_stop_colors(color_a: Color, color_b: Color, t: f32, gradient_interpolation: GradientInterpolation) -> Color { pub fn interpolate_stop_colors(color_a: Color, color_b: Color, t: f32, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Color {
match gradient_interpolation { match gradient_space {
GradientInterpolation::SrgbLinear => color_a.lerp(&color_b, t), GradientSpace::OkLab => lerp_in_space::<color::Oklab>(color_a, color_b, t, gradient_hue_direction),
GradientInterpolation::SrgbGamma => color_a.lerp_gamma_srgb(&color_b, t), GradientSpace::OkLCh => lerp_in_space::<color::Oklch>(color_a, color_b, t, gradient_hue_direction),
GradientSpace::Lab => lerp_in_space::<color::Lab>(color_a, color_b, t, gradient_hue_direction),
GradientSpace::LCh => lerp_in_space::<color::Lch>(color_a, color_b, t, gradient_hue_direction),
GradientSpace::Hsl => lerp_in_space::<color::Hsl>(color_a, color_b, t, gradient_hue_direction),
GradientSpace::Hsv => lerp_in_space::<Hsv>(color_a, color_b, t, gradient_hue_direction),
GradientSpace::RgbLinear => color_a.lerp(&color_b, t),
GradientSpace::RgbGamma => color_a.lerp_gamma_srgb(&color_b, t),
}
}
/// Mix two colors in the color space `CS`, with alpha interpolating linearly like the sRGB spaces.
/// Polar spaces arc through hue per the direction, with an achromatic endpoint's powerless hue adopting the other's per CSS.
/// The mix can land slightly outside the sRGB gamut; it stays unclamped here and clips at render encoding.
fn lerp_in_space<CS: color::ColorSpace>(color_a: Color, color_b: Color, t: f32, gradient_hue_direction: GradientHueDirection) -> Color {
use color::ColorSpaceLayout;
let mut a = CS::from_linear_srgb([color_a.r(), color_a.g(), color_a.b()]);
let mut b = CS::from_linear_srgb([color_b.r(), color_b.g(), color_b.b()]);
let hue_index = match CS::LAYOUT {
ColorSpaceLayout::HueFirst => Some(0),
ColorSpaceLayout::HueThird => Some(2),
_ => None,
};
if let Some(hue_index) = hue_index {
// Chroma (or saturation) is channel 1 in both polar layouts; the threshold scales to the space's
// lightness range so conversion noise on achromatic colors stays below it
let achromatic = 1e-4 * CS::WHITE_COMPONENTS.iter().fold(0_f32, |max, &component| max.max(component));
if a[1] < achromatic && b[1] >= achromatic {
a[hue_index] = b[hue_index];
}
if b[1] < achromatic && a[1] >= achromatic {
b[hue_index] = a[hue_index];
}
// The CSS Color 4 hue fixup, on hues the conversions already place in the 0 to 360 range
let delta = b[hue_index] - a[hue_index];
let delta = match gradient_hue_direction {
GradientHueDirection::Shorter => {
if delta > 180. {
delta - 360.
} else if delta < -180. {
delta + 360.
} else {
delta
}
}
GradientHueDirection::Longer => {
if 0. < delta && delta < 180. {
delta - 360.
} else if -180. < delta && delta <= 0. {
delta + 360.
} else {
delta
}
}
GradientHueDirection::Increasing => {
if delta < 0. {
delta + 360.
} else {
delta
}
}
GradientHueDirection::Decreasing => {
if delta > 0. {
delta - 360.
} else {
delta
}
}
};
b[hue_index] = a[hue_index] + delta;
}
let mixed = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
let [red, green, blue] = CS::to_linear_srgb(mixed);
Color::from_rgbaf32_unchecked(red, green, blue, color_a.a() + (color_b.a() - color_a.a()) * t)
}
/// The classic HSV cylinder as `[hue in degrees, saturation 0-100, value 0-100]`, implemented locally
/// since CSS Color 4 (and therefore the `color` crate) offers only its HWB reparameterization, which
/// mixes along different paths through shades and tones.
#[derive(Clone, Copy, Debug)]
struct Hsv;
impl color::ColorSpace for Hsv {
const LAYOUT: color::ColorSpaceLayout = color::ColorSpaceLayout::HueFirst;
const WHITE_COMPONENTS: [f32; 3] = [0., 0., 100.];
fn to_linear_srgb([hue, saturation, value]: [f32; 3]) -> [f32; 3] {
let (saturation, value) = (saturation / 100., value / 100.);
let channel = |n: f32| {
let k = (n + hue / 60.).rem_euclid(6.);
value - value * saturation * k.min(4. - k).clamp(0., 1.)
};
color::Srgb::to_linear_srgb([channel(5.), channel(3.), channel(1.)])
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let [red, green, blue] = color::Srgb::from_linear_srgb(src);
let max = red.max(green).max(blue);
let delta = max - red.min(green).min(blue);
let hue = if delta <= 0. {
0.
} else if max == red {
60. * ((green - blue) / delta).rem_euclid(6.)
} else if max == green {
60. * ((blue - red) / delta + 2.)
} else {
60. * ((red - green) / delta + 4.)
};
let saturation = if max <= 0. { 0. } else { delta / max };
[hue, saturation * 100., max * 100.]
}
fn clip([hue, saturation, value]: [f32; 3]) -> [f32; 3] {
[hue, saturation.clamp(0., 100.), value.clamp(0., 100.)]
} }
} }
@@ -550,8 +682,8 @@ impl Gradient {
/// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color. /// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color.
/// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start). /// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start).
/// Returns the index where the new stop was inserted. /// Returns the index where the new stop was inserted.
pub fn insert_stop(&mut self, position: f64, gradient_interpolation: GradientInterpolation) -> usize { pub fn insert_stop(&mut self, position: f64, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> usize {
let color = self.evaluate(position, Default::default(), gradient_interpolation); let color = self.evaluate(position, Default::default(), gradient_space, gradient_hue_direction);
let index = (0..self.len()).position(|i| self.position(i) > position).unwrap_or(self.len()); let index = (0..self.len()).position(|i| self.position(i) > position).unwrap_or(self.len());
let midpoint = if index > 0 { self.midpoint(index - 1) } else { 0.5 }; let midpoint = if index > 0 { self.midpoint(index - 1) } else { 0.5 };
self.insert_stop_values(position, midpoint, color) self.insert_stop_values(position, midpoint, color)
@@ -632,7 +764,7 @@ impl Gradient {
} }
/// Samples the gradient's color at `t`. Given a `t` outside the 0 to 1 range, the `gradient_spread` determines how the gradient extends. /// Samples the gradient's color at `t`. Given a `t` outside the 0 to 1 range, the `gradient_spread` determines how the gradient extends.
pub fn evaluate(&self, t: f64, gradient_spread: GradientSpread, gradient_interpolation: GradientInterpolation) -> Color { pub fn evaluate(&self, t: f64, gradient_spread: GradientSpread, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Color {
let t = match gradient_spread { let t = match gradient_spread {
GradientSpread::Pad => t.clamp(0., 1.), GradientSpread::Pad => t.clamp(0., 1.),
GradientSpread::Repeat => t.rem_euclid(1.), GradientSpread::Repeat => t.rem_euclid(1.),
@@ -662,7 +794,7 @@ impl Gradient {
if t >= a.position && t <= b.position { if t >= a.position && t <= b.position {
let normalized_t = (t - a.position) / (b.position - a.position); let normalized_t = (t - a.position) / (b.position - a.position);
let adjusted_t = apply_midpoint(normalized_t, a.midpoint); let adjusted_t = apply_midpoint(normalized_t, a.midpoint);
return interpolate_stop_colors(a.color, b.color, adjusted_t as f32, gradient_interpolation); return interpolate_stop_colors(a.color, b.color, adjusted_t as f32, gradient_space, gradient_hue_direction);
} }
} }
@@ -703,14 +835,14 @@ impl Gradient {
mapped mapped
} }
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves and interpolation color space so the rendered gradient matches Graphite's interpolation rather than browser defaults. /// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves and color space so the rendered gradient matches Graphite's interpolation rather than browser defaults.
pub fn to_css_linear_gradient(&self, gradient_interpolation: GradientInterpolation) -> String { pub fn to_css_linear_gradient(&self, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> String {
if self.len() <= 1 { if self.len() <= 1 {
let hex = self.color(0).map(|c| SRGBA8::from(c).to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string()); let hex = self.color(0).map(|c| SRGBA8::from(c).to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)"); return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
} }
let pieces = self let pieces = self
.interpolated_samples(gradient_interpolation) .interpolated_samples(gradient_space, gradient_hue_direction)
.into_iter() .into_iter()
.map(|(position, color, _)| { .map(|(position, color, _)| {
let percent = ((position * 100.) * 1e2).round() / 1e2; let percent = ((position * 100.) * 1e2).round() / 1e2;
@@ -722,15 +854,15 @@ impl Gradient {
} }
/// Produce a set of linearly-interpolated color samples that approximate the gradient's midpoint curves /// Produce a set of linearly-interpolated color samples that approximate the gradient's midpoint curves
/// and interpolation color space. /// and color space.
/// ///
/// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding /// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding
/// midpoint for actual gradient stops, and `None` for synthesized curve approximation samples. /// midpoint for actual gradient stops, and `None` for synthesized curve approximation samples.
/// ///
/// The downstream SVG/CSS and Vello renderers interpolate between adjacent emitted stops in gamma sRGB space, so the /// The downstream SVG/CSS and Vello renderers interpolate between adjacent emitted stops in gamma sRGB space, so the
/// subdivision emits enough samples that the gamma-drawn segments match the ramp's true curve: the midpoint bias, and /// subdivision emits enough samples that the gamma-drawn segments match the ramp's true curve: the midpoint bias, and
/// the interpolation color space when it is not gamma itself. /// the color space when it is not gamma itself.
pub fn interpolated_samples(&self, gradient_interpolation: GradientInterpolation) -> Vec<(f64, Color, Option<f64>)> { pub fn interpolated_samples(&self, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Vec<(f64, Color, Option<f64>)> {
/// Controls accuracy vs. number of samples tradeoff. /// Controls accuracy vs. number of samples tradeoff.
/// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias. /// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias.
const THRESHOLD: f64 = 2. / 255.; const THRESHOLD: f64 = 2. / 255.;
@@ -744,7 +876,8 @@ impl Gradient {
pos_b: f64, pos_b: f64,
color_a: Color, color_a: Color,
color_b: Color, color_b: Color,
gradient_interpolation: GradientInterpolation, gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
result: &mut Vec<(f64, Color, Option<f64>)>, result: &mut Vec<(f64, Color, Option<f64>)>,
depth: u32, depth: u32,
) { ) {
@@ -761,23 +894,28 @@ impl Gradient {
let y_linear = (y_left + y_right) / 2.; let y_linear = (y_left + y_right) / 2.;
// A sample is needed wherever the renderer's gamma segment between the flanking samples would stray // A sample is needed wherever the renderer's gamma segment between the flanking samples would stray
// from the ramp's true curve: from the midpoint bias, or from a non-gamma space's own curvature // from the ramp's true curve: from the midpoint bias, or from a non-gamma space's own curvature.
// The space check probes the quarter points as well as the center, since spaces with a steep toe
// (like CIE Lab near black) peak their deviation off-center
let midpoint_deviates = (y_actual - y_linear).abs() > THRESHOLD; let midpoint_deviates = (y_actual - y_linear).abs() > THRESHOLD;
let space_deviates = gradient_interpolation != GradientInterpolation::SrgbGamma && { let space_deviates = gradient_space != GradientSpace::RgbGamma && {
let color_target = interpolate_stop_colors(color_a, color_b, y_actual as f32, gradient_interpolation); let color_left = interpolate_stop_colors(color_a, color_b, y_left as f32, gradient_space, gradient_hue_direction);
let color_left = interpolate_stop_colors(color_a, color_b, y_left as f32, gradient_interpolation); let color_right = interpolate_stop_colors(color_a, color_b, y_right as f32, gradient_space, gradient_hue_direction);
let color_right = interpolate_stop_colors(color_a, color_b, y_right as f32, gradient_interpolation); [0.25, 0.5, 0.75].into_iter().any(|fraction| {
max_gamma_channel_deviation(color_target, color_left.lerp_gamma_srgb(&color_right, 0.5)) > THRESHOLD let y_probe = apply_midpoint(left + (right - left) * fraction, midpoint);
let color_target = interpolate_stop_colors(color_a, color_b, y_probe as f32, gradient_space, gradient_hue_direction);
max_gamma_channel_deviation(color_target, color_left.lerp_gamma_srgb(&color_right, fraction as f32)) > THRESHOLD
})
}; };
if midpoint_deviates || space_deviates { if midpoint_deviates || space_deviates {
subdivide(left, mid, midpoint, pos_a, pos_b, color_a, color_b, gradient_interpolation, result, depth + 1); subdivide(left, mid, midpoint, pos_a, pos_b, color_a, color_b, gradient_space, gradient_hue_direction, result, depth + 1);
let global_pos = pos_a + mid * (pos_b - pos_a); let global_pos = pos_a + mid * (pos_b - pos_a);
let color = interpolate_stop_colors(color_a, color_b, y_actual as f32, gradient_interpolation); let color = interpolate_stop_colors(color_a, color_b, y_actual as f32, gradient_space, gradient_hue_direction);
result.push((global_pos, color, None)); result.push((global_pos, color, None));
subdivide(mid, right, midpoint, pos_a, pos_b, color_a, color_b, gradient_interpolation, result, depth + 1); subdivide(mid, right, midpoint, pos_a, pos_b, color_a, color_b, gradient_space, gradient_hue_direction, result, depth + 1);
} }
} }
@@ -807,8 +945,8 @@ impl Gradient {
} }
// Only subdivide if the midpoint deviates from linear (0.5) or a non-gamma space may curve away from the drawn gamma segment // Only subdivide if the midpoint deviates from linear (0.5) or a non-gamma space may curve away from the drawn gamma segment
if (midpoint - 0.5).abs() >= 1e-6 || gradient_interpolation != GradientInterpolation::SrgbGamma { if (midpoint - 0.5).abs() >= 1e-6 || gradient_space != GradientSpace::RgbGamma {
subdivide(0., 1., midpoint, pos_a, pos_b, color_a, color_b, gradient_interpolation, &mut result, 0); subdivide(0., 1., midpoint, pos_a, pos_b, color_a, color_b, gradient_space, gradient_hue_direction, &mut result, 0);
} }
// Add the end stop // Add the end stop
@@ -875,24 +1013,73 @@ impl GradientSpread {
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)] #[widget(Dropdown)]
pub enum GradientInterpolation { pub enum GradientSpace {
/// Blends stops in linear light, keeping transitions evenly bright. /// Interpolates between stops in the OkLab perceptual color space, keeping transitions visually even.
#[default] #[default]
#[label("sRGB Linear")] #[label("Perceptual (OkLab)")]
SrgbLinear, OkLab,
/// Blends stops in gamma-encoded sRGB, the classic SVG and CSS look. /// Interpolates between stops in the CIE Lab color space, the longtime perceptual standard.
#[label("sRGB Gamma")] #[label("Perceptual (Lab)")]
SrgbGamma, Lab,
/// Interpolates between stops in the polar form of OkLab, arcing through hue instead of fading through gray.
#[label("Perceptual Hue (OkLCh)")]
OkLCh,
/// Interpolates between stops in the polar form of CIE Lab, arcing through hue instead of fading through gray.
#[label("Perceptual Hue (LCh)")]
LCh,
/// Interpolates between stops in linear light, keeping transitions evenly bright.
#[menu_separator]
#[cfg_attr(feature = "serde", serde(alias = "SrgbLinear"))]
#[label("Linear (RGB)")]
RgbLinear,
/// Interpolates between stops in gamma-encoded RGB, matching classic SVG and CSS gradients.
#[cfg_attr(feature = "serde", serde(alias = "SrgbGamma"))]
#[label("Classic (RGB)")]
RgbGamma,
/// Interpolates between stops in the hue, saturation, and value cylinder, keeping tints at full brightness.
#[label("Classic Hue (HSV)")]
Hsv,
/// Interpolates between stops in the hue, saturation, and lightness cylinder.
#[label("Classic Hue (HSL)")]
Hsl,
} }
impl GradientInterpolation { impl GradientSpace {
pub fn is_default(&self) -> bool { pub fn is_default(&self) -> bool {
*self == Self::default() *self == Self::default()
} }
/// Whether the space is polar (cylindrical), making the hue direction option meaningful.
pub fn is_polar(&self) -> bool {
matches!(self, Self::OkLCh | Self::LCh | Self::Hsl | Self::Hsv)
}
// TODO: Remove when switching to the new document format and Ctrl-C node serialization format // TODO: Remove when switching to the new document format and Ctrl-C node serialization format
fn legacy_gamma() -> Self { fn legacy_gamma() -> Self {
Self::SrgbGamma Self::RgbGamma
}
}
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)]
pub enum GradientHueDirection {
/// Interpolates across the shorter arc around the hue wheel.
#[default]
Shorter,
/// Interpolates across the longer arc around the hue wheel.
Longer,
/// Interpolates with the hue angle always increasing.
Increasing,
/// Interpolates with the hue angle always decreasing.
Decreasing,
}
impl GradientHueDirection {
pub fn is_default(&self) -> bool {
*self == Self::default()
} }
} }
@@ -966,7 +1153,7 @@ mod tests {
fn default_is_empty_and_black_to_white_is_the_artist_starting_gradient() { fn default_is_empty_and_black_to_white_is_the_artist_starting_gradient() {
assert!(Gradient::default().is_empty()); assert!(Gradient::default().is_empty());
assert_eq!(Gradient::black_to_white().positions(), vec![0., 1.]); assert_eq!(Gradient::black_to_white().positions(), vec![0., 1.]);
assert_eq!(Gradient::default().evaluate(0.5, Default::default(), Default::default()), Color::BLACK); assert_eq!(Gradient::default().evaluate(0.5, Default::default(), Default::default(), Default::default()), Color::BLACK);
} }
#[test] #[test]
@@ -1030,24 +1217,29 @@ mod tests {
} }
#[test] #[test]
fn gradient_interpolation_always_serializes_and_its_absence_reads_as_legacy_gamma() { fn gradient_space_always_serializes_and_its_absence_reads_as_legacy_gamma() {
let default_interpolation = GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE])); let default_space = GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]));
let json = serde_json::to_string(&default_interpolation).unwrap(); let json = serde_json::to_string(&default_space).unwrap();
assert!( assert!(
json.contains(r#""gradient_interpolation":"SrgbLinear""#), json.contains(r#""gradient_space":"OkLab""#),
"the interpolation must serialize even at its default, marking the ramp as post-legacy: {json}" "the space must serialize even at its default, marking the ramp as post-legacy: {json}"
); );
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), default_interpolation); assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), default_space);
// The pre-rename field key and variant names from the interim format alias to the current ones
let renamed_away = json.replace(r#""gradient_space""#, r#""gradient_interpolation""#).replace(r#""OkLab""#, r#""SrgbLinear""#);
let recovered = serde_json::from_str::<GradientRamp>(&renamed_away).unwrap();
assert_eq!(recovered.gradient_space, GradientSpace::RgbLinear, "the old key and variant names should decode via their aliases");
let gamma = GradientRamp { let gamma = GradientRamp {
gradient_interpolation: GradientInterpolation::SrgbGamma, gradient_space: GradientSpace::RgbGamma,
..default_interpolation.clone() ..default_space.clone()
}; };
let json = serde_json::to_string(&gamma).unwrap(); let json = serde_json::to_string(&gamma).unwrap();
assert!(json.contains(r#""gradient_interpolation":"SrgbGamma""#), "a non-default interpolation must serialize: {json}"); assert!(json.contains(r#""gradient_space":"RgbGamma""#), "a non-default space must serialize: {json}");
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), gamma); assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), gamma);
let legacy_json = json.replace(r#","gradient_interpolation":"SrgbGamma""#, ""); let legacy_json = json.replace(r#","gradient_space":"RgbGamma""#, "");
assert_eq!( assert_eq!(
serde_json::from_str::<GradientRamp>(&legacy_json).unwrap(), serde_json::from_str::<GradientRamp>(&legacy_json).unwrap(),
gamma, gamma,
@@ -1056,38 +1248,38 @@ mod tests {
} }
#[test] #[test]
fn gradient_interpolation_round_trips_through_the_item_attribute() { fn gradient_space_round_trips_through_the_item_attribute() {
let ramp = GradientRamp { let ramp = GradientRamp {
gradient_interpolation: GradientInterpolation::SrgbGamma, gradient_space: GradientSpace::RgbGamma,
..GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE])) ..GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]))
}; };
let item = Item::<Gradient>::from(ramp.clone()); let item = Item::<Gradient>::from(ramp.clone());
assert_eq!( assert_eq!(
item.attribute_cloned_or_default::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION), item.attribute_cloned_or_default::<GradientSpace>(ATTR_GRADIENT_SPACE),
GradientInterpolation::SrgbGamma, GradientSpace::RgbGamma,
"the runtime item should carry the interpolation as its attribute" "the runtime item should carry the space as its attribute"
); );
assert_eq!(GradientRamp::from(&item), ramp); assert_eq!(GradientRamp::from(&item), ramp);
let linear = Item::<Gradient>::from(GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]))); let linear = Item::<Gradient>::from(GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE])));
assert!( assert!(
linear.attribute::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION).is_none(), linear.attribute::<GradientSpace>(ATTR_GRADIENT_SPACE).is_none(),
"the default Linear must stay absent rather than materialize" "the default Linear must stay absent rather than materialize"
); );
} }
#[test] #[test]
fn linear_interpolation_densifies_samples_where_gamma_segments_deviate() { fn linear_space_densifies_samples_where_gamma_segments_deviate() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
// Gamma needs no synthesized samples since the renderers already draw gamma segments // Gamma needs no synthesized samples since the renderers already draw gamma segments
assert_eq!(gradient.interpolated_samples(GradientInterpolation::SrgbGamma).len(), 2); assert_eq!(gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default()).len(), 2);
// A linear black-to-white ramp curves away from any single gamma segment, so samples must densify, // A linear black-to-white ramp curves away from any single gamma segment, so samples must densify,
// keeping the end stops in place and every synthesized color on the linear-light line // keeping the end stops in place and every synthesized color on the linear-light line
let samples = gradient.interpolated_samples(GradientInterpolation::SrgbLinear); let samples = gradient.interpolated_samples(GradientSpace::RgbLinear, Default::default());
assert!(samples.len() > 2, "linear interpolation should synthesize samples, got {}", samples.len()); assert!(samples.len() > 2, "the linear space should synthesize samples, got {}", samples.len());
assert_eq!(samples.first().unwrap().0, 0.); assert_eq!(samples.first().unwrap().0, 0.);
assert_eq!(samples.last().unwrap().0, 1.); assert_eq!(samples.last().unwrap().0, 1.);
for &(position, color, _) in &samples { for &(position, color, _) in &samples {
@@ -1100,11 +1292,11 @@ mod tests {
// Identical end colors leave nothing to densify // Identical end colors leave nothing to densify
let flat = Gradient::from(vec![Color::WHITE, Color::WHITE]); let flat = Gradient::from(vec![Color::WHITE, Color::WHITE]);
assert_eq!(flat.interpolated_samples(GradientInterpolation::SrgbLinear).len(), 2); assert_eq!(flat.interpolated_samples(GradientSpace::RgbLinear, Default::default()).len(), 2);
} }
#[test] #[test]
fn midpoint_bias_and_interpolation_space_compose_within_playback_tolerance() { fn midpoint_bias_and_gradient_space_compose_within_playback_tolerance() {
let color_pairs = [ let color_pairs = [
(Color::BLACK, Color::WHITE), (Color::BLACK, Color::WHITE),
(Color::RED, Color::WHITE), (Color::RED, Color::WHITE),
@@ -1113,14 +1305,25 @@ mod tests {
// Sweep the midpoint against each space so its bias and the space's curvature also oppose each other, // Sweep the midpoint against each space so its bias and the space's curvature also oppose each other,
// asserting the emitted samples' gamma playback tracks the composed midpoint-then-space theoretical curve // asserting the emitted samples' gamma playback tracks the composed midpoint-then-space theoretical curve
for gradient_interpolation in [GradientInterpolation::SrgbLinear, GradientInterpolation::SrgbGamma] { for (gradient_space, gradient_hue_direction) in [
(GradientSpace::OkLab, GradientHueDirection::Shorter),
(GradientSpace::OkLCh, GradientHueDirection::Shorter),
(GradientSpace::OkLCh, GradientHueDirection::Longer),
(GradientSpace::Lab, GradientHueDirection::Shorter),
(GradientSpace::LCh, GradientHueDirection::Shorter),
(GradientSpace::Hsl, GradientHueDirection::Shorter),
(GradientSpace::Hsl, GradientHueDirection::Longer),
(GradientSpace::Hsv, GradientHueDirection::Shorter),
(GradientSpace::RgbLinear, GradientHueDirection::Shorter),
(GradientSpace::RgbGamma, GradientHueDirection::Shorter),
] {
for &(color_a, color_b) in &color_pairs { for &(color_a, color_b) in &color_pairs {
for midpoint_step in 1..40 { for midpoint_step in 1..40 {
let midpoint = midpoint_step as f64 / 40.; let midpoint = midpoint_step as f64 / 40.;
let mut gradient = Gradient::from(vec![color_a, color_b]); let mut gradient = Gradient::from(vec![color_a, color_b]);
gradient.set_midpoints(&[midpoint, 0.5]); gradient.set_midpoints(&[midpoint, 0.5]);
let samples = gradient.interpolated_samples(gradient_interpolation); let samples = gradient.interpolated_samples(gradient_space, gradient_hue_direction);
for probe in 0..=1000 { for probe in 0..=1000 {
let t = probe as f64 / 1000.; let t = probe as f64 / 1000.;
@@ -1139,11 +1342,11 @@ mod tests {
} }
}; };
let true_color = interpolate_stop_colors(color_a, color_b, apply_midpoint(t, midpoint) as f32, gradient_interpolation); let true_color = interpolate_stop_colors(color_a, color_b, apply_midpoint(t, midpoint) as f32, gradient_space, gradient_hue_direction);
let deviation = max_gamma_channel_deviation(playback, true_color); let deviation = max_gamma_channel_deviation(playback, true_color);
assert!( assert!(
deviation <= 4. / 255., deviation <= 4. / 255.,
"playback deviates {:.1}/255 at t={t} with midpoint {midpoint} in {gradient_interpolation:?} between {color_a:?} and {color_b:?}", "playback deviates {:.1}/255 at t={t} with midpoint {midpoint} in {gradient_space:?} ({gradient_hue_direction:?}) between {color_a:?} and {color_b:?}",
deviation * 255. deviation * 255.
); );
} }
@@ -1156,28 +1359,100 @@ mod tests {
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() { fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear, Default::default()), Color::TRANSPARENT); assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear, Default::default(), Default::default()), Color::TRANSPARENT);
assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear, Default::default()), Color::TRANSPARENT); assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear, Default::default(), Default::default()), Color::TRANSPARENT);
for t in [0., 0.25, 1.] { for t in [0., 0.25, 1.] {
assert_eq!( assert_eq!(
gradient.evaluate(t, GradientSpread::Clear, Default::default()), gradient.evaluate(t, GradientSpread::Clear, Default::default(), Default::default()),
gradient.evaluate(t, GradientSpread::Pad, Default::default()), gradient.evaluate(t, GradientSpread::Pad, Default::default(), Default::default()),
"inside the range Clear must match Pad at t = {t}" "inside the range Clear must match Pad at t = {t}"
); );
} }
} }
#[test] #[test]
fn evaluate_follows_the_interpolation_space() { fn evaluate_follows_the_gradient_space() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
let linear = gradient.evaluate(0.5, Default::default(), GradientInterpolation::SrgbLinear); let oklab = gradient.evaluate(0.5, Default::default(), GradientSpace::OkLab, Default::default());
let gamma = gradient.evaluate(0.5, Default::default(), GradientInterpolation::SrgbGamma); let linear = gradient.evaluate(0.5, Default::default(), GradientSpace::RgbLinear, Default::default());
let gamma = gradient.evaluate(0.5, Default::default(), GradientSpace::RgbGamma, Default::default());
assert_eq!(linear, Color::BLACK.lerp(&Color::WHITE, 0.5)); assert_eq!(linear, Color::BLACK.lerp(&Color::WHITE, 0.5));
assert_eq!(gamma, Color::BLACK.lerp_gamma_srgb(&Color::WHITE, 0.5)); assert_eq!(gamma, Color::BLACK.lerp_gamma_srgb(&Color::WHITE, 0.5));
assert_ne!(linear, gamma, "the two spaces must produce different mid colors between black and white");
// Halfway in OkLab between black and white is lightness 0.5, which cubes to 1/8 linear light in every channel
for channel in [oklab.r(), oklab.g(), oklab.b()] {
assert!((channel - 0.125).abs() < 1e-3, "the OkLab mid color of black and white should be 1/8 linear light, got {channel}");
}
assert_eq!(oklab.a(), 1.);
assert_ne!(linear, gamma, "each space must produce a different mid color between black and white");
assert_ne!(oklab, linear, "each space must produce a different mid color between black and white");
assert_ne!(oklab, gamma, "each space must produce a different mid color between black and white");
}
#[test]
fn polar_spaces_take_the_shorter_hue_arc_and_ignore_powerless_hues() {
use color::ColorSpace;
// Red to blue in HSL crosses through magenta on the shorter arc (300 degrees), not through green (120 degrees)
let red_to_blue = Gradient::from(vec![Color::RED, Color::BLUE]);
let magenta = red_to_blue.evaluate(0.5, Default::default(), GradientSpace::Hsl, Default::default());
for (channel, expected) in [(magenta.r(), 1.), (magenta.g(), 0.), (magenta.b(), 1.)] {
assert!((channel - expected).abs() < 1e-3, "the HSL mid color of red and blue should be magenta, got {magenta:?}");
}
// White's hue is powerless, so an OkLCh interpolation toward it keeps red's hue instead of drifting toward white's arbitrary hue
let red_to_white = Gradient::from(vec![Color::RED, Color::WHITE]);
let pink = red_to_white.evaluate(0.5, Default::default(), GradientSpace::OkLCh, Default::default());
let [_, _, red_hue] = color::Oklch::from_linear_srgb([Color::RED.r(), Color::RED.g(), Color::RED.b()]);
let [_, pink_chroma, pink_hue] = color::Oklch::from_linear_srgb([pink.r(), pink.g(), pink.b()]);
assert!(pink_chroma > 0.05, "the mid color should stay chromatic, got {pink:?}");
assert!((pink_hue - red_hue).abs() < 0.5, "the mid hue should hold red's {red_hue} degrees, got {pink_hue}");
// HSV rides the cube's top face toward white, keeping the mid tint at full brightness where HSL dips
let tint = red_to_white.evaluate(0.5, Default::default(), GradientSpace::Hsv, Default::default());
for (channel, target) in tint.to_gamma_srgb_channels().into_iter().zip([1., 0.5, 0.5, 1.]) {
assert!((channel - target).abs() < 1e-3, "the HSV mid tint of red and white should be gamma (1, 0.5, 0.5), got {tint:?}");
}
// Toward black both saturation and value halve, the classic HSV shade that neither HSL nor HWB produces
let red_to_black = Gradient::from(vec![Color::RED, Color::BLACK]);
let shade = red_to_black.evaluate(0.5, Default::default(), GradientSpace::Hsv, Default::default());
for (channel, target) in shade.to_gamma_srgb_channels().into_iter().zip([0.5, 0.25, 0.25, 1.]) {
assert!((channel - target).abs() < 1e-3, "the HSV mid shade of red and black should be gamma (0.5, 0.25, 0.25), got {shade:?}");
}
}
#[test]
fn hue_direction_chooses_the_arc_around_the_hue_wheel() {
// Red to blue spans 240 degrees upward, so Longer and Increasing agree on the green route
// while Shorter and Decreasing cross through magenta
let red_to_blue = Gradient::from(vec![Color::RED, Color::BLUE]);
let expectations = [
(GradientHueDirection::Shorter, [1., 0., 1.]),
(GradientHueDirection::Longer, [0., 1., 0.]),
(GradientHueDirection::Increasing, [0., 1., 0.]),
(GradientHueDirection::Decreasing, [1., 0., 1.]),
];
for (gradient_hue_direction, expected_rgb) in expectations {
let mid = red_to_blue.evaluate(0.5, Default::default(), GradientSpace::Hsl, gradient_hue_direction);
for (channel, target) in [mid.r(), mid.g(), mid.b()].into_iter().zip(expected_rgb) {
assert!(
(channel - target).abs() < 1e-3,
"the {gradient_hue_direction:?} mid of red and blue should be {expected_rgb:?}, got {mid:?}"
);
}
}
// Identical hues under Longer take a full turn around the wheel, passing through cyan halfway
let red_to_red = Gradient::from(vec![Color::RED, Color::RED]);
let mid = red_to_red.evaluate(0.5, Default::default(), GradientSpace::Hsl, GradientHueDirection::Longer);
for (channel, target) in [mid.r(), mid.g(), mid.b()].into_iter().zip([0., 1., 1.]) {
assert!((channel - target).abs() < 1e-3, "the full-turn mid of red and red should be cyan, got {mid:?}");
}
} }
#[test] #[test]
@@ -1215,13 +1490,17 @@ mod tests {
gradient.set_positions(&[1.5, 0.4, -0.5]); gradient.set_positions(&[1.5, 0.4, -0.5]);
assert_eq!(gradient.positions(), vec![1.5, 0.4, -0.5]); assert_eq!(gradient.positions(), vec![1.5, 0.4, -0.5]);
let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).iter().map(|(position, ..)| *position).collect(); let sample_positions: Vec<f64> = gradient
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
.iter()
.map(|(position, ..)| *position)
.collect();
assert!(sample_positions.windows(2).all(|pair| pair[0] <= pair[1]), "samples must ascend: {sample_positions:?}"); assert!(sample_positions.windows(2).all(|pair| pair[0] <= pair[1]), "samples must ascend: {sample_positions:?}");
assert_eq!(sample_positions.first(), Some(&0.)); assert_eq!(sample_positions.first(), Some(&0.));
assert_eq!(sample_positions.last(), Some(&1.)); assert_eq!(sample_positions.last(), Some(&1.));
assert_eq!(gradient.evaluate(0., Default::default(), Default::default()), Color::RED); assert_eq!(gradient.evaluate(0., Default::default(), Default::default(), Default::default()), Color::RED);
assert_eq!(gradient.evaluate(1., Default::default(), Default::default()), Color::WHITE); assert_eq!(gradient.evaluate(1., Default::default(), Default::default(), Default::default()), Color::WHITE);
} }
#[test] #[test]
@@ -1229,10 +1508,14 @@ mod tests {
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK]); let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK]);
gradient.set_positions(&[f64::INFINITY, f64::NEG_INFINITY]); gradient.set_positions(&[f64::INFINITY, f64::NEG_INFINITY]);
let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).iter().map(|(position, ..)| *position).collect(); let sample_positions: Vec<f64> = gradient
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
.iter()
.map(|(position, ..)| *position)
.collect();
assert_eq!(sample_positions, vec![0., 1.]); assert_eq!(sample_positions, vec![0., 1.]);
assert_eq!(gradient.evaluate(0., Default::default(), Default::default()), Color::BLACK); assert_eq!(gradient.evaluate(0., Default::default(), Default::default(), Default::default()), Color::BLACK);
assert_eq!(gradient.evaluate(1., Default::default(), Default::default()), Color::WHITE); assert_eq!(gradient.evaluate(1., Default::default(), Default::default(), Default::default()), Color::WHITE);
} }
#[test] #[test]
@@ -1240,9 +1523,16 @@ mod tests {
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK, Color::RED]); let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK, Color::RED]);
gradient.set_positions(&[0., f64::NAN, 1.]); gradient.set_positions(&[0., f64::NAN, 1.]);
let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).iter().map(|(position, ..)| *position).collect(); let sample_positions: Vec<f64> = gradient
.interpolated_samples(GradientSpace::RgbGamma, Default::default())
.iter()
.map(|(position, ..)| *position)
.collect();
assert_eq!(sample_positions, vec![0., 1.]); assert_eq!(sample_positions, vec![0., 1.]);
assert_eq!(gradient.evaluate(0.5, Default::default(), Default::default()), Color::WHITE.lerp(&Color::RED, 0.5)); assert_eq!(
gradient.evaluate(0.5, Default::default(), GradientSpace::RgbLinear, Default::default()),
Color::WHITE.lerp(&Color::RED, 0.5)
);
// A non-finite position is preserved as nondefault so write-back elision cannot resurrect the dropped stop // A non-finite position is preserved as nondefault so write-back elision cannot resurrect the dropped stop
assert!(gradient.nondefault_positions().is_some()); assert!(gradient.nondefault_positions().is_some());
@@ -1250,8 +1540,8 @@ mod tests {
// With every position NaN the gradient samples as stopless, painting solid black to signal the upstream bug // With every position NaN the gradient samples as stopless, painting solid black to signal the upstream bug
let mut gradient = Gradient::from(vec![Color::WHITE, Color::RED]); let mut gradient = Gradient::from(vec![Color::WHITE, Color::RED]);
gradient.set_positions(&[f64::NAN, f64::NAN]); gradient.set_positions(&[f64::NAN, f64::NAN]);
assert!(gradient.interpolated_samples(GradientInterpolation::SrgbGamma).is_empty()); assert!(gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default()).is_empty());
assert_eq!(gradient.evaluate(0.5, Default::default(), Default::default()), Color::BLACK); assert_eq!(gradient.evaluate(0.5, Default::default(), Default::default(), Default::default()), Color::BLACK);
} }
#[test] #[test]
@@ -1259,19 +1549,19 @@ mod tests {
let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK]); let mut gradient = Gradient::from(vec![Color::WHITE, Color::BLACK]);
gradient.set_positions(&[0.3, 1.]); gradient.set_positions(&[0.3, 1.]);
let samples = gradient.interpolated_samples(GradientInterpolation::SrgbGamma); let samples = gradient.interpolated_samples(GradientSpace::RgbGamma, Default::default());
assert_eq!(samples[0], (0.3, Color::WHITE, None), "renderers that need a flat lead-in before the first stop add it themselves"); assert_eq!(samples[0], (0.3, Color::WHITE, None), "renderers that need a flat lead-in before the first stop add it themselves");
} }
#[test] #[test]
fn nan_midpoints_read_as_linear() { fn nan_midpoints_read_as_linear() {
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
let linear_result = gradient.evaluate(0.25, Default::default(), Default::default()); let linear_result = gradient.evaluate(0.25, Default::default(), Default::default(), Default::default());
gradient.set_midpoints(&[f64::NAN, f64::NAN]); gradient.set_midpoints(&[f64::NAN, f64::NAN]);
assert_eq!(gradient.evaluate(0.25, Default::default(), Default::default()), linear_result); assert_eq!(gradient.evaluate(0.25, Default::default(), Default::default(), Default::default()), linear_result);
let no_nan_annotations = gradient let no_nan_annotations = gradient
.interpolated_samples(GradientInterpolation::SrgbGamma) .interpolated_samples(GradientSpace::RgbGamma, Default::default())
.iter() .iter()
.all(|(position, _, midpoint)| position.is_finite() && !midpoint.is_some_and(|midpoint| midpoint.is_nan())); .all(|(position, _, midpoint)| position.is_finite() && !midpoint.is_some_and(|midpoint| midpoint.is_nan()));
assert!(no_nan_annotations, "NaN must not escape into rendered sample annotations"); assert!(no_nan_annotations, "NaN must not escape into rendered sample annotations");
+1 -1
View File
@@ -9,7 +9,7 @@ pub mod vector;
// Re-export commonly used types at the crate root // Re-export commonly used types at the crate root
pub use core_types as gcore; pub use core_types as gcore;
pub use gradient::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop}; pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStop};
pub use markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_FORM, ATTR_GRADIENT_SPREAD}; pub use markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_FORM, ATTR_GRADIENT_SPREAD};
pub use math::{QuadExt, RectExt}; pub use math::{QuadExt, RectExt};
pub use subpath::Subpath; pub use subpath::Subpath;
@@ -6,8 +6,10 @@ use core_types::attribute::Attribute;
core_types::attribute! { core_types::attribute! {
/// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, `Repeat`, or `Clear`). /// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, `Repeat`, or `Clear`).
pub GradientSpread("gradient_spread"): crate::gradient::GradientSpread; pub GradientSpread("gradient_spread"): crate::gradient::GradientSpread;
/// Gradient's `GradientInterpolation` (`SrgbLinear` or `SrgbGamma`), the color space its stops blend in. /// Gradient's `GradientSpace`, the color space its stops blend in.
pub GradientInterpolation("gradient_interpolation"): crate::gradient::GradientInterpolation; pub GradientSpace("gradient_space"): crate::gradient::GradientSpace;
/// Gradient's `GradientHueDirection`, the hue path polar spaces interpolate along.
pub GradientHueDirection("gradient_hue_direction"): crate::gradient::GradientHueDirection;
/// Gradient's shape (`Linear` or `Radial`). /// Gradient's shape (`Linear` or `Radial`).
pub GradientForm("gradient_form"): crate::gradient::GradientForm; pub GradientForm("gradient_form"): crate::gradient::GradientForm;
/// Optional `Vector` that overrides the item's own geometry for click-target generation. /// Optional `Vector` that overrides the item's own geometry for click-target generation.
@@ -22,12 +24,14 @@ core_types::attribute! {
core_types::named_value! { core_types::named_value! {
for crate::gradient::GradientSpread; for crate::gradient::GradientSpread;
for crate::gradient::GradientForm; for crate::gradient::GradientForm;
for crate::gradient::GradientInterpolation; for crate::gradient::GradientSpace;
for crate::gradient::GradientHueDirection;
} }
pub const ATTR_GRADIENT_SPREAD: &str = GradientSpread::NAME; pub const ATTR_GRADIENT_SPREAD: &str = GradientSpread::NAME;
pub const ATTR_GRADIENT_FORM: &str = GradientForm::NAME; pub const ATTR_GRADIENT_FORM: &str = GradientForm::NAME;
pub const ATTR_GRADIENT_INTERPOLATION: &str = GradientInterpolation::NAME; pub const ATTR_GRADIENT_SPACE: &str = GradientSpace::NAME;
pub const ATTR_GRADIENT_HUE_DIRECTION: &str = GradientHueDirection::NAME;
pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME; pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME;
#[cfg(test)] #[cfg(test)]
@@ -76,7 +76,7 @@ impl FillChoice<SRGBA8> {
let hex = srgba.to_rgba_hex(); let hex = srgba.to_rgba_hex();
Some(format!("linear-gradient(#{hex}, #{hex})")) Some(format!("linear-gradient(#{hex}, #{hex})"))
} }
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_interpolation)), Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_space, ramp.gradient_hue_direction)),
} }
} }
} }
+5 -1
View File
@@ -14,7 +14,7 @@ use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, wal
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr}; use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector}; use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
use vector_types::gradient::{GradientForm as GradientFormValue, GradientSpread}; use vector_types::gradient::{GradientForm as GradientFormValue, GradientHueDirection, GradientSpace, GradientSpread};
use vector_types::{Gradient, ReferencePoint}; use vector_types::{Gradient, ReferencePoint};
fn arena_exhausted() -> Interrupt { fn arena_exhausted() -> Interrupt {
@@ -257,6 +257,10 @@ attribute_reads! {
read_gradient_form_attribute: GradientFormValue => GradientFormValue; read_gradient_form_attribute: GradientFormValue => GradientFormValue;
/// Reads a named gradient-spread attribute, such as `gradient_spread`. /// Reads a named gradient-spread attribute, such as `gradient_spread`.
read_gradient_spread_attribute: GradientSpread => GradientSpread; read_gradient_spread_attribute: GradientSpread => GradientSpread;
/// Reads a named gradient-space attribute, such as `gradient_space`.
read_gradient_space_attribute: GradientSpace => GradientSpace;
/// Reads a named gradient-hue-direction attribute, such as `gradient_hue_direction`.
read_gradient_hue_direction_attribute: GradientHueDirection => GradientHueDirection;
} }
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
+18 -11
View File
@@ -11,7 +11,7 @@ use math_parser::value::{Number, Value};
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use std::ops::{Add, Mul, Rem, Sub}; use std::ops::{Add, Mul, Rem, Sub};
use vector_types::Gradient; use vector_types::Gradient;
use vector_types::markers::{GradientForm as GradientFormAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpread as GradientSpreadAttr}; use vector_types::markers::{GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr};
/// The struct that stores the context for the maths parser. /// The struct that stores the context for the maths parser.
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs. /// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
@@ -1214,18 +1214,24 @@ fn gradient_spread(_: impl Ctx, gradient: Gradient, gradient_spread: vector_type
(gradient, Attr(gradient_spread)) (gradient, Attr(gradient_spread))
} }
/// Sets the color space each gradient in the input list blends between its stops with: linear light or gamma-encoded sRGB. /// Sets the color space in which each gradient in the input list interpolates between its stops.
#[node_macro::node(category("Gradient"))] #[node_macro::node(category("Gradient"))]
fn gradient_interpolation(_: impl Ctx, gradient: Gradient, gradient_interpolation: vector_types::GradientInterpolation) -> (Gradient, Attr<GradientInterpolationAttr>) { fn gradient_space(_: impl Ctx, gradient: Gradient, gradient_space: vector_types::GradientSpace) -> (Gradient, Attr<GradientSpaceAttr>) {
(gradient, Attr(gradient_interpolation)) (gradient, Attr(gradient_space))
}
/// Sets the hue path each gradient in the input list interpolates along in polar color spaces.
#[node_macro::node(category("Gradient"))]
fn gradient_hue_direction(_: impl Ctx, gradient: Gradient, gradient_hue_direction: vector_types::GradientHueDirection) -> (Gradient, Attr<GradientHueDirectionAttr>) {
(gradient, Attr(gradient_hue_direction))
} }
/// Sets the position of each of a gradient's stops, a factor from 0 to 1 along the gradient. /// 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. /// 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.
#[node_macro::node(category("Gradient"))] #[node_macro::node(category("Gradient"))]
fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: List<f64>) -> Gradient { fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: IList<f64>) -> Gradient {
let positions: Vec<f64> = positions.iter_element_values().copied().collect(); let positions: Vec<f64> = positions.iter().collect();
gradient.set_positions(&positions); gradient.set_positions(&positions);
gradient gradient
} }
@@ -1236,13 +1242,13 @@ fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: List<f64>)
/// ///
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5. /// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5.
#[node_macro::node(category("Gradient"))] #[node_macro::node(category("Gradient"))]
fn gradient_midpoints(_: impl Ctx, mut gradient: Gradient, midpoints: List<f64>) -> Gradient { fn gradient_midpoints(_: impl Ctx, mut gradient: Gradient, midpoints: IList<f64>) -> Gradient {
let midpoints: Vec<f64> = midpoints.iter_element_values().copied().collect(); let midpoints: Vec<f64> = midpoints.iter().collect();
gradient.set_midpoints(&midpoints); gradient.set_midpoints(&midpoints);
gradient gradient
} }
/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear. Colors between stops blend in the gradient's `gradient_interpolation` color space. /// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear. Colors between stops interpolate in the gradient's `gradient_space` color space.
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
fn sample_gradient( fn sample_gradient(
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
@@ -1256,8 +1262,9 @@ fn sample_gradient(
} }
let gradient_spread = gradient.lane(0).attr::<GradientSpreadAttr>(); let gradient_spread = gradient.lane(0).attr::<GradientSpreadAttr>();
let gradient_interpolation = gradient.lane(0).attr::<GradientInterpolationAttr>(); let gradient_space = gradient.lane(0).attr::<GradientSpaceAttr>();
Ok(gradient.element_ref(0).evaluate(position, gradient_spread, gradient_interpolation)) let gradient_hue_direction = gradient.lane(0).attr::<GradientHueDirectionAttr>();
Ok(gradient.element_ref(0).evaluate(position, gradient_spread, gradient_space, gradient_hue_direction))
} }
/// 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. /// 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.
@@ -38,15 +38,15 @@ mod blend_std {
impl Blend<Color> for Gradient { impl Blend<Color> for Gradient {
// TODO: This joining is unfaithful in several ways: it samples only at stop positions so midpoint curves flatten away; // TODO: This joining is unfaithful in several ways: it samples only at stop positions so midpoint curves flatten away;
// it evaluates both sources with the default spread and interpolation rather than their own attributes (which this // it evaluates both sources with the default spread and space rather than their own attributes (which this
// element-level impl cannot read); and the output keeps over's attributes despite being sampled in the default space // element-level impl cannot read); and the output keeps over's attributes despite being sampled in the default space
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut combined_stops = self.positions().into_iter().chain(under.positions()).collect::<Vec<_>>(); let mut combined_stops = self.positions().into_iter().chain(under.positions()).collect::<Vec<_>>();
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6); combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let stops = combined_stops.into_iter().map(|position| { let stops = combined_stops.into_iter().map(|position| {
let over_color = self.evaluate(position, Default::default(), Default::default()); let over_color = self.evaluate(position, Default::default(), Default::default(), Default::default());
let under_color = under.evaluate(position, Default::default(), Default::default()); let under_color = under.evaluate(position, Default::default(), Default::default(), Default::default());
let color = blend_fn(over_color, under_color); let color = blend_fn(over_color, under_color);
GradientStop { position, midpoint: 0.5, color } GradientStop { position, midpoint: 0.5, color }
}); });
+3 -2
View File
@@ -24,13 +24,14 @@ fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash +
return image; return image;
} }
let gradient_spread = gradient.lane(0).attr::<vector_types::markers::GradientSpread>(); let gradient_spread = gradient.lane(0).attr::<vector_types::markers::GradientSpread>();
let gradient_interpolation = gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(); 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 = gradient.element_ref(0); let gradient = gradient.element_ref(0);
image.adjust(|color| { image.adjust(|color| {
let intensity = color.luminance_rec_709(); let intensity = color.luminance_rec_709();
let intensity = if reverse { 1. - intensity } else { intensity }; let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64, gradient_spread, gradient_interpolation) gradient.evaluate(intensity as f64, gradient_spread, gradient_space, gradient_hue_direction)
}); });
image image
+18 -7
View File
@@ -39,13 +39,22 @@ use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles, bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
}; };
use vector_types::vector::style::{DashPattern, Gradient, GradientInterpolation, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::vector::{PointDomain, RegionDomain}; use vector_types::vector::{PointDomain, RegionDomain};
/// The gradient color for one assign-colors position, replaying the /// The gradient color for one assign-colors position, replaying the
/// randomized draws up to it. /// randomized draws up to it.
fn assign_color_at(gradient: &Gradient, gradient_interpolation: vector_types::GradientInterpolation, position: usize, length: usize, randomize: bool, seed: SeedValue, repeat_every: u32) -> Color { fn assign_color_at(
gradient: &Gradient,
gradient_space: vector_types::GradientSpace,
gradient_hue_direction: vector_types::GradientHueDirection,
position: usize,
length: usize,
randomize: bool,
seed: SeedValue,
repeat_every: u32,
) -> Color {
let factor = match randomize { let factor = match randomize {
true => { true => {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -61,7 +70,7 @@ fn assign_color_at(gradient: &Gradient, gradient_interpolation: vector_types::Gr
_ => position as f64 % repeat_every as f64 / (repeat_every - 1) as f64, _ => position as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
}, },
}; };
gradient.evaluate(factor, Default::default(), gradient_interpolation) gradient.evaluate(factor, Default::default(), gradient_space, gradient_hue_direction)
} }
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
@@ -104,7 +113,8 @@ fn assign_colors<'e>(
if gradient.is_empty() { if gradient.is_empty() {
return Ok((content.lane(lane).map_element(element), Attr(existing_fill), Attr(existing_stroke))); return Ok((content.lane(lane).map_element(element), Attr(existing_fill), Attr(existing_stroke)));
} }
let gradient_interpolation = gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(); 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_element = gradient.element_ref(0); let gradient_element = gradient.element_ref(0);
let reversed; let reversed;
let gradient_element = match reverse { let gradient_element = match reverse {
@@ -115,7 +125,7 @@ fn assign_colors<'e>(
false => gradient_element, false => gradient_element,
}; };
let color = assign_color_at(gradient_element, gradient_interpolation, lane, content.len(), randomize, seed, repeat_every); let color = assign_color_at(gradient_element, gradient_space, gradient_hue_direction, lane, content.len(), randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list(); let paint = List::new_from_element(color).into_graphic_list();
let parked = park_paint(ctx.arena(), paint)?; let parked = park_paint(ctx.arena(), paint)?;
@@ -173,7 +183,8 @@ fn assign_colors_graphic<'e>(
if gradient.is_empty() { if gradient.is_empty() {
return Ok(content.lane(lane).map_element(original.clone())); return Ok(content.lane(lane).map_element(original.clone()));
} }
let gradient_interpolation = gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(); 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_element = gradient.element_ref(0); let gradient_element = gradient.element_ref(0);
let reversed; let reversed;
let gradient_element = match reverse { let gradient_element = match reverse {
@@ -219,7 +230,7 @@ fn assign_colors_graphic<'e>(
Some(mut rows) => { Some(mut rows) => {
for row in 0..rows.len() { for row in 0..rows.len() {
let has_stroke = rows.element(row).is_some_and(|vector| vector.stroke.is_some()); let has_stroke = rows.element(row).is_some_and(|vector| vector.stroke.is_some());
let color = assign_color_at(gradient_element, gradient_interpolation, position + row, length, randomize, seed, repeat_every); let color = assign_color_at(gradient_element, gradient_space, gradient_hue_direction, position + row, length, randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list(); let paint = List::new_from_element(color).into_graphic_list();
if fill { if fill {
set_paint_attribute_at(&mut rows, row, ATTR_FILL, paint.clone()); set_paint_attribute_at(&mut rows, row, ATTR_FILL, paint.clone());
+3 -1
View File
@@ -286,7 +286,9 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp
let Some(ty) = field.default_type.as_ref().or_else(|| first_node_io.inputs.get(index)) else { let Some(ty) = field.default_type.as_ref().or_else(|| first_node_io.inputs.get(index)) else {
return NodeInput::value(TaggedValue::None, true); return NodeInput::value(TaggedValue::None, true);
}; };
let exposed = if index == 0 { *ty != fn_type_fut!(Context, ()) } else { field.exposed }; // A unit primary is a placeholder, so it stays hidden in either of its spellings
let unit_primary = *ty == fn_type_fut!(Context, ()) || *ty == registry::record_source_type::<()>();
let exposed = if index == 0 { !unit_primary } else { field.exposed };
match &field.value_source { match &field.value_source {
RegistryValueSource::None => {} RegistryValueSource::None => {}