mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 15:38:12 +08:00
Add a gradient interpolation space attribute with sRGB Gamma (existing) and sRGB Linear (new) (#4412)
* Build dropdown menu entries from choice type metadata * Add a gradient interpolation color space attribute, blending stops in linear light by default * Add a Space interpolation dropdown to the color picker popover * Stamp legacy gradient ramps with their implicit gamma interpolation during deserialization * Resolve color-interpolation from SVG style blocks and fix cascade order among repeated declarations
This commit is contained in:
Generated
+1
@@ -2386,6 +2386,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"simplecss",
|
||||||
"spin",
|
"spin",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ vello = "0.9"
|
|||||||
vello_encoding = "0.9"
|
vello_encoding = "0.9"
|
||||||
resvg = "0.47"
|
resvg = "0.47"
|
||||||
usvg = "0.47"
|
usvg = "0.47"
|
||||||
|
simplecss = "0.2"
|
||||||
parley = { version = "0.9", default-features = false, features = ["std"] }
|
parley = { version = "0.9", default-features = false, features = ["std"] }
|
||||||
skrifa = "0.42"
|
skrifa = "0.42"
|
||||||
polycool = "0.4"
|
polycool = "0.4"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ tsify = { workspace = true }
|
|||||||
dyn-any = { workspace = true }
|
dyn-any = { workspace = true }
|
||||||
num_enum = { workspace = true }
|
num_enum = { workspace = true }
|
||||||
usvg = { workspace = true }
|
usvg = { workspace = true }
|
||||||
|
simplecss = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
web-sys = { workspace = true }
|
web-sys = { workspace = true }
|
||||||
vello = { workspace = true }
|
vello = { workspace = true }
|
||||||
|
|||||||
@@ -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, GradientSpread};
|
use graphene_std::vector::style::{FillChoice, GradientInterpolation, 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,6 +49,8 @@ pub enum ColorPickerMessage {
|
|||||||
GradientUpdate { update: SpectrumInputUpdate },
|
GradientUpdate { update: SpectrumInputUpdate },
|
||||||
/// Gradient spread choice from the gradient "Ends" selection.
|
/// Gradient spread choice from the gradient "Ends" selection.
|
||||||
SetGradientSpread { gradient_spread: GradientSpread },
|
SetGradientSpread { gradient_spread: GradientSpread },
|
||||||
|
/// Gradient interpolation choice: the color space the stops blend in, from the "Space" dropdown.
|
||||||
|
SetGradientInterpolation { gradient_interpolation: GradientInterpolation },
|
||||||
|
|
||||||
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
|
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
|
||||||
StartTransaction,
|
StartTransaction,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
|||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::core_types::misc::parse_css_color;
|
use graphene_std::core_types::misc::parse_css_color;
|
||||||
use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientSpread, GradientStops};
|
use graphene_std::vector::style::{FillChoice, Gradient, GradientInterpolation, GradientRamp, 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,6 +30,7 @@ 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,
|
||||||
active_marker_index: Option<u32>,
|
active_marker_index: Option<u32>,
|
||||||
active_marker_is_midpoint: bool,
|
active_marker_is_midpoint: bool,
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ 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(),
|
||||||
active_marker_index: None,
|
active_marker_index: None,
|
||||||
active_marker_is_midpoint: false,
|
active_marker_is_midpoint: false,
|
||||||
allow_none: true,
|
allow_none: true,
|
||||||
@@ -73,12 +75,14 @@ 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.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.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);
|
||||||
@@ -87,6 +91,7 @@ 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;
|
||||||
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);
|
||||||
@@ -199,6 +204,20 @@ 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,
|
||||||
|
..GradientRamp::from(gradient)
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
|
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
|
||||||
|
let Some(gradient) = &self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
self.gradient_interpolation = gradient_interpolation;
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(GradientRamp {
|
||||||
|
gradient_spread: self.gradient_spread,
|
||||||
|
gradient_interpolation,
|
||||||
..GradientRamp::from(gradient)
|
..GradientRamp::from(gradient)
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -290,6 +309,7 @@ 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,
|
||||||
..GradientRamp::from(&*gradient)
|
..GradientRamp::from(&*gradient)
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -420,6 +440,7 @@ 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,
|
||||||
..GradientRamp::from(&gradient)
|
..GradientRamp::from(&gradient)
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -450,6 +471,7 @@ 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)
|
||||||
.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)
|
||||||
@@ -632,6 +654,20 @@ impl ColorPickerMessageHandler {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gradient interpolation color space (only present when the picker is in gradient mode)
|
||||||
|
if self.gradient.is_some() {
|
||||||
|
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into());
|
||||||
|
|
||||||
|
groups.push(LayoutGroup::row(vec![
|
||||||
|
TextLabel::new("Space").tooltip_label("Gradient Interpolation").tooltip_description(SPACE_DESCRIPTION).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
DropdownInput::new(entries)
|
||||||
|
.selected_index(Some(self.gradient_interpolation as u32))
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.widget_instance(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
// Color presets (None / Black / White / pure colors / eyedropper)
|
// Color presets (None / Black / White / pure colors / eyedropper)
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
ColorPresetsInput::default()
|
ColorPresetsInput::default()
|
||||||
@@ -686,6 +722,7 @@ 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.";
|
||||||
|
|
||||||
/// 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_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_interpolation);
|
||||||
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, GradientStops};
|
use graphene_std::vector::style::{FillChoice, GradientInterpolation, 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))]
|
||||||
@@ -151,6 +151,35 @@ impl std::hash::Hash for MenuListEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MenuListEntry {
|
||||||
|
/// One entry per variant of a choice type enum, keeping the choice type's section groupings, shown as the variant's label alongside its icon when it has one.
|
||||||
|
pub fn sections_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> MenuListEntrySections
|
||||||
|
where
|
||||||
|
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
|
||||||
|
{
|
||||||
|
E::list()
|
||||||
|
.iter()
|
||||||
|
.map(|section| {
|
||||||
|
section
|
||||||
|
.iter()
|
||||||
|
.map(|(variant, metadata)| {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
let variant = *variant;
|
||||||
|
|
||||||
|
let entry = MenuListEntry::new(metadata.name)
|
||||||
|
.label(metadata.label)
|
||||||
|
.tooltip_label(metadata.label)
|
||||||
|
.tooltip_description(metadata.description.unwrap_or_default())
|
||||||
|
.on_update(move |_| to_message(variant));
|
||||||
|
|
||||||
|
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
@@ -557,6 +586,9 @@ 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.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub track_interpolation: GradientInterpolation,
|
||||||
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
||||||
#[serde(rename = "trackCSS")]
|
#[serde(rename = "trackCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
|
|||||||
use graphene_std::vector::misc::{
|
use graphene_std::vector::misc::{
|
||||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientRamp, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
||||||
use graphene_std::{Artboard, Color, Graphic};
|
use graphene_std::{Artboard, Color, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
@@ -218,6 +218,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
List<BlendMode>,
|
List<BlendMode>,
|
||||||
List<GradientForm>,
|
List<GradientForm>,
|
||||||
List<GradientSpread>,
|
List<GradientSpread>,
|
||||||
|
List<GradientInterpolation>,
|
||||||
List<DashPattern>,
|
List<DashPattern>,
|
||||||
List<BoxCorners>,
|
List<BoxCorners>,
|
||||||
List<StrokeJoin>,
|
List<StrokeJoin>,
|
||||||
@@ -270,6 +271,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
DAffine2,
|
DAffine2,
|
||||||
BlendMode,
|
BlendMode,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
|
GradientInterpolation,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
DashPattern,
|
DashPattern,
|
||||||
BoxCorners,
|
BoxCorners,
|
||||||
@@ -1009,6 +1011,7 @@ impl_table_item_layout_for_choice_enum!(
|
|||||||
BlendMode,
|
BlendMode,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
|
GradientInterpolation,
|
||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
@@ -1221,6 +1224,7 @@ macro_rules! known_item_types {
|
|||||||
BlendMode,
|
BlendMode,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
|
GradientInterpolation,
|
||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
|
|||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientForm, GradientSpread, Stroke};
|
use graphene_std::vector::style::{GradientForm, GradientInterpolation, 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,6 +30,7 @@ pub enum GraphOperationMessage {
|
|||||||
gradient: Gradient,
|
gradient: Gradient,
|
||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
},
|
},
|
||||||
BlendingFillSet {
|
BlendingFillSet {
|
||||||
@@ -61,6 +62,10 @@ pub enum GraphOperationMessage {
|
|||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
},
|
},
|
||||||
|
GradientInterpolationSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
|
},
|
||||||
OpacitySet {
|
OpacitySet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
opacity: f64,
|
opacity: f64,
|
||||||
|
|||||||
+271
-24
@@ -14,7 +14,7 @@ use graph_craft::document::{NodeId, NodeInput};
|
|||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{Gradient, GradientForm, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{Gradient, GradientForm, GradientInterpolation, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::{Artboard, Color};
|
use graphene_std::{Artboard, Color};
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
@@ -50,10 +50,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
gradient,
|
gradient,
|
||||||
gradient_form,
|
gradient_form,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
|
gradient_interpolation,
|
||||||
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, transform);
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
||||||
@@ -91,6 +92,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.gradient_spread_set(gradient_spread);
|
modify_inputs.gradient_spread_set(gradient_spread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_interpolation_set(gradient_interpolation);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.opacity_set(opacity);
|
modify_inputs.opacity_set(opacity);
|
||||||
@@ -481,18 +487,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
};
|
};
|
||||||
placement_transform.translation = placement_transform.translation.round();
|
placement_transform.translation = placement_transform.translation.round();
|
||||||
|
|
||||||
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
|
let gradient_info = SvgGradientInfo {
|
||||||
|
graphite_stops: extract_graphite_gradient_stops(&svg),
|
||||||
|
interpolations: extract_gradient_interpolations(&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`.
|
||||||
// The placement offset is then applied once to the root group layer below.
|
// The placement offset is then applied once to the root group layer below.
|
||||||
import_usvg_node(
|
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), id, parent, insert_index, &gradient_info);
|
||||||
&mut modify_inputs,
|
|
||||||
&usvg::Node::Group(Box::new(tree.root().clone())),
|
|
||||||
id,
|
|
||||||
parent,
|
|
||||||
insert_index,
|
|
||||||
&graphite_gradient_stops,
|
|
||||||
);
|
|
||||||
|
|
||||||
// After import, `layer_node` is set to the root group. Apply the placement transform to it
|
// After import, `layer_node` is set to the root group. Apply the placement transform to it
|
||||||
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
|
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
|
||||||
@@ -524,6 +526,131 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
|
|||||||
|
|
||||||
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
||||||
|
|
||||||
|
/// Gradient information pre-parsed from the raw SVG XML, carrying what usvg's simplified tree drops.
|
||||||
|
struct SvgGradientInfo {
|
||||||
|
/// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data.
|
||||||
|
graphite_stops: HashMap<String, Gradient>,
|
||||||
|
/// Interpolation spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
|
||||||
|
interpolations: HashMap<String, GradientInterpolation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value.
|
||||||
|
fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpolation> {
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
|
// Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default
|
||||||
|
if !svg.contains("color-interpolation") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = match usvg::roxmltree::Document::parse(svg) {
|
||||||
|
Ok(doc) => doc,
|
||||||
|
Err(_) => return result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The document's `<style>` blocks apply to every element, so parse them once up front
|
||||||
|
let mut stylesheet = simplecss::StyleSheet::new();
|
||||||
|
for style_element in doc.descendants().filter(|node| node.tag_name().name() == "style") {
|
||||||
|
if !matches!(style_element.attribute("type"), None | Some("") | Some("text/css")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for text in style_element.children().filter(|child| child.is_text()).filter_map(|child| child.text()) {
|
||||||
|
stylesheet.parse_more(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in doc.descendants() {
|
||||||
|
match node.tag_name().name() {
|
||||||
|
"linearGradient" | "radialGradient" => {}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(gradient_id) = node.attribute("id")
|
||||||
|
&& let Some(gradient_interpolation) = resolve_color_interpolation(node, &stylesheet)
|
||||||
|
{
|
||||||
|
result.insert(gradient_id.to_string(), gradient_interpolation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientInterpolation> {
|
||||||
|
let mut next = Some(element);
|
||||||
|
|
||||||
|
while let Some(element) = next {
|
||||||
|
match declared_color_interpolation(element, stylesheet) {
|
||||||
|
Some("linearRGB") => return Some(GradientInterpolation::SrgbLinear),
|
||||||
|
// `inherit` defers to the ancestors like an undeclared element
|
||||||
|
Some("inherit") | None => {}
|
||||||
|
Some(_) => return Some(GradientInterpolation::SrgbGamma),
|
||||||
|
}
|
||||||
|
|
||||||
|
next = element.parent_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The winning `color-interpolation` declaration on a single element per the CSS cascade: `!important` declarations
|
||||||
|
/// beat normal ones, the inline `style` beats the `<style>` rules (already specificity-sorted, so their last match
|
||||||
|
/// wins), and the presentation attribute yields to them all. Later declarations win priority ties.
|
||||||
|
fn declared_color_interpolation<'a>(element: usvg::roxmltree::Node<'a, '_>, stylesheet: &simplecss::StyleSheet<'a>) -> Option<&'a str> {
|
||||||
|
let mut winner: Option<(u8, &'a str)> = None;
|
||||||
|
let mut consider = |priority: u8, value: &'a str| {
|
||||||
|
if winner.is_none_or(|(existing, _)| priority >= existing) {
|
||||||
|
winner = Some((priority, value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = element.attribute("color-interpolation") {
|
||||||
|
consider(0, value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
for rule in stylesheet.rules.iter().filter(|rule| rule.selector.matches(&CssElement(element))) {
|
||||||
|
for declaration in rule.declarations.iter().filter(|declaration| declaration.name == "color-interpolation") {
|
||||||
|
consider(if declaration.important { 3 } else { 1 }, declaration.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = element.attribute("style") {
|
||||||
|
for declaration in simplecss::DeclarationTokenizer::from(style).filter(|declaration| declaration.name == "color-interpolation") {
|
||||||
|
consider(if declaration.important { 4 } else { 2 }, declaration.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
winner.map(|(_, value)| value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adapts a roxmltree element to simplecss's selector-matching interface.
|
||||||
|
struct CssElement<'a, 'input>(usvg::roxmltree::Node<'a, 'input>);
|
||||||
|
|
||||||
|
impl simplecss::Element for CssElement<'_, '_> {
|
||||||
|
fn parent_element(&self) -> Option<Self> {
|
||||||
|
self.0.parent_element().map(CssElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_sibling_element(&self) -> Option<Self> {
|
||||||
|
self.0.prev_sibling_element().map(CssElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_local_name(&self, local_name: &str) -> bool {
|
||||||
|
self.0.tag_name().name() == local_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool {
|
||||||
|
self.0.attribute(local_name).is_some_and(|value| operator.matches(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool {
|
||||||
|
matches!(class, simplecss::PseudoClass::FirstChild) && self.0.prev_sibling_element().is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
||||||
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
||||||
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
||||||
@@ -598,7 +725,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
|||||||
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
||||||
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
||||||
/// the known tree structure.
|
/// the known tree structure.
|
||||||
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, graphite_gradient_stops: &HashMap<String, Gradient>) {
|
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, gradient_info: &SvgGradientInfo) {
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
|
|
||||||
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||||
@@ -618,7 +745,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||||||
modify_inputs.import = true;
|
modify_inputs.import = true;
|
||||||
|
|
||||||
for child in group.children() {
|
for child in group.children() {
|
||||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map);
|
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, &mut group_extents_map);
|
||||||
child_extents_svg_order.push(extent);
|
child_extents_svg_order.push(extent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,7 +764,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||||||
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
|
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
|
||||||
}
|
}
|
||||||
usvg::Node::Path(path) => {
|
usvg::Node::Path(path) => {
|
||||||
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
|
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
|
||||||
}
|
}
|
||||||
usvg::Node::Image(_image) => {
|
usvg::Node::Image(_image) => {
|
||||||
warn!("Skip image");
|
warn!("Skip image");
|
||||||
@@ -661,7 +788,7 @@ fn import_usvg_node_inner(
|
|||||||
id: NodeId,
|
id: NodeId,
|
||||||
parent: LayerNodeIdentifier,
|
parent: LayerNodeIdentifier,
|
||||||
insert_index: usize,
|
insert_index: usize,
|
||||||
graphite_gradient_stops: &HashMap<String, Gradient>,
|
gradient_info: &SvgGradientInfo,
|
||||||
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
@@ -672,7 +799,7 @@ fn import_usvg_node_inner(
|
|||||||
usvg::Node::Group(group) => {
|
usvg::Node::Group(group) => {
|
||||||
let mut child_extents: Vec<u32> = Vec::new();
|
let mut child_extents: Vec<u32> = Vec::new();
|
||||||
for child in group.children() {
|
for child in group.children() {
|
||||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map);
|
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, group_extents_map);
|
||||||
child_extents.push(extent);
|
child_extents.push(extent);
|
||||||
}
|
}
|
||||||
modify_inputs.layer_node = Some(layer);
|
modify_inputs.layer_node = Some(layer);
|
||||||
@@ -687,7 +814,7 @@ fn import_usvg_node_inner(
|
|||||||
total_extent
|
total_extent
|
||||||
}
|
}
|
||||||
usvg::Node::Path(path) => {
|
usvg::Node::Path(path) => {
|
||||||
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
|
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
usvg::Node::Image(_image) => {
|
usvg::Node::Image(_image) => {
|
||||||
@@ -704,7 +831,7 @@ fn import_usvg_node_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
||||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, Gradient>) {
|
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
|
||||||
let subpaths = convert_usvg_path(path);
|
let subpaths = convert_usvg_path(path);
|
||||||
|
|
||||||
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
||||||
@@ -718,7 +845,7 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(fill) = path.fill() {
|
if let Some(fill) = path.fill() {
|
||||||
apply_usvg_fill(fill, modify_inputs, graphite_gradient_stops);
|
apply_usvg_fill(fill, modify_inputs, gradient_info);
|
||||||
}
|
}
|
||||||
if let Some(stroke) = path.stroke() {
|
if let Some(stroke) = path.stroke() {
|
||||||
apply_usvg_stroke(stroke, modify_inputs, node_transform);
|
apply_usvg_stroke(stroke, modify_inputs, node_transform);
|
||||||
@@ -819,7 +946,7 @@ fn convert_gradient_spread(spread_method: usvg::SpreadMethod) -> GradientSpread
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, Gradient>) {
|
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, gradient_info: &SvgGradientInfo) {
|
||||||
match &fill.paint() {
|
match &fill.paint() {
|
||||||
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
||||||
usvg::Paint::LinearGradient(linear) => {
|
usvg::Paint::LinearGradient(linear) => {
|
||||||
@@ -831,7 +958,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
|
|
||||||
let gradient_form = GradientForm::Linear;
|
let gradient_form = GradientForm::Linear;
|
||||||
|
|
||||||
let gradient = match graphite_gradient_stops.get(linear.id()) {
|
let gradient = match gradient_info.graphite_stops.get(linear.id()) {
|
||||||
Some(graphite_stops) => graphite_stops.clone(),
|
Some(graphite_stops) => graphite_stops.clone(),
|
||||||
None => {
|
None => {
|
||||||
let stops = linear.stops().iter().map(|stop| GradientStop {
|
let stops = linear.stops().iter().map(|stop| GradientStop {
|
||||||
@@ -843,7 +970,9 @@ 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());
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, transform);
|
// 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);
|
||||||
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
|
||||||
}
|
}
|
||||||
usvg::Paint::RadialGradient(radial) => {
|
usvg::Paint::RadialGradient(radial) => {
|
||||||
let gradient_transform = usvg_transform(radial.transform());
|
let gradient_transform = usvg_transform(radial.transform());
|
||||||
@@ -855,7 +984,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
|
|
||||||
let gradient_form = GradientForm::Radial;
|
let gradient_form = GradientForm::Radial;
|
||||||
|
|
||||||
let gradient = match graphite_gradient_stops.get(radial.id()) {
|
let gradient = match gradient_info.graphite_stops.get(radial.id()) {
|
||||||
Some(graphite_stops) => graphite_stops.clone(),
|
Some(graphite_stops) => graphite_stops.clone(),
|
||||||
None => {
|
None => {
|
||||||
let stops = radial.stops().iter().map(|stop| GradientStop {
|
let stops = radial.stops().iter().map(|stop| GradientStop {
|
||||||
@@ -867,9 +996,127 @@ 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);
|
||||||
|
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, transform);
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
|
||||||
}
|
}
|
||||||
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="inherited"/>
|
||||||
|
<linearGradient id="attribute" color-interpolation="sRGB"/>
|
||||||
|
<linearGradient id="styled" color-interpolation="sRGB" style="fill: red; color-interpolation: linearRGB"/>
|
||||||
|
<radialGradient id="auto" color-interpolation="auto"/>
|
||||||
|
</defs>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let interpolations = extract_gradient_interpolations(svg);
|
||||||
|
assert_eq!(
|
||||||
|
interpolations.get("inherited"),
|
||||||
|
Some(&GradientInterpolation::SrgbLinear),
|
||||||
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_interpolation_reads_style_blocks_with_selector_specificity() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
linearGradient { color-interpolation: linearRGB }
|
||||||
|
.classy { color-interpolation: sRGB }
|
||||||
|
#exact { color-interpolation: linearRGB }
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="from-type-rule"/>
|
||||||
|
<linearGradient id="from-class-rule" class="classy"/>
|
||||||
|
<linearGradient id="exact" class="classy"/>
|
||||||
|
<linearGradient id="inline-beats-rules" class="classy" style="color-interpolation: linearRGB"/>
|
||||||
|
<linearGradient id="rule-beats-attribute" class="classy" color-interpolation="linearRGB"/>
|
||||||
|
</defs>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let interpolations = extract_gradient_interpolations(svg);
|
||||||
|
assert_eq!(
|
||||||
|
interpolations.get("from-type-rule"),
|
||||||
|
Some(&GradientInterpolation::SrgbLinear),
|
||||||
|
"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"
|
||||||
|
);
|
||||||
|
assert_eq!(interpolations.get("exact"), Some(&GradientInterpolation::SrgbLinear), "the ID rule should outrank the class rule");
|
||||||
|
assert_eq!(
|
||||||
|
interpolations.get("inline-beats-rules"),
|
||||||
|
Some(&GradientInterpolation::SrgbLinear),
|
||||||
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_and_important_declarations_resolve_by_cascade_order() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>.forced { color-interpolation: linearRGB !important }</style>
|
||||||
|
<linearGradient id="last-declaration-wins" style="color-interpolation: sRGB; color-interpolation: linearRGB"/>
|
||||||
|
<linearGradient id="important-beats-later" style="color-interpolation: linearRGB !important; color-interpolation: sRGB"/>
|
||||||
|
<linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let interpolations = extract_gradient_interpolations(svg);
|
||||||
|
assert_eq!(
|
||||||
|
interpolations.get("last-declaration-wins"),
|
||||||
|
Some(&GradientInterpolation::SrgbLinear),
|
||||||
|
"the last of repeated inline declarations should win"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
interpolations.get("important-beats-later"),
|
||||||
|
Some(&GradientInterpolation::SrgbLinear),
|
||||||
|
"an `!important` declaration should beat a later normal one"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
interpolations.get("important-rule-beats-inline"),
|
||||||
|
Some(&GradientInterpolation::SrgbLinear),
|
||||||
|
"an `!important` style block rule should beat the inline style"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_interpolation_yields_nothing_when_never_declared() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
extract_gradient_interpolations(svg).is_empty(),
|
||||||
|
"gradients without any declaration should fall back to the caller's gamma default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use graphene_std::raster::BlendMode;
|
|||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientForm, GradientSpread, Stroke};
|
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke};
|
||||||
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
|
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
|
||||||
use graphene_std::{Artboard, Color, Graphic};
|
use graphene_std::{Artboard, Color, Graphic};
|
||||||
|
|
||||||
@@ -433,14 +433,18 @@ 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, transform: DAffine2) {
|
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, gradient_spread: GradientSpread, gradient_interpolation: GradientInterpolation, transform: DAffine2) {
|
||||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
||||||
|
|
||||||
let ramp = GradientRamp::from(gradient);
|
let ramp = GradientRamp::from(gradient);
|
||||||
let ramp = GradientRamp { gradient_spread, ..ramp };
|
let ramp = GradientRamp {
|
||||||
|
gradient_spread,
|
||||||
|
gradient_interpolation,
|
||||||
|
..ramp
|
||||||
|
};
|
||||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
|
||||||
|
|
||||||
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
||||||
@@ -605,10 +609,9 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Only the stops are being replaced, so the ramp's other settings stay as the value node already holds them
|
// Only the stops are being replaced, so the ramp's other settings stay as the value node already holds them
|
||||||
let gradient_spread = self.gradient_value_ramp(gradient_value_id).unwrap_or_default().gradient_spread;
|
|
||||||
let ramp = GradientRamp {
|
let ramp = GradientRamp {
|
||||||
gradient_spread,
|
stops: (&stops).into(),
|
||||||
..GradientRamp::from(stops)
|
..self.gradient_value_ramp(gradient_value_id).unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -766,6 +769,20 @@ 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
|
||||||
|
/// 'Gradient Interpolation' node: that one is a user-authored procedural override, not something the tools manage.
|
||||||
|
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
|
let ramp = GradientRamp { gradient_interpolation, ..ramp };
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||||
let clip = !clip_mode.unwrap_or(false);
|
let clip = !clip_mode.unwrap_or(false);
|
||||||
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ 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, GradientRamp, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
|
FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||||
use graphene_std::{NodeParameter, ParameterRef};
|
use graphene_std::{NodeParameter, ParameterRef};
|
||||||
@@ -294,6 +294,7 @@ 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::<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(),
|
||||||
@@ -1389,6 +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)
|
||||||
.markers(spectrum_markers)
|
.markers(spectrum_markers)
|
||||||
.show_midpoints(false)
|
.show_midpoints(false)
|
||||||
.allow_insert(false)
|
.allow_insert(false)
|
||||||
@@ -1561,6 +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)
|
||||||
.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)
|
||||||
@@ -2907,24 +2910,22 @@ pub mod choice {
|
|||||||
U: Fn(&E) -> Message + 'static + Send + Sync,
|
U: Fn(&E) -> Message + 'static + Send + Sync,
|
||||||
C: Fn(&()) -> Message + 'static + Send + Sync,
|
C: Fn(&()) -> Message + 'static + Send + Sync,
|
||||||
{
|
{
|
||||||
let items = E::list()
|
let updater = std::sync::Arc::new(updater_factory());
|
||||||
.iter()
|
let committer = std::sync::Arc::new(committer_factory());
|
||||||
|
|
||||||
|
let items = MenuListEntry::sections_from_choice_type(move |variant: E| updater(&variant))
|
||||||
|
.into_iter()
|
||||||
.map(|section| {
|
.map(|section| {
|
||||||
section
|
section
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(|(item, metadata)| {
|
.map(|entry| {
|
||||||
let updater = updater_factory();
|
let committer = committer.clone();
|
||||||
let committer = committer_factory();
|
entry.on_commit(move |value| committer(value))
|
||||||
MenuListEntry::new(metadata.name)
|
|
||||||
.label(metadata.label)
|
|
||||||
.tooltip_label(metadata.label)
|
|
||||||
.tooltip_description(metadata.description.unwrap_or_default())
|
|
||||||
.on_update(move |_| updater(item))
|
|
||||||
.on_commit(committer)
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
DropdownInput::new(items).disabled(self.disabled).selected_index(Some(current.as_u32())).widget_instance()
|
DropdownInput::new(items).disabled(self.disabled).selected_index(Some(current.as_u32())).widget_instance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -807,6 +807,11 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
|
|||||||
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
|
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
|
||||||
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!(
|
||||||
|
ramp.gradient_interpolation,
|
||||||
|
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
|
||||||
|
"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);
|
||||||
assert_eq!(stops.len(), 2);
|
assert_eq!(stops.len(), 2);
|
||||||
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
|
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
|
||||||
@@ -844,12 +849,22 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
|
|||||||
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
|
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
|
||||||
};
|
};
|
||||||
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!(
|
||||||
|
ramp.gradient_interpolation,
|
||||||
|
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
|
||||||
|
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
|
||||||
|
);
|
||||||
|
|
||||||
let backup = fill_node.input_value(graphene_std::vector::fill::BackupGradientInput);
|
let backup = fill_node.input_value(graphene_std::vector::fill::BackupGradientInput);
|
||||||
let Some(TaggedValue::GradientRamp(backup_ramp)) = backup else {
|
let Some(TaggedValue::GradientRamp(backup_ramp)) = backup else {
|
||||||
panic!("the backup gradient input should keep its gradient ramp value, but became {backup:?}");
|
panic!("the backup gradient input should keep its gradient ramp value, but became {backup:?}");
|
||||||
};
|
};
|
||||||
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!(
|
||||||
|
backup_ramp.gradient_interpolation,
|
||||||
|
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
|
||||||
|
"the backup ramp should carry the explicit gamma interpolation too"
|
||||||
|
);
|
||||||
|
|
||||||
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
|
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -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, GradientSpread, PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PointId, SegmentId, VectorModificationType};
|
||||||
use graphene_std::{NodeParameter, ParameterRef};
|
use graphene_std::{NodeParameter, ParameterRef};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@@ -388,13 +388,23 @@ pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &No
|
|||||||
Some(*node_id)
|
Some(*node_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The spread baked into the 'Gradient Value' node feeding a layer's chain.
|
/// The ramp held by the 'Gradient Value' node feeding a layer's chain, which carries the whole-ramp settings.
|
||||||
pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpread> {
|
fn get_chain_source_gradient_ramp<'a>(layer: LayerNodeIdentifier, network_interface: &'a NodeNetworkInterface) -> Option<&'a GradientRamp> {
|
||||||
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
||||||
let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
|
let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(ramp.gradient_spread)
|
Some(ramp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The spread baked into the 'Gradient Value' node feeding a layer's chain.
|
||||||
|
pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpread> {
|
||||||
|
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The interpolation 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> {
|
||||||
|
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_interpolation)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the gradient stops of a layer, if any.
|
/// Get the gradient stops of a layer, if any.
|
||||||
@@ -752,6 +762,7 @@ 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 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,
|
||||||
@@ -765,6 +776,7 @@ 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 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,
|
||||||
@@ -782,6 +794,7 @@ 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,
|
||||||
transform,
|
transform,
|
||||||
transform_is_value: transform_input.is_some(),
|
transform_is_value: transform_input.is_some(),
|
||||||
})
|
})
|
||||||
@@ -931,6 +944,7 @@ 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,
|
||||||
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_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input,
|
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,
|
||||||
replaceable_paint_chain, reverse_direction_tooltip_description,
|
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, GradientRamp, GradientSpread, GradientStop, build_transform_with_y_preservation};
|
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop, build_transform_with_y_preservation};
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
pub struct GradientTool {
|
pub struct GradientTool {
|
||||||
@@ -30,6 +30,7 @@ pub struct GradientTool {
|
|||||||
pub struct GradientOptions {
|
pub struct GradientOptions {
|
||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[impl_message(Message, ToolMessage, Gradient)]
|
#[impl_message(Message, ToolMessage, Gradient)]
|
||||||
@@ -138,7 +139,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
|
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
|
||||||
let ramp = GradientRamp::from(&ramp);
|
let ramp = GradientRamp::from(&ramp);
|
||||||
self.options.gradient_spread = ramp.gradient_spread;
|
self.options.gradient_spread = ramp.gradient_spread;
|
||||||
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.gradient_spread);
|
self.options.gradient_interpolation = ramp.gradient_interpolation;
|
||||||
|
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.gradient_spread, ramp.gradient_interpolation);
|
||||||
}
|
}
|
||||||
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
||||||
if self.data.color_picker_transaction_open {
|
if self.data.color_picker_transaction_open {
|
||||||
@@ -176,6 +178,10 @@ 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 {
|
||||||
|
self.options.gradient_interpolation = appearance.gradient_interpolation;
|
||||||
|
needs_refresh = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_gradient = current_gradient.is_some();
|
let has_gradient = current_gradient.is_some();
|
||||||
@@ -256,6 +262,7 @@ impl LayoutHolder for GradientTool {
|
|||||||
});
|
});
|
||||||
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
|
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
|
||||||
gradient_spread: self.options.gradient_spread,
|
gradient_spread: self.options.gradient_spread,
|
||||||
|
gradient_interpolation: self.options.gradient_interpolation,
|
||||||
..GradientRamp::from(&stops_value)
|
..GradientRamp::from(&stops_value)
|
||||||
}))
|
}))
|
||||||
.allow_none(false)
|
.allow_none(false)
|
||||||
@@ -356,6 +363,7 @@ 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,
|
||||||
transform: gradient.transform,
|
transform: gradient.transform,
|
||||||
},
|
},
|
||||||
GradientSource::Direct,
|
GradientSource::Direct,
|
||||||
@@ -375,6 +383,7 @@ struct GradientAppearance {
|
|||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
gradient_form: GradientForm,
|
gradient_form: GradientForm,
|
||||||
gradient_spread: GradientSpread,
|
gradient_spread: GradientSpread,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -415,6 +424,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,6 +761,7 @@ 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,
|
||||||
transform: self.appearance.transform,
|
transform: self.appearance.transform,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -789,6 +800,10 @@ 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 {
|
||||||
|
layer,
|
||||||
|
gradient_interpolation: appearance.gradient_interpolation,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientTool {
|
impl GradientTool {
|
||||||
@@ -1468,6 +1483,7 @@ 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,
|
||||||
},
|
},
|
||||||
// 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() {
|
||||||
@@ -1826,6 +1842,7 @@ 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,
|
||||||
transform: appearance.transform,
|
transform: appearance.transform,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1849,7 +1866,14 @@ fn apply_gradient_update(
|
|||||||
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
||||||
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
||||||
/// single undo entry by the surrounding 'on_commit' callback.
|
/// single undo entry by the surrounding 'on_commit' callback.
|
||||||
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient, gradient_spread: GradientSpread) {
|
fn apply_stops_update(
|
||||||
|
data: &mut GradientToolData,
|
||||||
|
context: &mut ToolActionMessageContext,
|
||||||
|
responses: &mut VecDeque<Message>,
|
||||||
|
new_gradient: Gradient,
|
||||||
|
gradient_spread: GradientSpread,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
|
) {
|
||||||
let selected_layers: Vec<_> = context
|
let selected_layers: Vec<_> = context
|
||||||
.document
|
.document
|
||||||
.network_interface
|
.network_interface
|
||||||
@@ -1866,6 +1890,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||||||
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 });
|
||||||
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 {
|
||||||
@@ -1873,6 +1898,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||||||
gradient: new_gradient.clone(),
|
gradient: new_gradient.clone(),
|
||||||
gradient_form: appearance.gradient_form,
|
gradient_form: appearance.gradient_form,
|
||||||
gradient_spread,
|
gradient_spread,
|
||||||
|
gradient_interpolation,
|
||||||
transform: appearance.transform,
|
transform: appearance.transform,
|
||||||
});
|
});
|
||||||
updated_any_layer = true;
|
updated_any_layer = true;
|
||||||
@@ -1882,6 +1908,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@@ -614,6 +614,7 @@ 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),
|
||||||
ReferencePoint(vector::ReferencePoint),
|
ReferencePoint(vector::ReferencePoint),
|
||||||
CentroidType(vector::misc::CentroidType),
|
CentroidType(vector::misc::CentroidType),
|
||||||
BooleanOperation(vector::misc::BooleanOperation),
|
BooleanOperation(vector::misc::BooleanOperation),
|
||||||
@@ -862,11 +863,15 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
|
|||||||
.and_then(|c| c.get("element").or_else(|| c.get("instance")).or_else(|| c.get("instances")))
|
.and_then(|c| c.get("element").or_else(|| c.get("instance")).or_else(|| c.get("instances")))
|
||||||
.and_then(|element| element.as_array());
|
.and_then(|element| element.as_array());
|
||||||
|
|
||||||
// An empty legacy table wrapper carries no gradient, degrading to the default rather than failing the document load
|
// An empty legacy table wrapper carries no gradient, degrading to the default (in the era's gamma) rather than failing the document load
|
||||||
if let Some(array) = table_element
|
if let Some(array) = table_element
|
||||||
&& array.is_empty()
|
&& array.is_empty()
|
||||||
{
|
{
|
||||||
return Ok(MemoHash::new(TaggedValue::GradientRamp(GradientRamp::default())));
|
let ramp = GradientRamp {
|
||||||
|
gradient_interpolation: vector::style::GradientInterpolation::SrgbGamma,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = table_element.and_then(|array| array.first()).unwrap_or(content);
|
let payload = table_element.and_then(|array| array.first()).unwrap_or(content);
|
||||||
@@ -1088,7 +1093,7 @@ mod paint_default_parsing {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod gradient_shape_migration {
|
mod gradient_shape_migration {
|
||||||
use graphic_types::vector_types::GradientSpread;
|
use graphic_types::vector_types::{GradientInterpolation, GradientSpread};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -1115,9 +1120,29 @@ 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!(
|
||||||
|
json.get("GradientRamp").and_then(|payload| payload.get("gradient_interpolation")),
|
||||||
|
Some(&serde_json::json!("SrgbLinear")),
|
||||||
|
"the interpolation 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
|
||||||
|
#[test]
|
||||||
|
fn ramp_without_interpolation_field_reads_as_legacy_gamma() {
|
||||||
|
let json = serde_json::json!({ "GradientRamp": { "stops": { "color": [white(), white()] } } });
|
||||||
|
let TaggedValue::GradientRamp(ramp) = load(json) else {
|
||||||
|
panic!("the ramp payload should become a gradient ramp value")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
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
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_flat_stops_parse_faithfully() {
|
fn legacy_flat_stops_parse_faithfully() {
|
||||||
@@ -1125,6 +1150,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");
|
||||||
|
|
||||||
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]);
|
||||||
@@ -1138,6 +1164,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");
|
||||||
|
|
||||||
let gradient = Gradient::from(ramp);
|
let gradient = Gradient::from(ramp);
|
||||||
assert_eq!(gradient.positions(), vec![0., 1.]);
|
assert_eq!(gradient.positions(), vec![0., 1.]);
|
||||||
@@ -1148,7 +1175,11 @@ mod gradient_shape_migration {
|
|||||||
#[test]
|
#[test]
|
||||||
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": [] } });
|
||||||
assert_eq!(load(json), TaggedValue::GradientRamp(GradientRamp::default()));
|
let expected = GradientRamp {
|
||||||
|
gradient_interpolation: GradientInterpolation::SrgbGamma,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(load(json), TaggedValue::GradientRamp(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub mod migrations {
|
|||||||
use crate::Vector;
|
use crate::Vector;
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use vector_types::gradient::GradientStops;
|
use vector_types::gradient::GradientStops;
|
||||||
use vector_types::{Gradient, GradientRamp};
|
use vector_types::{Gradient, GradientInterpolation, GradientRamp};
|
||||||
|
|
||||||
// Storing legacy structs that are only used in document migration.
|
// Storing legacy structs that are only used in document migration.
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
@@ -152,6 +152,7 @@ pub mod migrations {
|
|||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Recovers a [`GradientRamp`] from any of its on-disk shapes: the current nested form, the flat stops struct
|
/// Recovers a [`GradientRamp`] from any of its on-disk shapes: the current nested form, the flat stops struct
|
||||||
/// that preceded it, or the ancient position-color tuple list (whose even positions elide back to absence).
|
/// that preceded it, or the ancient position-color tuple list (whose even positions elide back to absence).
|
||||||
|
/// The pre-ramp shapes come from documents that rendered in gamma, so they carry that interpolation explicitly.
|
||||||
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;
|
||||||
|
|
||||||
@@ -165,13 +166,20 @@ 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::from(stops),
|
GradientRampFormat::FlatStops(stops) => GradientRamp {
|
||||||
|
gradient_interpolation: GradientInterpolation::SrgbGamma,
|
||||||
|
..GradientRamp::from(stops)
|
||||||
|
},
|
||||||
GradientRampFormat::Tuples(stops) => {
|
GradientRampFormat::Tuples(stops) => {
|
||||||
let position: Vec<f64> = stops.iter().map(|(position, _)| *position).collect();
|
let position: Vec<f64> = stops.iter().map(|(position, _)| *position).collect();
|
||||||
let mut gradient = Gradient::from(stops.into_iter().map(|(_, color)| color).collect::<Vec<_>>());
|
let mut gradient = Gradient::from(stops.into_iter().map(|(_, color)| color).collect::<Vec<_>>());
|
||||||
gradient.set_positions(&position);
|
gradient.set_positions(&position);
|
||||||
gradient.elide_default_attributes();
|
gradient.elide_default_attributes();
|
||||||
GradientRamp::from(gradient)
|
|
||||||
|
GradientRamp {
|
||||||
|
gradient_interpolation: GradientInterpolation::SrgbGamma,
|
||||||
|
..GradientRamp::from(gradient)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -919,6 +919,14 @@ impl Color {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`Self::lerp`] but interpolating in gamma sRGB space, the space SVG interpolates in between adjacent gradient stops.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn lerp_gamma_srgb(&self, other: &Color, t: f32) -> Self {
|
||||||
|
let a = self.to_gamma_srgb_channels();
|
||||||
|
let b = other.to_gamma_srgb_channels();
|
||||||
|
Color::from_gamma_srgb_channels(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generic power curve `c.powf(1 / exponent)` applied per RGB channel. Distinct from the sRGB transfer curve (see [`Self::to_gamma_srgb_channels`]).
|
/// Generic power curve `c.powf(1 / exponent)` applied per RGB channel. Distinct from the sRGB transfer curve (see [`Self::to_gamma_srgb_channels`]).
|
||||||
/// The expected output must still be treated as linear-light.
|
/// The expected output must still be treated as linear-light.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use core_types::uuid::generate_uuid;
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::Graphic;
|
use graphic_types::Graphic;
|
||||||
use graphic_types::vector_types::gradient::GradientForm;
|
use graphic_types::vector_types::gradient::GradientForm;
|
||||||
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientSpread as GradientSpreadAttr};
|
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpread as GradientSpreadAttr};
|
||||||
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
use vector_types::gradient::GradientSpread;
|
use vector_types::gradient::{GradientInterpolation, GradientSpread};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
pub enum PaintTarget {
|
pub enum PaintTarget {
|
||||||
@@ -111,8 +111,9 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
|
|||||||
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(0);
|
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(0);
|
||||||
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
|
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
|
||||||
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0);
|
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0);
|
||||||
|
let gradient_interpolation: GradientInterpolation = source.attr::<GradientInterpolationAttr>(0);
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, ClearGuardPlacement::SvgStopOrder);
|
let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
|
||||||
for (position, color, original_midpoint) in samples {
|
for (position, color, original_midpoint) in samples {
|
||||||
stop.push_str("<stop");
|
stop.push_str("<stop");
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ use graphene_resource::Resource;
|
|||||||
use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
|
use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
|
||||||
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
|
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
|
||||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
||||||
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
|
use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientInterpolation};
|
||||||
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientSpread as GradientSpreadAttr};
|
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpread as GradientSpreadAttr};
|
||||||
use graphic_types::vector_types::subpath::Subpath;
|
use graphic_types::vector_types::subpath::Subpath;
|
||||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
||||||
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
@@ -422,8 +422,14 @@ pub(crate) enum ClearGuardPlacement {
|
|||||||
/// The `Clear` spread brackets the samples with transparent guard stops placed per `guards`: the pad extension then
|
/// The `Clear` spread brackets the samples with transparent guard stops placed per `guards`: the pad extension then
|
||||||
/// paints transparency outward while hard stops cut the paint off exactly at the unit range's boundaries. A radial
|
/// paints transparency outward while hard stops cut the paint off exactly at the unit range's boundaries. A radial
|
||||||
/// gradient's span still starts at zero, since its sampling distance never goes below the center.
|
/// gradient's span still starts at zero, since its sampling distance never goes below the center.
|
||||||
pub(crate) fn spread_adjusted_samples(gradient: &Gradient, gradient_spread: GradientSpread, gradient_form: GradientForm, guards: ClearGuardPlacement) -> (GradientSamples, (f64, f64)) {
|
pub(crate) fn spread_adjusted_samples(
|
||||||
let samples = gradient.interpolated_samples();
|
gradient: &Gradient,
|
||||||
|
gradient_spread: GradientSpread,
|
||||||
|
gradient_form: GradientForm,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
|
guards: ClearGuardPlacement,
|
||||||
|
) -> (GradientSamples, (f64, f64)) {
|
||||||
|
let samples = gradient.interpolated_samples(gradient_interpolation);
|
||||||
if gradient_spread != GradientSpread::Clear {
|
if gradient_spread != GradientSpread::Clear {
|
||||||
return (samples, (0., 1.));
|
return (samples, (0., 1.));
|
||||||
}
|
}
|
||||||
@@ -508,8 +514,10 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
|
|||||||
let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0);
|
let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0);
|
||||||
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
|
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
|
||||||
let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0);
|
let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0);
|
||||||
|
let gradient_interpolation: GradientInterpolation = gradient_list.attr::<GradientInterpolationAttr>(0);
|
||||||
|
|
||||||
|
let (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, ClearGuardPlacement::VelloRampTexels);
|
|
||||||
let peniko_stops = peniko_color_stops(&samples);
|
let peniko_stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
|
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
|
||||||
@@ -2403,6 +2411,7 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
|
|||||||
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
|
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
|
||||||
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
|
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
|
||||||
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
|
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
|
||||||
|
let gradient_interpolation: GradientInterpolation = source.attr::<GradientInterpolationAttr>(index);
|
||||||
let 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 {
|
||||||
@@ -2418,7 +2427,7 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
|
|||||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, ClearGuardPlacement::SvgStopOrder);
|
let (samples, _) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_interpolation, 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 {
|
||||||
@@ -2489,6 +2498,7 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
|
|||||||
let Some(gradient) = source.element(index) else { continue };
|
let Some(gradient) = source.element(index) else { continue };
|
||||||
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
|
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(index);
|
||||||
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
|
let gradient_form: GradientForm = source.attr::<GradientFormAttr>(index);
|
||||||
|
let gradient_interpolation: GradientInterpolation = source.attr::<GradientInterpolationAttr>(index);
|
||||||
let transform: DAffine2 = source.attr::<Transform>(index);
|
let transform: DAffine2 = source.attr::<Transform>(index);
|
||||||
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
|
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
|
||||||
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
||||||
@@ -2498,7 +2508,7 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
|
|||||||
let blend_mode = blend_mode_attr.to_peniko();
|
let blend_mode = blend_mode_attr.to_peniko();
|
||||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||||
|
|
||||||
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, ClearGuardPlacement::VelloRampTexels);
|
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, gradient_interpolation, ClearGuardPlacement::VelloRampTexels);
|
||||||
let stops = peniko_color_stops(&samples);
|
let stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
let extend = peniko_extend(gradient_spread);
|
let extend = peniko_extend(gradient_spread);
|
||||||
@@ -3242,12 +3252,24 @@ mod spread_tests {
|
|||||||
fn spread_adjusted_samples_wraps_clear_in_transparent_guards() {
|
fn spread_adjusted_samples_wraps_clear_in_transparent_guards() {
|
||||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Repeat, GradientForm::Linear, ClearGuardPlacement::SvgStopOrder);
|
let (samples, span) = spread_adjusted_samples(
|
||||||
|
&gradient,
|
||||||
|
GradientSpread::Repeat,
|
||||||
|
GradientForm::Linear,
|
||||||
|
GradientInterpolation::SrgbGamma,
|
||||||
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
|
);
|
||||||
assert_eq!(span, (0., 1.));
|
assert_eq!(span, (0., 1.));
|
||||||
assert_eq!(samples, gradient.interpolated_samples());
|
assert_eq!(samples, gradient.interpolated_samples(GradientInterpolation::SrgbGamma));
|
||||||
|
|
||||||
// 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(&gradient, GradientSpread::Clear, GradientForm::Linear, ClearGuardPlacement::SvgStopOrder);
|
let (samples, span) = spread_adjusted_samples(
|
||||||
|
&gradient,
|
||||||
|
GradientSpread::Clear,
|
||||||
|
GradientForm::Linear,
|
||||||
|
GradientInterpolation::SrgbGamma,
|
||||||
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
|
);
|
||||||
assert_eq!(span, (0., 1.));
|
assert_eq!(span, (0., 1.));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
samples,
|
samples,
|
||||||
@@ -3256,7 +3278,13 @@ mod spread_tests {
|
|||||||
|
|
||||||
// Vello guards own the outermost ramp texels, with the visible range compressed inward to make room
|
// Vello guards own the outermost ramp texels, with the visible range compressed inward to make room
|
||||||
let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.);
|
let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.);
|
||||||
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Clear, GradientForm::Linear, ClearGuardPlacement::VelloRampTexels);
|
let (samples, span) = spread_adjusted_samples(
|
||||||
|
&gradient,
|
||||||
|
GradientSpread::Clear,
|
||||||
|
GradientForm::Linear,
|
||||||
|
GradientInterpolation::SrgbGamma,
|
||||||
|
ClearGuardPlacement::VelloRampTexels,
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
samples,
|
samples,
|
||||||
vec![
|
vec![
|
||||||
@@ -3269,7 +3297,13 @@ mod spread_tests {
|
|||||||
assert!(span.0 < 0. && span.1 > 1., "the geometry must stretch to compensate for the compressed stops: {span:?}");
|
assert!(span.0 < 0. && span.1 > 1., "the geometry must stretch to compensate for the compressed stops: {span:?}");
|
||||||
|
|
||||||
// A radial keeps its stops and span anchored at zero, with no guard below the center
|
// A radial keeps its stops and span anchored at zero, with no guard below the center
|
||||||
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Clear, GradientForm::Radial, ClearGuardPlacement::VelloRampTexels);
|
let (samples, span) = spread_adjusted_samples(
|
||||||
|
&gradient,
|
||||||
|
GradientSpread::Clear,
|
||||||
|
GradientForm::Radial,
|
||||||
|
GradientInterpolation::SrgbGamma,
|
||||||
|
ClearGuardPlacement::VelloRampTexels,
|
||||||
|
);
|
||||||
assert_eq!(span.0, 0.);
|
assert_eq!(span.0, 0.);
|
||||||
assert_eq!(samples.first().unwrap(), &(0., Color::BLACK, None));
|
assert_eq!(samples.first().unwrap(), &(0., Color::BLACK, None));
|
||||||
assert_eq!(samples.last().unwrap(), &(1., Color::TRANSPARENT, None));
|
assert_eq!(samples.last().unwrap(), &(1., Color::TRANSPARENT, None));
|
||||||
@@ -3277,7 +3311,13 @@ mod spread_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spread_adjusted_samples_keeps_a_stopless_clear_gradient_black_inside_the_range() {
|
fn spread_adjusted_samples_keeps_a_stopless_clear_gradient_black_inside_the_range() {
|
||||||
let (samples, _) = spread_adjusted_samples(&Gradient::from(Vec::new()), GradientSpread::Clear, GradientForm::Linear, ClearGuardPlacement::SvgStopOrder);
|
let (samples, _) = spread_adjusted_samples(
|
||||||
|
&Gradient::from(Vec::new()),
|
||||||
|
GradientSpread::Clear,
|
||||||
|
GradientForm::Linear,
|
||||||
|
GradientInterpolation::SrgbGamma,
|
||||||
|
ClearGuardPlacement::SvgStopOrder,
|
||||||
|
);
|
||||||
let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect();
|
let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect();
|
||||||
assert_eq!(colors, vec![Color::TRANSPARENT, Color::BLACK, Color::BLACK, Color::TRANSPARENT]);
|
assert_eq!(colors, vec![Color::TRANSPARENT, Color::BLACK, Color::BLACK, Color::TRANSPARENT]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::markers::ATTR_GRADIENT_SPREAD;
|
use crate::markers::{ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPREAD};
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION, Item, List};
|
use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION, Item, List};
|
||||||
@@ -94,12 +94,14 @@ 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) -> String {
|
pub fn to_css_linear_gradient(&self, gradient_interpolation: GradientInterpolation) -> String {
|
||||||
Gradient::from(self).to_css_linear_gradient()
|
Gradient::from(self).to_css_linear_gradient(gradient_interpolation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The serialized exchange form of a gradient: its stops, with whole-ramp settings as sibling fields serialized only when non-default.
|
/// 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
|
||||||
|
/// 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)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
@@ -108,6 +110,9 @@ 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
|
||||||
|
#[cfg_attr(feature = "serde", serde(default = "GradientInterpolation::legacy_gamma"))]
|
||||||
|
pub gradient_interpolation: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> {
|
unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> {
|
||||||
@@ -119,6 +124,7 @@ impl<C> From<GradientStops<C>> for GradientRamp<C> {
|
|||||||
Self {
|
Self {
|
||||||
stops,
|
stops,
|
||||||
gradient_spread: Default::default(),
|
gradient_spread: Default::default(),
|
||||||
|
gradient_interpolation: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,6 +134,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,6 +165,9 @@ 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() {
|
||||||
|
item.set_attribute(ATTR_GRADIENT_INTERPOLATION, ramp.gradient_interpolation);
|
||||||
|
}
|
||||||
item
|
item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +177,7 @@ 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,6 +204,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +214,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,6 +223,7 @@ 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,
|
||||||
..Self::from(&ramp.stops)
|
..Self::from(&ramp.stops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,6 +274,20 @@ 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.
|
||||||
|
pub fn interpolate_stop_colors(color_a: Color, color_b: Color, t: f32, gradient_interpolation: GradientInterpolation) -> Color {
|
||||||
|
match gradient_interpolation {
|
||||||
|
GradientInterpolation::SrgbLinear => color_a.lerp(&color_b, t),
|
||||||
|
GradientInterpolation::SrgbGamma => color_a.lerp_gamma_srgb(&color_b, t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The largest difference between two colors across their gamma sRGB channels, the 8-bit-adjacent measure that rendered output quantizes to.
|
||||||
|
fn max_gamma_channel_deviation(a: Color, b: Color) -> f64 {
|
||||||
|
let (a, b) = (a.to_gamma_srgb_channels(), b.to_gamma_srgb_channels());
|
||||||
|
(0..4).fold(0_f64, |max, i| max.max((a[i] - b[i]).abs() as f64))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct GradientStop {
|
pub struct GradientStop {
|
||||||
pub position: f64,
|
pub position: f64,
|
||||||
@@ -634,6 +662,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);
|
||||||
|
// Sampling deliberately stays in linear light; the ramp's interpolation space attribute only shapes what the renderers draw
|
||||||
return a.color.lerp(&b.color, adjusted_t as f32);
|
return a.color.lerp(&b.color, adjusted_t as f32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,14 +704,14 @@ impl Gradient {
|
|||||||
mapped
|
mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves 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 interpolation color space so the rendered gradient matches Graphite's interpolation rather than browser defaults.
|
||||||
pub fn to_css_linear_gradient(&self) -> String {
|
pub fn to_css_linear_gradient(&self, gradient_interpolation: GradientInterpolation) -> 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()
|
.interpolated_samples(gradient_interpolation)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(position, color, _)| {
|
.map(|(position, color, _)| {
|
||||||
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||||
@@ -693,21 +722,33 @@ impl Gradient {
|
|||||||
format!("linear-gradient(to right, {pieces})")
|
format!("linear-gradient(to right, {pieces})")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
/// 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 midpoint-curve approximation samples.
|
/// midpoint for actual gradient stops, and `None` for synthesized curve approximation samples.
|
||||||
///
|
///
|
||||||
/// Interpolation is performed in sRGB gamma space (then lifted back to linear-light for output) because the downstream SVG/CSS
|
/// The downstream SVG/CSS and Vello renderers interpolate between adjacent emitted stops in gamma sRGB space, so the
|
||||||
/// renderer interpolates between adjacent `<stop>` colors in gamma space; doing the subdivision math in the same space ensures
|
/// subdivision emits enough samples that the gamma-drawn segments match the ramp's true curve: the midpoint bias, and
|
||||||
/// the chosen samples actually match the curve the browser will draw.
|
/// the interpolation color space when it is not gamma itself.
|
||||||
pub fn interpolated_samples(&self) -> Vec<(f64, Color, Option<f64>)> {
|
pub fn interpolated_samples(&self, gradient_interpolation: GradientInterpolation) -> 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.;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn subdivide(left: f64, right: f64, midpoint: f64, pos_a: f64, pos_b: f64, color_a_gamma: [f32; 4], color_b_gamma: [f32; 4], result: &mut Vec<(f64, Color, Option<f64>)>, depth: u32) {
|
fn subdivide(
|
||||||
|
left: f64,
|
||||||
|
right: f64,
|
||||||
|
midpoint: f64,
|
||||||
|
pos_a: f64,
|
||||||
|
pos_b: f64,
|
||||||
|
color_a: Color,
|
||||||
|
color_b: Color,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
|
result: &mut Vec<(f64, Color, Option<f64>)>,
|
||||||
|
depth: u32,
|
||||||
|
) {
|
||||||
const MAX_DEPTH: u32 = 20;
|
const MAX_DEPTH: u32 = 20;
|
||||||
if depth >= MAX_DEPTH {
|
if depth >= MAX_DEPTH {
|
||||||
return;
|
return;
|
||||||
@@ -720,19 +761,24 @@ impl Gradient {
|
|||||||
let y_right = apply_midpoint(right, midpoint);
|
let y_right = apply_midpoint(right, midpoint);
|
||||||
let y_linear = (y_left + y_right) / 2.;
|
let y_linear = (y_left + y_right) / 2.;
|
||||||
|
|
||||||
if (y_actual - y_linear).abs() > THRESHOLD {
|
// A sample is needed wherever the renderer's gamma segment between the flanking samples would stray
|
||||||
subdivide(left, mid, midpoint, pos_a, pos_b, color_a_gamma, color_b_gamma, result, depth + 1);
|
// from the ramp's true curve: from the midpoint bias, or from a non-gamma space's own curvature
|
||||||
|
let midpoint_deviates = (y_actual - y_linear).abs() > THRESHOLD;
|
||||||
|
let space_deviates = gradient_interpolation != GradientInterpolation::SrgbGamma && {
|
||||||
|
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_interpolation);
|
||||||
|
let color_right = interpolate_stop_colors(color_a, color_b, y_right as f32, gradient_interpolation);
|
||||||
|
max_gamma_channel_deviation(color_target, color_left.lerp_gamma_srgb(&color_right, 0.5)) > THRESHOLD
|
||||||
|
};
|
||||||
|
|
||||||
|
if midpoint_deviates || space_deviates {
|
||||||
|
subdivide(left, mid, midpoint, pos_a, pos_b, color_a, color_b, gradient_interpolation, result, depth + 1);
|
||||||
|
|
||||||
let global_pos = pos_a + mid * (pos_b - pos_a);
|
let global_pos = pos_a + mid * (pos_b - pos_a);
|
||||||
let t = y_actual as f32;
|
let color = interpolate_stop_colors(color_a, color_b, y_actual as f32, gradient_interpolation);
|
||||||
let r = color_a_gamma[0] + (color_b_gamma[0] - color_a_gamma[0]) * t;
|
|
||||||
let g = color_a_gamma[1] + (color_b_gamma[1] - color_a_gamma[1]) * t;
|
|
||||||
let b = color_a_gamma[2] + (color_b_gamma[2] - color_a_gamma[2]) * t;
|
|
||||||
let a = color_a_gamma[3] + (color_b_gamma[3] - color_a_gamma[3]) * t;
|
|
||||||
let color = Color::from_gamma_srgb_channels(r, g, b, a);
|
|
||||||
result.push((global_pos, color, None));
|
result.push((global_pos, color, None));
|
||||||
|
|
||||||
subdivide(mid, right, midpoint, pos_a, pos_b, color_a_gamma, color_b_gamma, result, depth + 1);
|
subdivide(mid, right, midpoint, pos_a, pos_b, color_a, color_b, gradient_interpolation, result, depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,9 +807,9 @@ impl Gradient {
|
|||||||
result.push((pos_a, color_a, Some(midpoint)));
|
result.push((pos_a, color_a, Some(midpoint)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only subdivide if midpoint deviates from linear (0.5)
|
// 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 {
|
if (midpoint - 0.5).abs() >= 1e-6 || gradient_interpolation != GradientInterpolation::SrgbGamma {
|
||||||
subdivide(0., 1., midpoint, pos_a, pos_b, color_a.to_gamma_srgb_channels(), color_b.to_gamma_srgb_channels(), &mut result, 0);
|
subdivide(0., 1., midpoint, pos_a, pos_b, color_a, color_b, gradient_interpolation, &mut result, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the end stop
|
// Add the end stop
|
||||||
@@ -825,6 +871,32 @@ impl GradientSpread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 GradientInterpolation {
|
||||||
|
/// Blends stops in linear light, keeping transitions evenly bright.
|
||||||
|
#[default]
|
||||||
|
#[label("sRGB Linear")]
|
||||||
|
SrgbLinear,
|
||||||
|
/// Blends stops in gamma-encoded sRGB, the classic SVG and CSS look.
|
||||||
|
#[label("sRGB Gamma")]
|
||||||
|
SrgbGamma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GradientInterpolation {
|
||||||
|
pub fn is_default(&self) -> bool {
|
||||||
|
*self == Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove when switching to the new document format and Ctrl-C node serialization format
|
||||||
|
fn legacy_gamma() -> Self {
|
||||||
|
Self::SrgbGamma
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
||||||
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
||||||
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
||||||
@@ -958,6 +1030,129 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gradient_interpolation_always_serializes_and_its_absence_reads_as_legacy_gamma() {
|
||||||
|
let default_interpolation = GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]));
|
||||||
|
let json = serde_json::to_string(&default_interpolation).unwrap();
|
||||||
|
assert!(
|
||||||
|
json.contains(r#""gradient_interpolation":"SrgbLinear""#),
|
||||||
|
"the interpolation must serialize even at its default, marking the ramp as post-legacy: {json}"
|
||||||
|
);
|
||||||
|
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), default_interpolation);
|
||||||
|
|
||||||
|
let gamma = GradientRamp {
|
||||||
|
gradient_interpolation: GradientInterpolation::SrgbGamma,
|
||||||
|
..default_interpolation.clone()
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&gamma).unwrap();
|
||||||
|
assert!(json.contains(r#""gradient_interpolation":"SrgbGamma""#), "a non-default interpolation must serialize: {json}");
|
||||||
|
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), gamma);
|
||||||
|
|
||||||
|
let legacy_json = json.replace(r#","gradient_interpolation":"SrgbGamma""#, "");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<GradientRamp>(&legacy_json).unwrap(),
|
||||||
|
gamma,
|
||||||
|
"a ramp saved before the field existed should read as the gamma it rendered with: {legacy_json}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gradient_interpolation_round_trips_through_the_item_attribute() {
|
||||||
|
let ramp = GradientRamp {
|
||||||
|
gradient_interpolation: GradientInterpolation::SrgbGamma,
|
||||||
|
..GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]))
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = Item::<Gradient>::from(ramp.clone());
|
||||||
|
assert_eq!(
|
||||||
|
item.attribute_cloned_or_default::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION),
|
||||||
|
GradientInterpolation::SrgbGamma,
|
||||||
|
"the runtime item should carry the interpolation as its attribute"
|
||||||
|
);
|
||||||
|
assert_eq!(GradientRamp::from(&item), ramp);
|
||||||
|
|
||||||
|
let linear = Item::<Gradient>::from(GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE])));
|
||||||
|
assert!(
|
||||||
|
linear.attribute::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION).is_none(),
|
||||||
|
"the default Linear must stay absent rather than materialize"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linear_interpolation_densifies_samples_where_gamma_segments_deviate() {
|
||||||
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
|
// Gamma needs no synthesized samples since the renderers already draw gamma segments
|
||||||
|
assert_eq!(gradient.interpolated_samples(GradientInterpolation::SrgbGamma).len(), 2);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
let samples = gradient.interpolated_samples(GradientInterpolation::SrgbLinear);
|
||||||
|
assert!(samples.len() > 2, "linear interpolation should synthesize samples, got {}", samples.len());
|
||||||
|
assert_eq!(samples.first().unwrap().0, 0.);
|
||||||
|
assert_eq!(samples.last().unwrap().0, 1.);
|
||||||
|
for &(position, color, _) in &samples {
|
||||||
|
assert!(
|
||||||
|
(color.r() as f64 - position).abs() < 1e-5,
|
||||||
|
"sample at {position} should sit on the linear-light line, got {}",
|
||||||
|
color.r()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identical end colors leave nothing to densify
|
||||||
|
let flat = Gradient::from(vec![Color::WHITE, Color::WHITE]);
|
||||||
|
assert_eq!(flat.interpolated_samples(GradientInterpolation::SrgbLinear).len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn midpoint_bias_and_interpolation_space_compose_within_playback_tolerance() {
|
||||||
|
let color_pairs = [
|
||||||
|
(Color::BLACK, Color::WHITE),
|
||||||
|
(Color::RED, Color::WHITE),
|
||||||
|
(Color::from_rgbaf32_unchecked(0.9, 0.2, 0.05, 1.), Color::from_rgbaf32_unchecked(0.05, 0.3, 0.8, 0.5)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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
|
||||||
|
for gradient_interpolation in [GradientInterpolation::SrgbLinear, GradientInterpolation::SrgbGamma] {
|
||||||
|
for &(color_a, color_b) in &color_pairs {
|
||||||
|
for midpoint_step in 1..40 {
|
||||||
|
let midpoint = midpoint_step as f64 / 40.;
|
||||||
|
|
||||||
|
let mut gradient = Gradient::from(vec![color_a, color_b]);
|
||||||
|
gradient.set_midpoints(&[midpoint, 0.5]);
|
||||||
|
let samples = gradient.interpolated_samples(gradient_interpolation);
|
||||||
|
|
||||||
|
for probe in 0..=1000 {
|
||||||
|
let t = probe as f64 / 1000.;
|
||||||
|
|
||||||
|
let after = samples.iter().position(|&(position, ..)| position >= t).unwrap_or(samples.len() - 1);
|
||||||
|
let playback = if after == 0 {
|
||||||
|
samples[0].1
|
||||||
|
} else {
|
||||||
|
let (left_position, left_color, _) = samples[after - 1];
|
||||||
|
let (right_position, right_color, _) = samples[after];
|
||||||
|
let span = right_position - left_position;
|
||||||
|
if span < 1e-12 {
|
||||||
|
right_color
|
||||||
|
} else {
|
||||||
|
left_color.lerp_gamma_srgb(&right_color, ((t - left_position) / span) as f32)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let true_color = interpolate_stop_colors(color_a, color_b, apply_midpoint(t, midpoint) as f32, gradient_interpolation);
|
||||||
|
let deviation = max_gamma_channel_deviation(playback, true_color);
|
||||||
|
assert!(
|
||||||
|
deviation <= 4. / 255.,
|
||||||
|
"playback deviates {:.1}/255 at t={t} with midpoint {midpoint} in {gradient_interpolation:?} between {color_a:?} and {color_b:?}",
|
||||||
|
deviation * 255.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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]);
|
||||||
@@ -1009,7 +1204,7 @@ 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().iter().map(|(position, ..)| *position).collect();
|
let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).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.));
|
||||||
@@ -1023,7 +1218,7 @@ 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().iter().map(|(position, ..)| *position).collect();
|
let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).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()), Color::BLACK);
|
assert_eq!(gradient.evaluate(0., Default::default()), Color::BLACK);
|
||||||
assert_eq!(gradient.evaluate(1., Default::default()), Color::WHITE);
|
assert_eq!(gradient.evaluate(1., Default::default()), Color::WHITE);
|
||||||
@@ -1034,7 +1229,7 @@ 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().iter().map(|(position, ..)| *position).collect();
|
let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).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()), Color::WHITE.lerp(&Color::RED, 0.5));
|
assert_eq!(gradient.evaluate(0.5, Default::default()), Color::WHITE.lerp(&Color::RED, 0.5));
|
||||||
|
|
||||||
@@ -1044,7 +1239,7 @@ 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().is_empty());
|
assert!(gradient.interpolated_samples(GradientInterpolation::SrgbGamma).is_empty());
|
||||||
assert_eq!(gradient.evaluate(0.5, Default::default()), Color::BLACK);
|
assert_eq!(gradient.evaluate(0.5, Default::default()), Color::BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1053,7 +1248,7 @@ 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();
|
let samples = gradient.interpolated_samples(GradientInterpolation::SrgbGamma);
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,7 +1260,7 @@ mod tests {
|
|||||||
gradient.set_midpoints(&[f64::NAN, f64::NAN]);
|
gradient.set_midpoints(&[f64::NAN, f64::NAN]);
|
||||||
assert_eq!(gradient.evaluate(0.25, Default::default()), linear_result);
|
assert_eq!(gradient.evaluate(0.25, Default::default()), linear_result);
|
||||||
let no_nan_annotations = gradient
|
let no_nan_annotations = gradient
|
||||||
.interpolated_samples()
|
.interpolated_samples(GradientInterpolation::SrgbGamma)
|
||||||
.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");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub mod vector;
|
|||||||
|
|
||||||
// Re-export commonly used types at the crate root
|
// Re-export commonly used types at the crate root
|
||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
pub use gradient::{Gradient, GradientForm, GradientRamp, GradientSpread, GradientStop};
|
pub use gradient::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop};
|
||||||
pub use markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_FORM, ATTR_GRADIENT_SPREAD};
|
pub use markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_FORM, ATTR_GRADIENT_SPREAD};
|
||||||
pub use math::{QuadExt, RectExt};
|
pub use math::{QuadExt, RectExt};
|
||||||
pub use subpath::Subpath;
|
pub use subpath::Subpath;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use core_types::attribute::Attribute;
|
|||||||
core_types::attribute! {
|
core_types::attribute! {
|
||||||
/// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, `Repeat`, or `Clear`).
|
/// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, `Repeat`, or `Clear`).
|
||||||
pub GradientSpread("gradient_spread"): crate::gradient::GradientSpread;
|
pub GradientSpread("gradient_spread"): crate::gradient::GradientSpread;
|
||||||
|
/// Gradient's `GradientInterpolation` (`SrgbLinear` or `SrgbGamma`), the color space its stops blend in.
|
||||||
|
pub GradientInterpolation("gradient_interpolation"): crate::gradient::GradientInterpolation;
|
||||||
/// Gradient's shape (`Linear` or `Radial`).
|
/// Gradient's shape (`Linear` or `Radial`).
|
||||||
pub GradientForm("gradient_form"): crate::gradient::GradientForm;
|
pub GradientForm("gradient_form"): crate::gradient::GradientForm;
|
||||||
/// Optional `Vector` that overrides the item's own geometry for click-target generation.
|
/// Optional `Vector` that overrides the item's own geometry for click-target generation.
|
||||||
@@ -20,10 +22,12 @@ core_types::attribute! {
|
|||||||
core_types::named_value! {
|
core_types::named_value! {
|
||||||
for crate::gradient::GradientSpread;
|
for crate::gradient::GradientSpread;
|
||||||
for crate::gradient::GradientForm;
|
for crate::gradient::GradientForm;
|
||||||
|
for crate::gradient::GradientInterpolation;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ATTR_GRADIENT_SPREAD: &str = GradientSpread::NAME;
|
pub const ATTR_GRADIENT_SPREAD: &str = GradientSpread::NAME;
|
||||||
pub const ATTR_GRADIENT_FORM: &str = GradientForm::NAME;
|
pub const ATTR_GRADIENT_FORM: &str = GradientForm::NAME;
|
||||||
|
pub const ATTR_GRADIENT_INTERPOLATION: &str = GradientInterpolation::NAME;
|
||||||
pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME;
|
pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl FillChoice<SRGBA8> {
|
|||||||
let hex = srgba.to_rgba_hex();
|
let hex = srgba.to_rgba_hex();
|
||||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||||
}
|
}
|
||||||
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient()),
|
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.gradient_interpolation)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use math_parser::value::{Number, Value};
|
|||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
use std::ops::{Add, Mul, Rem, Sub};
|
use std::ops::{Add, Mul, Rem, Sub};
|
||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
use vector_types::markers::{GradientForm as GradientFormAttr, GradientSpread as GradientSpreadAttr};
|
use vector_types::markers::{GradientForm as GradientFormAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpread as GradientSpreadAttr};
|
||||||
|
|
||||||
/// The struct that stores the context for the maths parser.
|
/// The struct that stores the context for the maths parser.
|
||||||
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
|
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
|
||||||
@@ -1214,12 +1214,18 @@ fn gradient_spread(_: impl Ctx, gradient: Gradient, gradient_spread: vector_type
|
|||||||
(gradient, Attr(gradient_spread))
|
(gradient, Attr(gradient_spread))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the color space each gradient in the input list blends between its stops with: linear light or gamma-encoded sRGB.
|
||||||
|
#[node_macro::node(category("Gradient"))]
|
||||||
|
fn gradient_interpolation(_: impl Ctx, gradient: Gradient, gradient_interpolation: vector_types::GradientInterpolation) -> (Gradient, Attr<GradientInterpolationAttr>) {
|
||||||
|
(gradient, Attr(gradient_interpolation))
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the position of each of a gradient's stops, a factor from 0 to 1 along the gradient.
|
/// Sets the position of each of a gradient's stops, a factor from 0 to 1 along the gradient.
|
||||||
///
|
///
|
||||||
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each stop to its default evenly spaced position.
|
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each stop to its default evenly spaced position.
|
||||||
#[node_macro::node(category("Gradient"))]
|
#[node_macro::node(category("Gradient"))]
|
||||||
fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: IList<f64>) -> Gradient {
|
fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: List<f64>) -> Gradient {
|
||||||
let positions: Vec<f64> = positions.iter().collect();
|
let positions: Vec<f64> = positions.iter_element_values().copied().collect();
|
||||||
gradient.set_positions(&positions);
|
gradient.set_positions(&positions);
|
||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
@@ -1230,8 +1236,8 @@ fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: IList<f64>
|
|||||||
///
|
///
|
||||||
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5.
|
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5.
|
||||||
#[node_macro::node(category("Gradient"))]
|
#[node_macro::node(category("Gradient"))]
|
||||||
fn gradient_midpoints(_: impl Ctx, mut gradient: Gradient, midpoints: IList<f64>) -> Gradient {
|
fn gradient_midpoints(_: impl Ctx, mut gradient: Gradient, midpoints: List<f64>) -> Gradient {
|
||||||
let midpoints: Vec<f64> = midpoints.iter().collect();
|
let midpoints: Vec<f64> = midpoints.iter_element_values().copied().collect();
|
||||||
gradient.set_midpoints(&midpoints);
|
gradient.set_midpoints(&midpoints);
|
||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user