Fix Vello rendering incompatibility with its gradients using premultiplied alpha instead of SVG's straight (#4419)

* Upgrade Vello to a git main revision that honors the brush's interpolation alpha space

* Interpolate Vello gradients with straight alpha, matching the SVG renderer

* Draw the gradient picker strip and fill swatches as SVG so transparency previews with straight alpha

* Keep gradient stop handles opaque so a transparent stop's RGB stays visible

* Paint a stopless gradient's picker strip and swatch black rather than transparent
This commit is contained in:
Keavon Chambers
2026-08-07 18:45:12 -07:00
committed by GitHub
parent 681b4033f5
commit 6b1e9912ef
9 changed files with 172 additions and 69 deletions

View File

@@ -539,9 +539,14 @@ fn populate_computed_display_fields(layout: &mut Layout) {
hue_direction: spectrum_input.track_hue_direction,
interpolation: spectrum_input.track_interpolation,
};
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(settings);
let track_gradient = graphene_std::vector::style::Gradient::from(&spectrum_input.track);
spectrum_input.track_samples = track_gradient
.interpolated_samples_or_black(settings)
.into_iter()
.map(|(position, color, _)| SpectrumSample::new(position, color))
.collect();
// The end caps sample the track's boundary colors, which a cyclic wrap makes the wrapped interval's boundary-crossing color rather than the outermost stops'
let track_evaluator = graphene_std::vector::style::Gradient::from(&spectrum_input.track).evaluator(settings);
let track_evaluator = track_gradient.evaluator(settings);
let cap = |t: f64| {
let color = track_evaluator.evaluate(t);
SRGBA8::from(color).to_css_hex()

View File

@@ -586,22 +586,22 @@ pub struct SpectrumInput {
/// The colored gradient drawn behind the markers (display-only, caller-owned).
#[widget_builder(constructor)]
pub track: GradientStops<SRGBA8>,
/// The color space the track's stops interpolate in, used to compute `track_css`. Not sent to the frontend.
/// The color space the track's stops interpolate in, used to bake `track_samples`. Not sent to the frontend.
#[serde(skip)]
pub track_space: GradientSpace,
/// Whether the track's stops wrap as a cycle, used to compute `track_css` and by the frontend to draw the wrapped interval's midpoint diamond.
/// Whether the track's stops wrap as a cycle, used to bake `track_samples` and by the frontend to draw the wrapped interval's midpoint diamond.
#[serde(rename = "trackCyclic")]
pub track_cyclic: bool,
/// The hue direction the track's stops interpolate with in a polar space, used to compute `track_css`. Not sent to the frontend.
/// The hue direction the track's stops interpolate with in a polar space, used to bake `track_samples`. Not sent to the frontend.
#[serde(skip)]
pub track_hue_direction: GradientHueDirection,
/// The path the track's stops interpolate along, used to compute `track_css` and by the frontend to suppress the midpoint diamonds when stepped.
/// The path the track's stops interpolate along, used to bake `track_samples` and by the frontend to suppress the midpoint diamonds when stepped.
#[serde(rename = "trackInterpolation")]
pub track_interpolation: GradientInterpolation,
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
#[serde(rename = "trackCSS")]
/// Straight-alpha samples the frontend draws as the stops of an SVG gradient filling the track strip. Auto-populated from `track` at layout-send time.
#[serde(rename = "trackSamples")]
#[widget_builder(skip)]
pub track_css: String,
pub track_samples: Vec<SpectrumSample>,
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated by evaluating `track` at position 0.
#[serde(rename = "trackStartCSS")]
#[widget_builder(skip)]
@@ -650,18 +650,40 @@ pub struct SpectrumMarker {
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true.
/// The last marker's value controls the wrapped interval when `track_cyclic` is set, and is otherwise ignored.
midpoint: f64,
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`],
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
#[serde(rename = "handleColorCSS")]
handle_color_css: String,
}
impl SpectrumMarker {
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
let handle_color_css = SRGBA8::from(handle_color).to_css_hex();
let handle_color_css = format!("#{}", SRGBA8::from(handle_color).to_rgb_hex());
Self { position, midpoint, handle_color_css }
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SpectrumSample {
/// Position (0..1) of the sample along the spectrum track, drawn as the SVG stop's `offset`.
position: f64,
/// `#rrggbb` hex of the sample's color, drawn as the SVG stop's `stop-color`.
color: String,
/// Straight alpha (0..1) of the sample, drawn as the SVG stop's `stop-opacity`.
alpha: f32,
}
impl SpectrumSample {
pub fn new(position: f64, color: Color) -> Self {
Self {
position,
color: format!("#{}", SRGBA8::from(color).to_rgb_hex()),
alpha: color.a(),
}
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum SpectrumInputUpdate {

View File

@@ -963,12 +963,12 @@ impl Fsm for GradientToolFsmState {
let (start, end) = (unit_to_viewport.transform_point2(DVec2::ZERO), unit_to_viewport.transform_point2(DVec2::X));
fn color_to_hex(color: graphene_std::Color) -> String {
SRGBA8::from(color).to_css_hex()
fn color_to_opaque_hex(color: graphene_std::Color) -> String {
format!("#{}", SRGBA8::from(color).to_rgb_hex())
}
let start_hex = gradient.color(0).map(color_to_hex).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
let end_hex = gradient.color(gradient.len().saturating_sub(1)).map(color_to_hex).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
let start_hex = gradient.color(0).map(color_to_opaque_hex).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
let end_hex = gradient.color(gradient.len().saturating_sub(1)).map(color_to_opaque_hex).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
// Check if the first/last stops are at position ~0/~1 (rendered as the endpoint dots rather than as separate stops)
let settings = appearance.settings;
@@ -1027,7 +1027,7 @@ impl Fsm for GradientToolFsmState {
StopId::End => overlay_context.gradient_color_stop(end, emphasis, &end_hex, !last_at_end),
StopId::Middle(i) => {
if let Some(color) = gradient.color(i) {
overlay_context.gradient_color_stop(start.lerp(end, gradient.position(i, gradient_cyclic)), emphasis, &color_to_hex(color), false);
overlay_context.gradient_color_stop(start.lerp(end, gradient.position(i, gradient_cyclic)), emphasis, &color_to_opaque_hex(color), false);
}
}
};