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 b78e4b107e
commit 86d4106592
36 changed files with 1112 additions and 464 deletions

View File

@@ -127,6 +127,11 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
stop.push_str(" />")
}
// A gradient with no stops paints as solid black, matching `Gradient::evaluate` (a stopless def would otherwise render as no paint per the SVG spec)
if stop.is_empty() {
stop.push_str(r##"<stop stop-color="#000000" />"##);
}
// Need to cancel out the element's transform as it is already applied to the path itself.
let element_transform_inverse = if transform_is_invertible(element_transform) {
element_transform.inverse()

View File

@@ -400,6 +400,31 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp
}
}
/// Converts a gradient's renderer samples to peniko color stops, duplicating an off-zero first stop at position 0 since Vello ignores the first stop's position and always treats it as 0.
fn peniko_color_stops(gradient: &Gradient) -> peniko::ColorStops {
let mut peniko_stops = peniko::ColorStops::new();
for (position, color, _) in gradient.interpolated_samples() {
let color = peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color());
if peniko_stops.is_empty() && position > 0. {
peniko_stops.push(peniko::ColorStop { offset: 0., color });
}
peniko_stops.push(peniko::ColorStop { offset: position as f32, color });
}
// A gradient with no stops paints as solid black, matching `Gradient::evaluate`
if peniko_stops.is_empty() {
peniko_stops.push(peniko::ColorStop {
offset: 0.,
color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(Color::BLACK).to_peniko_color()),
});
}
peniko_stops
}
fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?;
@@ -407,13 +432,7 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
let spread_method: GradientSpreadMethod = gradient_list.attr::<SpreadMethod>(0);
let mut peniko_stops = peniko::ColorStops::new();
for (position, color, _) in stops.interpolated_samples() {
peniko_stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()),
});
}
let peniko_stops = peniko_color_stops(stops);
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
let (start, end, gradient_to_device) = (DVec2::ZERO, DVec2::X, gradient_placement(multiplied_transform * gradient_transform, gradient_type));
@@ -2403,13 +2422,7 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
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 mut stops: peniko::ColorStops = peniko::ColorStops::new();
for (position, color, _) in gradient.interpolated_samples() {
stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()),
})
}
let stops = peniko_color_stops(gradient);
let extend = match spread_method {
GradientSpreadMethod::Pad => peniko::Extend::Pad,