Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes (#4397)

* Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes

* Fix Vello stopless-gradient fallback coverage, empty legacy gradient tables, the node docs gradient swatch, NaN position elision, and wired setter input overwrites
This commit is contained in:
Keavon Chambers
2026-08-03 04:05:02 -07:00
committed by GitHub
parent e52a442504
commit 2f24459344
34 changed files with 1121 additions and 475 deletions

View File

@@ -5,7 +5,7 @@ pub mod brush_stroke;
pub mod migrations {
use crate::brush_stroke::BrushStroke;
// TODO: Eventually remove this migration document upgrade code
// TODO: Eventually remove this document upgrade code
pub fn migrate_to_brush_strokes<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<BrushStroke>, D::Error> {
use serde::Deserialize;

View File

@@ -10,7 +10,7 @@ use rand::seq::SliceRandom;
use raster_types::{CPU, GPU, Raster};
use std::cmp::Ordering;
use vector_types::gradient::{GradientSpreadMethod, GradientType};
use vector_types::{Gradient, GradientStop, ReferencePoint};
use vector_types::{Gradient, ReferencePoint};
/// Returns the list with the item at the specified index removed.
/// If no value exists at that index, the list is returned unchanged.
@@ -1009,45 +1009,7 @@ pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
#[node_macro::node(category("Color"), name("Colors to Gradient"))]
fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> Item<Gradient> {
let colors = colors.into_flattened_list::<Color>();
let total_colors = colors.len();
if total_colors == 0 {
return Item::new_from_element(Gradient::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: Color::BLACK,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: Color::BLACK,
},
]));
}
if let (1, Some(&single_color)) = (total_colors, colors.element(0)) {
return Item::new_from_element(Gradient::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: single_color,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: single_color,
},
]));
}
let colors = colors.into_iter().enumerate().map(|(index, row)| GradientStop {
position: index as f64 / (total_colors - 1) as f64,
midpoint: 0.5,
color: row.into_element(),
});
Item::new_from_element(Gradient::new(colors))
Item::new_from_element(Gradient::from(colors.into_flattened_list::<Color>()))
}
#[cfg(test)]

View File

@@ -1373,7 +1373,7 @@ fn hex_to_color(_: impl Ctx, hex_code: Item<String>) -> Item<Color> {
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_value(_: impl Ctx, _primary: (), gradient: Item<Gradient>) -> Item<Gradient> {
fn gradient_value(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>) -> Item<Gradient> {
gradient
}
@@ -1393,11 +1393,35 @@ fn spread_method(_: impl Ctx, gradient: Item<Gradient>, spread_method: Item<vect
gradient
}
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
/// 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.
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
let position = position.element().clamp(0., 1.);
let color = gradient.element().evaluate(position);
fn gradient_positions(_: impl Ctx, gradient: Item<Gradient>, positions: List<f64>) -> Item<Gradient> {
let mut gradient = gradient;
let positions: Vec<f64> = positions.iter_element_values().copied().collect();
gradient.element_mut().set_positions(&positions);
gradient
}
/// Sets the interpolation midpoint for each interval between gradient stops, a factor from 0 to 1 where the 0.5 default means linear interpolation and another value skews the transition speed toward one stop or the other.
///
/// The final stop belongs to no interval so its midpoint is ignored.
///
/// 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("Color"))]
fn gradient_midpoints(_: impl Ctx, gradient: Item<Gradient>, midpoints: List<f64>) -> Item<Gradient> {
let mut gradient = gradient;
let midpoints: Vec<f64> = midpoints.iter_element_values().copied().collect();
gradient.element_mut().set_midpoints(&midpoints);
gradient
}
/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `spread_method` attribute: Pad (default), Reflect, or Repeat.
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
let spread_method = gradient.attribute_cloned_or_default::<vector_types::GradientSpreadMethod>(core_types::ATTR_SPREAD_METHOD);
let color = gradient.element().evaluate(*position.element(), spread_method);
Item::new_from_element(color)
}

View File

@@ -24,9 +24,7 @@ mod adjust_std {
}
impl Adjust<Color> for Gradient {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for color in self.color.iter_mut() {
*color = map_fn(color);
}
*self = self.map_colors(map_fn);
}
}
}

