mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 01:38:11 +08:00
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:
Generated
+1
@@ -6599,6 +6599,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)]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
|
|||||||
use graphene_std::vector::misc::{
|
use graphene_std::vector::misc::{
|
||||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, 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, Context, Graphic};
|
use graphene_std::{Artboard, Color, Context, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
@@ -214,7 +214,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>,
|
||||||
@@ -268,7 +268,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
Item<BlendMode>,
|
Item<BlendMode>,
|
||||||
Item<GradientForm>,
|
Item<GradientForm>,
|
||||||
Item<GradientSpread>,
|
Item<GradientSpread>,
|
||||||
Item<GradientInterpolation>,
|
Item<GradientSpace>,
|
||||||
Item<DashPattern>,
|
Item<DashPattern>,
|
||||||
Item<BoxCorners>,
|
Item<BoxCorners>,
|
||||||
Item<StrokeJoin>,
|
Item<StrokeJoin>,
|
||||||
@@ -1005,7 +1005,7 @@ impl_table_item_layout_for_choice_enum!(
|
|||||||
BlendMode,
|
BlendMode,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientInterpolation,
|
GradientSpace,
|
||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
@@ -1218,7 +1218,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,
|
||||||
|
|||||||
+47
-61
@@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput};
|
|||||||
use graph_craft::list;
|
use graph_craft::list;
|
||||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{Gradient, GradientForm, 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)]
|
||||||
@@ -49,11 +49,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 } => {
|
||||||
@@ -91,9 +92,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 } => {
|
||||||
@@ -488,7 +494,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`.
|
||||||
@@ -529,14 +535,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
|
||||||
@@ -567,9 +573,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,15 +584,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();
|
||||||
@@ -970,8 +976,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());
|
||||||
@@ -995,9 +1001,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"),
|
||||||
};
|
};
|
||||||
@@ -1018,25 +1024,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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1058,26 +1052,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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1091,20 +1077,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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1100,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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use graphene_std::raster::BlendMode;
|
|||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientForm, 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};
|
||||||
|
|
||||||
@@ -432,7 +432,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;
|
||||||
};
|
};
|
||||||
@@ -441,7 +449,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);
|
||||||
@@ -768,16 +777,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ use graphene_std::vector::misc::{
|
|||||||
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{
|
use graphene_std::vector::style::{
|
||||||
FillChoice, Gradient, GradientForm, 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};
|
||||||
@@ -303,7 +304,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(),
|
||||||
@@ -1388,7 +1390,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)
|
||||||
@@ -1561,7 +1563,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)
|
||||||
@@ -2390,7 +2392,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2405,6 +2407,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,
|
||||||
@@ -2435,6 +2439,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,
|
||||||
},
|
},
|
||||||
@@ -2462,9 +2468,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")
|
||||||
@@ -2473,6 +2487,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())
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -2494,8 +2510,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"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::graphic::read_attribute_gradient_spread::IDENTIFIER,
|
node: graphene_std::graphic::read_attribute_gradient_spread::IDENTIFIER,
|
||||||
aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode"],
|
aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode"],
|
||||||
},
|
},
|
||||||
|
NodeReplacement {
|
||||||
|
node: graphene_std::graphic::read_attribute_gradient_space::IDENTIFIER,
|
||||||
|
aliases: &["graphic_nodes::graphic::ReadAttributeGradientInterpolationNode"],
|
||||||
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::graphic::remove_at_index::IDENTIFIER,
|
node: graphene_std::graphic::remove_at_index::IDENTIFIER,
|
||||||
aliases: &["graphic_nodes::graphic::OmitElementNode"],
|
aliases: &["graphic_nodes::graphic::OmitElementNode"],
|
||||||
@@ -274,6 +278,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, ¤t_gradient) {
|
let new_orientation = match (current_layer, ¤t_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()
|
||||||
@@ -2772,11 +2801,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.);
|
||||||
@@ -2799,9 +2828,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.),
|
||||||
@@ -2850,10 +2879,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.);
|
||||||
|
|||||||
@@ -555,7 +555,9 @@ tagged_value! {
|
|||||||
GradientForm(vector::style::GradientForm),
|
GradientForm(vector::style::GradientForm),
|
||||||
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
|
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
|
||||||
GradientSpread(vector::style::GradientSpread),
|
GradientSpread(vector::style::GradientSpread),
|
||||||
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),
|
||||||
@@ -811,7 +813,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)));
|
||||||
@@ -1028,7 +1030,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::*;
|
||||||
|
|
||||||
@@ -1056,26 +1058,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
|
||||||
@@ -1085,7 +1083,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]);
|
||||||
@@ -1099,7 +1097,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.]);
|
||||||
@@ -1111,7 +1109,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));
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use graphene_std::transform::{Footprint, ReferencePoint, ScaleType};
|
|||||||
use graphene_std::vector::misc::{
|
use graphene_std::vector::misc::{
|
||||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{DashPattern, GradientForm, GradientInterpolation, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{DashPattern, GradientForm, GradientHueDirection, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification};
|
||||||
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
||||||
use node_registry_macros::async_node;
|
use node_registry_macros::async_node;
|
||||||
@@ -76,7 +76,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientForm>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientForm>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpread>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpread>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientInterpolation>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpace>]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientHueDirection>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
|
||||||
@@ -111,7 +112,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientForm>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientForm>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpread>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpread>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientInterpolation>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpace>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientHueDirection>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Artboard>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Artboard>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Graphic>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Graphic>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Vector>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Vector>]),
|
||||||
@@ -335,7 +337,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
PaintOrder,
|
PaintOrder,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientInterpolation,
|
GradientSpace,
|
||||||
|
GradientHueDirection,
|
||||||
DashPattern,
|
DashPattern,
|
||||||
BoxCorners,
|
BoxCorners,
|
||||||
MergeByDistanceAlgorithm,
|
MergeByDistanceAlgorithm,
|
||||||
@@ -425,7 +428,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
BlendMode,
|
BlendMode,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientInterpolation
|
GradientSpace,
|
||||||
|
GradientHueDirection
|
||||||
));
|
));
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
node_types.extend(list_dyn_rows!(Raster<GPU>));
|
node_types.extend(list_dyn_rows!(Raster<GPU>));
|
||||||
@@ -541,7 +545,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
attribute_value_node!(Item<BlendMode>),
|
attribute_value_node!(Item<BlendMode>),
|
||||||
attribute_value_node!(Item<GradientForm>),
|
attribute_value_node!(Item<GradientForm>),
|
||||||
attribute_value_node!(Item<GradientSpread>),
|
attribute_value_node!(Item<GradientSpread>),
|
||||||
attribute_value_node!(Item<GradientInterpolation>),
|
attribute_value_node!(Item<GradientSpace>),
|
||||||
|
attribute_value_node!(Item<GradientHueDirection>),
|
||||||
attribute_value_node!(Item<NodeIdPath>),
|
attribute_value_node!(Item<NodeIdPath>),
|
||||||
attribute_value_node!(List<String>),
|
attribute_value_node!(List<String>),
|
||||||
attribute_value_node!(List<Color>),
|
attribute_value_node!(List<Color>),
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ pub use graphene_hash;
|
|||||||
pub use graphene_hash::CacheHash;
|
pub use graphene_hash::CacheHash;
|
||||||
pub use list::{
|
pub use list::{
|
||||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
||||||
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT,
|
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION,
|
||||||
ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
||||||
};
|
};
|
||||||
pub use memo::MemoHash;
|
pub use memo::MemoHash;
|
||||||
pub use no_std_types::AsU32;
|
pub use no_std_types::AsU32;
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ pub const ATTR_CLIP: &str = "clip";
|
|||||||
pub const ATTR_GRADIENT_SPREAD: &str = "gradient_spread";
|
pub const ATTR_GRADIENT_SPREAD: &str = "gradient_spread";
|
||||||
/// Gradient's `GradientForm` (`Linear` or `Radial`).
|
/// Gradient's `GradientForm` (`Linear` or `Radial`).
|
||||||
pub const ATTR_GRADIENT_FORM: &str = "gradient_form";
|
pub const ATTR_GRADIENT_FORM: &str = "gradient_form";
|
||||||
/// Gradient's `GradientInterpolation` (`SrgbLinear` or `SrgbGamma`), the color space its stops blend in.
|
/// Gradient's `GradientSpace`, the color space its stops interpolate in.
|
||||||
pub const ATTR_GRADIENT_INTERPOLATION: &str = "gradient_interpolation";
|
pub const ATTR_GRADIENT_SPACE: &str = "gradient_space";
|
||||||
|
/// Gradient's `GradientHueDirection` (`Shorter`, `Longer`, `Increasing`, or `Decreasing`), which way around the
|
||||||
|
/// hue wheel the stops interpolate when the gradient space is polar.
|
||||||
|
pub const ATTR_GRADIENT_HUE_DIRECTION: &str = "gradient_hue_direction";
|
||||||
/// Gradient stop's `f64` position from 0 to 1 along the gradient, on the `List<Color>` inside a `Gradient`.
|
/// Gradient stop's `f64` position from 0 to 1 along the gradient, on the `List<Color>` inside a `Gradient`.
|
||||||
/// When the attribute is absent, stops distribute evenly across the 0 to 1 range.
|
/// When the attribute is absent, stops distribute evenly across the 0 to 1 range.
|
||||||
pub const ATTR_POSITION: &str = "position";
|
pub const ATTR_POSITION: &str = "position";
|
||||||
|
|||||||
@@ -14,7 +14,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
|
||||||
@@ -149,7 +149,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;
|
||||||
|
|
||||||
@@ -164,7 +164,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) => {
|
||||||
@@ -174,7 +174,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ use crate::{Render, RenderSvgSegmentList, SvgRender};
|
|||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::uuid::generate_uuid;
|
use core_types::uuid::generate_uuid;
|
||||||
use core_types::{ATTR_GRADIENT_FORM, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD, ATTR_TRANSFORM, Color};
|
use core_types::{ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_TRANSFORM, Color};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::Graphic;
|
use graphic_types::Graphic;
|
||||||
use graphic_types::vector_types::gradient::GradientForm;
|
use graphic_types::vector_types::gradient::GradientForm;
|
||||||
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 {
|
||||||
@@ -96,9 +96,10 @@ impl RenderExt for List<Gradient> {
|
|||||||
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
|
||||||
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
||||||
let gradient_interpolation: GradientInterpolation = self.attribute_cloned_or_default(ATTR_GRADIENT_INTERPOLATION, 0);
|
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, 0);
|
||||||
|
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, 0);
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_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");
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use core_types::transform::Footprint;
|
|||||||
use core_types::uuid::{NodeId, generate_uuid};
|
use core_types::uuid::{NodeId, generate_uuid};
|
||||||
use core_types::{
|
use core_types::{
|
||||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
||||||
ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH,
|
ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION,
|
||||||
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||||
};
|
};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
@@ -39,7 +39,7 @@ use std::fmt::Write;
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
use vector_types::gradient::{GradientInterpolation, GradientSpread};
|
use vector_types::gradient::{GradientHueDirection, GradientSpace, GradientSpread};
|
||||||
use vello::*;
|
use vello::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
@@ -420,10 +420,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.));
|
||||||
}
|
}
|
||||||
@@ -508,9 +509,10 @@ fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_trans
|
|||||||
let gradient_form: GradientForm = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
|
let gradient_form: GradientForm = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
|
||||||
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
let gradient_spread: GradientSpread = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
let gradient_spread: GradientSpread = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, 0);
|
||||||
let gradient_interpolation: GradientInterpolation = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_INTERPOLATION, 0);
|
let gradient_space: GradientSpace = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, 0);
|
||||||
|
let gradient_hue_direction: GradientHueDirection = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, 0);
|
||||||
|
|
||||||
let (samples, span) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_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);
|
||||||
|
|
||||||
@@ -2191,7 +2193,8 @@ impl Render for List<Gradient> {
|
|||||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||||
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, index);
|
let gradient_spread: GradientSpread = self.attribute_cloned_or_default(ATTR_GRADIENT_SPREAD, index);
|
||||||
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
let gradient_interpolation: GradientInterpolation = self.attribute_cloned_or_default(ATTR_GRADIENT_INTERPOLATION, index);
|
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index);
|
||||||
|
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index);
|
||||||
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
|
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
|
||||||
render.leaf_tag(tag, |attributes| {
|
render.leaf_tag(tag, |attributes| {
|
||||||
if let Some((min, size)) = thumbnail_rect {
|
if let Some((min, size)) = thumbnail_rect {
|
||||||
@@ -2207,7 +2210,7 @@ impl Render for List<Gradient> {
|
|||||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(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 {
|
||||||
@@ -2289,8 +2292,9 @@ impl Render for List<Gradient> {
|
|||||||
let blend_mode = blend_mode_attr.to_peniko();
|
let blend_mode = blend_mode_attr.to_peniko();
|
||||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||||
|
|
||||||
let gradient_interpolation: GradientInterpolation = self.attribute_cloned_or_default(ATTR_GRADIENT_INTERPOLATION, index);
|
let gradient_space: GradientSpace = self.attribute_cloned_or_default(ATTR_GRADIENT_SPACE, index);
|
||||||
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::VelloRampTexels);
|
let gradient_hue_direction: GradientHueDirection = self.attribute_cloned_or_default(ATTR_GRADIENT_HUE_DIRECTION, index);
|
||||||
|
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_space, gradient_hue_direction, ClearGuardPlacement::VelloRampTexels);
|
||||||
|
|
||||||
let stops = peniko_color_stops(&samples);
|
let stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
@@ -2843,18 +2847,20 @@ mod 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.));
|
||||||
@@ -2869,7 +2875,8 @@ mod 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!(
|
||||||
@@ -2888,7 +2895,8 @@ mod 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.);
|
||||||
@@ -2902,7 +2910,8 @@ mod 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 }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD, ATTR_MIDPOINT, ATTR_POSITION, Item, List};
|
use core_types::list::{ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_MIDPOINT, ATTR_POSITION, Item, List};
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -93,13 +93,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)]
|
||||||
@@ -109,9 +109,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> {
|
||||||
@@ -123,7 +126,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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,8 +169,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
|
||||||
}
|
}
|
||||||
@@ -176,7 +184,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +212,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +223,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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +233,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,11 +285,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.)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,8 +681,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)
|
||||||
@@ -631,7 +763,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.),
|
||||||
@@ -661,7 +793,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,14 +834,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;
|
||||||
@@ -721,15 +853,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.;
|
||||||
@@ -743,7 +875,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,
|
||||||
) {
|
) {
|
||||||
@@ -760,23 +893,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,8 +944,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
|
||||||
@@ -874,24 +1012,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,7 +1152,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]
|
||||||
@@ -1029,24 +1216,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,
|
||||||
@@ -1055,38 +1247,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 {
|
||||||
@@ -1099,11 +1291,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),
|
||||||
@@ -1112,14 +1304,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.;
|
||||||
@@ -1138,11 +1341,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.
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1155,28 +1358,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]
|
||||||
@@ -1214,13 +1489,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]
|
||||||
@@ -1228,10 +1507,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]
|
||||||
@@ -1239,9 +1522,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());
|
||||||
@@ -1249,8 +1539,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]
|
||||||
@@ -1258,19 +1548,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");
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub mod vector;
|
|||||||
|
|
||||||
// Re-export commonly used types at the crate root
|
// Re-export commonly used types at the crate root
|
||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
pub use gradient::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop};
|
pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStop};
|
||||||
pub use math::{QuadExt, RectExt};
|
pub use math::{QuadExt, RectExt};
|
||||||
pub use subpath::Subpath;
|
pub use subpath::Subpath;
|
||||||
pub use vector::Vector;
|
pub use vector::Vector;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl FillChoice<SRGBA8> {
|
|||||||
let hex = srgba.to_rgba_hex();
|
let hex = srgba.to_rgba_hex();
|
||||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||||
}
|
}
|
||||||
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_interpolation)),
|
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_space, ramp.gradient_hue_direction)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rand::SeedableRng;
|
|||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use vector_types::gradient::{GradientForm, GradientInterpolation, GradientSpread};
|
use vector_types::gradient::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread};
|
||||||
use vector_types::{Gradient, ReferencePoint};
|
use vector_types::{Gradient, ReferencePoint};
|
||||||
|
|
||||||
/// Returns the list with the item at the specified index removed.
|
/// Returns the list with the item at the specified index removed.
|
||||||
@@ -747,18 +747,35 @@ fn read_attribute_gradient_spread(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a named `GradientInterpolation` attribute from the input list, outputting each value as an element of a new `GradientInterpolation[]`.
|
/// Reads a named `GradientSpace` attribute from the input list, outputting each value as an element of a new `GradientSpace[]`.
|
||||||
#[node_macro::node(category("Attributes: Read"))]
|
#[node_macro::node(category("Attributes: Read"))]
|
||||||
fn read_attribute_gradient_interpolation(
|
fn read_attribute_gradient_space(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: Item<String>,
|
name: Item<String>,
|
||||||
) -> List<GradientInterpolation> {
|
) -> List<GradientSpace> {
|
||||||
let name = name.into_element();
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<GradientInterpolation>(&name, index) else { continue };
|
let Some(value) = content.attribute::<GradientSpace>(&name, index) else { continue };
|
||||||
|
result.push(Item::new_from_element(*value));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a named `GradientHueDirection` attribute from the input list, outputting each value as an element of a new `GradientHueDirection[]`.
|
||||||
|
#[node_macro::node(category("Attributes: Read"))]
|
||||||
|
fn read_attribute_gradient_hue_direction(
|
||||||
|
_: impl Ctx,
|
||||||
|
content: ListDyn,
|
||||||
|
/// The attribute name (key) to read.
|
||||||
|
name: Item<String>,
|
||||||
|
) -> List<GradientHueDirection> {
|
||||||
|
let name = name.into_element();
|
||||||
|
let mut result = List::with_capacity(content.len());
|
||||||
|
for index in 0..content.len() {
|
||||||
|
let Some(value) = content.attribute::<GradientHueDirection>(&name, index) else { continue };
|
||||||
result.push(Item::new_from_element(*value));
|
result.push(Item::new_from_element(*value));
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
|
|||||||
@@ -1393,11 +1393,19 @@ fn gradient_spread(_: impl Ctx, gradient: Item<Gradient>, gradient_spread: Item<
|
|||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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: Item<Gradient>, gradient_interpolation: Item<vector_types::GradientInterpolation>) -> Item<Gradient> {
|
fn gradient_space(_: impl Ctx, gradient: Item<Gradient>, space: Item<vector_types::GradientSpace>) -> Item<Gradient> {
|
||||||
let mut gradient = gradient;
|
let mut gradient = gradient;
|
||||||
gradient.set_attribute(core_types::ATTR_GRADIENT_INTERPOLATION, *gradient_interpolation.element());
|
gradient.set_attribute(core_types::ATTR_GRADIENT_SPACE, *space.element());
|
||||||
|
gradient
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets which way around the hue wheel each gradient in the input list interpolates, for polar color spaces.
|
||||||
|
#[node_macro::node(category("Gradient"))]
|
||||||
|
fn gradient_hue_direction(_: impl Ctx, gradient: Item<Gradient>, hue_direction: Item<vector_types::GradientHueDirection>) -> Item<Gradient> {
|
||||||
|
let mut gradient = gradient;
|
||||||
|
gradient.set_attribute(core_types::ATTR_GRADIENT_HUE_DIRECTION, *hue_direction.element());
|
||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1425,12 +1433,13 @@ fn gradient_midpoints(_: impl Ctx, gradient: Item<Gradient>, midpoints: List<f64
|
|||||||
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(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
|
fn sample_gradient(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
|
||||||
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
||||||
let gradient_interpolation = gradient.attribute_cloned_or_default::<vector_types::GradientInterpolation>(core_types::ATTR_GRADIENT_INTERPOLATION);
|
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
|
||||||
let color = gradient.element().evaluate(*position.element(), gradient_spread, gradient_interpolation);
|
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
|
||||||
|
let color = gradient.element().evaluate(*position.element(), gradient_spread, gradient_space, gradient_hue_direction);
|
||||||
Item::new_from_element(color)
|
Item::new_from_element(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List};
|
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List};
|
||||||
use core_types::{
|
use core_types::{
|
||||||
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_FORM, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD, ATTR_OPACITY, ATTR_OPACITY_FILL,
|
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, ATTR_OPACITY,
|
||||||
ATTR_TRANSFORM, Color, Ctx,
|
ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx,
|
||||||
};
|
};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
||||||
use graphic_types::vector_types::gradient::{GradientForm, GradientInterpolation, GradientSpread};
|
use graphic_types::vector_types::gradient::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread};
|
||||||
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
|
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
|
||||||
use graphic_types::vector_types::vector::PointId;
|
use graphic_types::vector_types::vector::PointId;
|
||||||
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||||
@@ -290,8 +290,11 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
if let Some(gradient_spread) = attributes.remove::<GradientSpread>(ATTR_GRADIENT_SPREAD) {
|
if let Some(gradient_spread) = attributes.remove::<GradientSpread>(ATTR_GRADIENT_SPREAD) {
|
||||||
gradient_paint.set_attribute(ATTR_GRADIENT_SPREAD, 0, gradient_spread);
|
gradient_paint.set_attribute(ATTR_GRADIENT_SPREAD, 0, gradient_spread);
|
||||||
}
|
}
|
||||||
if let Some(gradient_interpolation) = attributes.remove::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION) {
|
if let Some(gradient_space) = attributes.remove::<GradientSpace>(ATTR_GRADIENT_SPACE) {
|
||||||
gradient_paint.set_attribute(ATTR_GRADIENT_INTERPOLATION, 0, gradient_interpolation);
|
gradient_paint.set_attribute(ATTR_GRADIENT_SPACE, 0, gradient_space);
|
||||||
|
}
|
||||||
|
if let Some(gradient_hue_direction) = attributes.remove::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION) {
|
||||||
|
gradient_paint.set_attribute(ATTR_GRADIENT_HUE_DIRECTION, 0, gradient_hue_direction);
|
||||||
}
|
}
|
||||||
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
|
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
|
||||||
|
|
||||||
|
|||||||
@@ -42,15 +42,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 }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ async fn gradient_map<T: Adjust<Color> + Send>(
|
|||||||
) -> Item<T> {
|
) -> Item<T> {
|
||||||
let mut image = image;
|
let mut image = image;
|
||||||
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
|
||||||
let gradient_interpolation = gradient.attribute_cloned_or_default::<vector_types::GradientInterpolation>(core_types::ATTR_GRADIENT_INTERPOLATION);
|
let gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
|
||||||
|
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
|
||||||
let gradient = gradient.into_element();
|
let gradient = gradient.into_element();
|
||||||
let reverse = reverse.into_element();
|
let reverse = reverse.into_element();
|
||||||
|
|
||||||
image.element_mut().adjust(|color| {
|
image.element_mut().adjust(|color| {
|
||||||
let intensity = color.luminance_rec_709();
|
let intensity = color.luminance_rec_709();
|
||||||
let intensity = if reverse { 1. - intensity } else { intensity };
|
let intensity = if reverse { 1. - intensity } else { intensity };
|
||||||
gradient.evaluate(intensity as f64, gradient_spread, gradient_interpolation)
|
gradient.evaluate(intensity as f64, gradient_spread, gradient_space, gradient_hue_direction)
|
||||||
});
|
});
|
||||||
|
|
||||||
image
|
image
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU};
|
|||||||
use core::hash::{Hash, Hasher};
|
use core::hash::{Hash, Hasher};
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::list::{ATTR_FILL, ATTR_GRADIENT_INTERPOLATION, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
|
use core_types::list::{ATTR_FILL, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_SPACE, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
|
||||||
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
@@ -32,7 +32,7 @@ use vector_types::vector::misc::{
|
|||||||
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
|
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
|
||||||
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
||||||
};
|
};
|
||||||
use vector_types::vector::style::{DashPattern, Gradient, GradientInterpolation, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use vector_types::vector::style::{DashPattern, Gradient, GradientHueDirection, GradientSpace, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
||||||
use vector_types::vector::{PointDomain, RegionDomain};
|
use vector_types::vector::{PointDomain, RegionDomain};
|
||||||
|
|
||||||
@@ -141,7 +141,8 @@ where
|
|||||||
|
|
||||||
let mut content = content;
|
let mut content = content;
|
||||||
let length = content.vector_count();
|
let length = content.vector_count();
|
||||||
let gradient_interpolation = gradient.attribute_cloned_or_default::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION);
|
let gradient_space = gradient.attribute_cloned_or_default::<GradientSpace>(ATTR_GRADIENT_SPACE);
|
||||||
|
let gradient_hue_direction = gradient.attribute_cloned_or_default::<GradientHueDirection>(ATTR_GRADIENT_HUE_DIRECTION);
|
||||||
let element = gradient.into_element();
|
let element = gradient.into_element();
|
||||||
let gradient = if reverse { element.reversed() } else { element };
|
let gradient = if reverse { element.reversed() } else { element };
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The factor spans 0..=1 inclusively, so the spread deliberately stays Pad (Repeat would wrap the final element onto the first stop's color)
|
// The factor spans 0..=1 inclusively, so the spread deliberately stays Pad (Repeat would wrap the final element onto the first stop's color)
|
||||||
let color = gradient.evaluate(factor, Default::default(), gradient_interpolation);
|
let color = gradient.evaluate(factor, Default::default(), gradient_space, gradient_hue_direction);
|
||||||
let paint = List::new_from_element(color).into_graphic_list();
|
let paint = List::new_from_element(color).into_graphic_list();
|
||||||
|
|
||||||
if fill {
|
if fill {
|
||||||
|
|||||||
Reference in New Issue
Block a user