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 Dennis Kobert
parent 8504564f5d
commit f6b4ce71e6
36 changed files with 1112 additions and 464 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

@@ -15,7 +15,7 @@ use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
use raster_types::{CPU, GPU, Raster};
use vector_types::gradient::{GradientSpreadMethod, GradientType as GradientTypeValue};
use vector_types::{Gradient, GradientStop, ReferencePoint};
use vector_types::{Gradient, ReferencePoint};
fn arena_exhausted() -> Interrupt {
GraphError {
@@ -555,21 +555,10 @@ pub fn flatten_gradient<'e>(
flatten_leaf_lane(content, ctx.index() as usize)
}
/// A gradient with `colors` as evenly spaced stops from 0 to 1; none makes a
/// black gradient and one repeats at both ends.
fn evenly_spaced_gradient(colors: &[Color]) -> Gradient {
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
match colors {
[] => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
[color] => Gradient::new(vec![stop(0., *color), stop(1., *color)]),
colors => Gradient::new(colors.iter().enumerate().map(|(index, color)| stop(index as f64 / (colors.len() - 1) as f64, *color))),
}
}
/// 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"))]
pub fn colors_to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
evenly_spaced_gradient(&colors.iter().collect::<Vec<_>>())
Gradient::from(colors.iter().collect::<Vec<_>>())
}
/// The gradient over a graphic level's color leaves, as [`colors_to_gradient`].
@@ -583,7 +572,7 @@ pub fn colors_to_gradient_graphic(_: impl Ctx, colors: IList<Graphic<'static>>)
RowStep::Continue
});
}
evenly_spaced_gradient(&leaves)
Gradient::from(leaves)
}
pub use _colors_to_gradient_graphic_mod::colors_to_gradient_graphic_entries;

View File

@@ -796,11 +796,12 @@ mod tests {
assert_eq!(three.iter().map(|stop| stop.position).collect::<Vec<_>>(), vec![0., 0.5, 1.]);
assert_eq!(three.iter().map(|stop| stop.color).collect::<Vec<_>>(), vec![Color::BLACK, Color::WHITE, Color::BLACK]);
// A lone color is a one-stop gradient and no colors a stopless one; neither is padded
let single = stops_of(vec![Color::WHITE]);
assert_eq!(single.iter().map(|stop| (stop.position, stop.color)).collect::<Vec<_>>(), vec![(0., Color::WHITE), (1., Color::WHITE)]);
assert_eq!(single.iter().map(|stop| (stop.position, stop.color)).collect::<Vec<_>>(), vec![(0., Color::WHITE)]);
let empty = stops_of(Vec::new());
assert_eq!(empty.iter().map(|stop| (stop.position, stop.color)).collect::<Vec<_>>(), vec![(0., Color::BLACK), (1., Color::BLACK)]);
assert!(empty.iter().next().is_none());
}
#[test]

View File

@@ -1198,7 +1198,7 @@ fn hex_to_color(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, hex_code: Str
/// 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: Gradient) -> Gradient {
fn gradient_value(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Gradient) -> Gradient {
gradient
}
@@ -1214,16 +1214,43 @@ fn spread_method(_: impl Ctx, gradient: Gradient, spread_method: vector_types::G
(gradient, Attr(spread_method))
}
/// 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(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, _primary: (), gradient: IList<Gradient>, position: Fraction) -> Result<IList<Color>, Interrupt> {
fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: IList<f64>) -> Gradient {
let positions: Vec<f64> = positions.iter().collect();
gradient.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, mut gradient: Gradient, midpoints: IList<f64>) -> Gradient {
let midpoints: Vec<f64> = midpoints.iter().collect();
gradient.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(
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
_primary: (),
#[default(Color::BLACK, Color::WHITE)] gradient: IList<Gradient>,
position: Fraction,
) -> Result<IList<Color>, Interrupt> {
// An unwired gradient serves an empty level: no color
if gradient.is_empty() || ctx.index() != 0 {
return Err(GraphError::past_end().into());
}
let position = position.clamp(0., 1.);
Ok(gradient.element_ref(0).evaluate(position))
let spread_method = gradient.lane(0).attr::<SpreadMethodAttr>();
Ok(gradient.element_ref(0).evaluate(position, spread_method))
}
/// Constructs a footprint value which may be set to any transformation of a unit square describing a render area, and a render resolution at least 1x1 integer pixels.

View File

@@ -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

@@ -38,16 +38,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

@@ -17,18 +17,19 @@ fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash +
Gradient,
)]
mut image: T,
gradient: IList<Gradient>,
#[default(Color::BLACK, Color::WHITE)] gradient: IList<Gradient>,
reverse: bool,
) -> T {
if gradient.is_empty() {
return image;
}
let spread_method = gradient.lane(0).attr::<vector_types::markers::SpreadMethod>();
let gradient = gradient.element_ref(0);
image.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

@@ -61,7 +61,7 @@ fn assign_color_at(gradient: &Gradient, position: usize, length: usize, randomiz
_ => position as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
};
gradient.evaluate(factor)
gradient.evaluate(factor, Default::default())
}
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
@@ -77,6 +77,7 @@ fn assign_colors<'e>(
/// Whether to style the stroke.
stroke: bool,
/// The range of colors to select from.
#[default(Color::BLACK, Color::WHITE)]
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
gradient: IList<Gradient>,
/// Whether to reverse the gradient.
@@ -323,7 +324,7 @@ fn fill<'e>(
#[default(Color::BLACK)]
fill: IList<Graphic<'static>>,
_backup_color: IList<Color>,
_backup_gradient: IList<Gradient>,
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>,
_gradient_type: GradientType,
_spread_method: GradientSpreadMethod,
_has_transform: bool,
@@ -344,7 +345,7 @@ fn fill_graphic_leveled<'e>(
(element, _content_fill): (Graphic<'static>, Attr<Fill>),
#[default(Color::BLACK)] fill: IList<Graphic<'static>>,
_backup_color: IList<Color>,
_backup_gradient: IList<Gradient>,
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>,
_gradient_type: GradientType,
_spread_method: GradientSpreadMethod,
_has_transform: bool,
@@ -2950,14 +2951,12 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
match (a.element(0), b.element(0)) {
(Some(Graphic::Color(color_a)), Some(Graphic::Color(color_b))) => Some(List::new_from_element(Graphic::from(color_a.lerp(color_b, time as f32)))),
(Some(Graphic::Color(color_a)), Some(Graphic::Gradient(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);
Some(gradient_paint(b, stops, None))
}
(Some(Graphic::Gradient(stops_a)), Some(Graphic::Color(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);
Some(gradient_paint(a, stops, None))
}