View File

@@ -42,16 +42,19 @@ mod blend_std {
}
impl Blend<Color> for Gradient {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut combined_stops = self.position.iter().chain(under.position.iter()).copied().collect::<Vec<_>>();
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let mut combined_stops = self.positions().into_iter().chain(under.positions()).collect::<Vec<_>>();
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let stops = combined_stops.into_iter().map(|position| {
let over_color = self.evaluate(position);
let under_color = under.evaluate(position);
let over_color = self.evaluate(position, Default::default());
let under_color = under.evaluate(position, Default::default());
let color = blend_fn(over_color, under_color);
GradientStop { position, midpoint: 0.5, color }
});
Gradient::new(stops)
let mut gradient = Gradient::new(stops);
gradient.elide_default_attributes();
gradient
}
}
}

View File

@@ -18,17 +18,18 @@ async fn gradient_map<T: Adjust<Color> + Send>(
Gradient,
)]
image: Item<T>,
gradient: Item<Gradient>,
#[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>,
reverse: Item<bool>,
) -> Item<T> {
let mut image = image;
let spread_method = gradient.attribute_cloned_or_default::<vector_types::GradientSpreadMethod>(core_types::ATTR_SPREAD_METHOD);
let gradient = gradient.into_element();
let reverse = reverse.into_element();
image.element_mut().adjust(|color| {
let intensity = color.luminance_rec_709();
let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64)
gradient.evaluate(intensity as f64, spread_method)
});
image

View File

@@ -64,7 +64,7 @@ impl Default for Font {
}
}
// TODO: Eventually remove this migration document upgrade code
// TODO: Eventually remove this document upgrade code
fn migrate_font_style<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
use serde::Deserialize;
String::deserialize(deserializer).map(|name| if name == "Normal (400)" { "Regular (400)".to_string() } else { name })

View File

@@ -118,6 +118,7 @@ async fn assign_colors<T>(
/// Whether to style the stroke.
stroke: Item<bool>,
/// The range of colors to select from.
#[default(Color::BLACK, Color::WHITE)]
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
gradient: Item<Gradient>,
/// Whether to reverse the gradient.
@@ -157,7 +158,7 @@ where
},
};
let color = gradient.evaluate(factor);
let color = gradient.evaluate(factor, Default::default());
let paint = List::new_from_element(color).into_graphic_list();
if fill {
@@ -189,7 +190,7 @@ async fn fill<V, F: IntoGraphicList + 'n + Send + 'static>(
)]
fill: F,
_backup_color: Item<Color>,
_backup_gradient: Item<Gradient>,
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: Item<Gradient>,
_gradient_type: Item<GradientType>,
_spread_method: Item<GradientSpreadMethod>,
_has_transform: Item<bool>,
@@ -2454,14 +2455,12 @@ async fn morph<I: IntoGraphicList>(
.zip(color_list_b.element(0))
.map(|(color_a, color_b)| Graphic::from(color_a.lerp(color_b, time as f32))),
(Some(Graphic::Color(color_list_a)), Some(Graphic::Gradient(gradient_list_b))) => color_list_a.element(0).zip(gradient_list_b.element(0)).map(|(color_a, stops_b)| {
let mut solid_to_gradient = stops_b.clone();
solid_to_gradient.color.iter_mut().for_each(|color| *color = *color_a);
let solid_to_gradient = stops_b.map_colors(|_| *color_a);
let stops = solid_to_gradient.lerp(stops_b, time);
gradient_with_stops(gradient_list_b.clone(), stops)
}),
(Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Color(color_list_b))) => gradient_list_a.element(0).zip(color_list_b.element(0)).map(|(stops_a, color_b)| {
let mut gradient_to_solid = stops_a.clone();
gradient_to_solid.color.iter_mut().for_each(|color| *color = *color_b);
let gradient_to_solid = stops_a.map_colors(|_| *color_b);
let stops = stops_a.lerp(&gradient_to_solid, time);
gradient_with_stops(gradient_list_a.clone(), stops)
}),