mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
committed by
Dennis Kobert
parent
9181a62e99
commit
da3ffa1721
@@ -547,7 +547,8 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
|
||||
},
|
||||
extend: peniko_extend(settings.spread),
|
||||
stops: peniko_stops,
|
||||
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
|
||||
// Straight alpha, keeping parity with the SVG renderer's stop interpolation
|
||||
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Unpremultiplied,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -2559,7 +2560,8 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
|
||||
kind,
|
||||
stops,
|
||||
extend,
|
||||
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
|
||||
// Straight alpha, keeping parity with the SVG renderer's stop interpolation
|
||||
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Unpremultiplied,
|
||||
..Default::default()
|
||||
});
|
||||
let brush_transform = kurbo::Affine::new(gradient_placement(gradient_transform, gradient_form).to_cols_array());
|
||||
|
||||
@@ -92,9 +92,9 @@ impl From<&GradientStops<SRGBA8>> for Gradient {
|
||||
}
|
||||
|
||||
impl GradientStops<SRGBA8> {
|
||||
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
|
||||
pub fn to_css_linear_gradient(&self, settings: GradientSettings) -> String {
|
||||
Gradient::from(self).to_css_linear_gradient(settings)
|
||||
/// CSS `background-image` value drawing the stops as an SVG data URI, keeping straight-alpha interpolation.
|
||||
pub fn to_svg_background_image(&self, settings: GradientSettings) -> String {
|
||||
Gradient::from(self).to_svg_background_image(settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1296,22 +1296,33 @@ impl Gradient {
|
||||
mapped
|
||||
}
|
||||
|
||||
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves and color space so the rendered gradient matches Graphite's interpolation rather than browser defaults.
|
||||
pub fn to_css_linear_gradient(&self, settings: GradientSettings) -> String {
|
||||
if self.len() <= 1 {
|
||||
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%)");
|
||||
/// The gradient's [`Gradient::interpolated_samples`], falling back to one black sample when it has no stops, since a
|
||||
/// stopless SVG gradient paints nothing where [`Gradient::evaluate`] gives black.
|
||||
pub fn interpolated_samples_or_black(&self, settings: GradientSettings) -> Vec<(f64, Color, Option<f64>)> {
|
||||
let samples = self.interpolated_samples(settings);
|
||||
if samples.is_empty() { vec![(0., Color::BLACK, None)] } else { samples }
|
||||
}
|
||||
|
||||
/// Build a CSS `background-image` value embedding the gradient as an SVG data URI, sampling the midpoint curves, color
|
||||
/// space, and spline. SVG interpolates its stops with straight alpha, matching the canvas renderers, where a CSS
|
||||
/// `linear-gradient` interpolates premultiplied and would hide the pull a transparent stop's RGB exerts on the render.
|
||||
pub fn to_svg_background_image(&self, settings: GradientSettings) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut stops = String::new();
|
||||
for (position, color, _) in self.interpolated_samples_or_black(settings) {
|
||||
let srgba = SRGBA8::from(color);
|
||||
let _ = write!(stops, "<stop offset='{}' stop-color='#{}'", (position * 1e4).round() / 1e4, srgba.to_rgb_hex());
|
||||
if srgba.alpha < 255 {
|
||||
let _ = write!(stops, " stop-opacity='{}'", (color.a() as f64 * 1000.).round() / 1000.);
|
||||
}
|
||||
stops.push_str("/>");
|
||||
}
|
||||
let pieces = self
|
||||
.interpolated_samples(settings)
|
||||
.into_iter()
|
||||
.map(|(position, color, _)| {
|
||||
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||
format!("#{} {percent}%", SRGBA8::from(color).to_rgba_hex())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("linear-gradient(to right, {pieces})")
|
||||
|
||||
// A sizeless SVG stretches to fill the CSS background area; the encoding covers the URI-hostile characters
|
||||
let svg = format!("<svg xmlns='http://www.w3.org/2000/svg'><linearGradient id='g' x1='0' y1='0' x2='1' y2='0'>{stops}</linearGradient><rect width='100%' height='100%' fill='url(#g)'/></svg>");
|
||||
let encoded = svg.replace('%', "%25").replace('#', "%23").replace('<', "%3C").replace('>', "%3E");
|
||||
format!("url(\"data:image/svg+xml,{encoded}\")")
|
||||
}
|
||||
|
||||
/// Produce a set of linearly-interpolated color samples that approximate the gradient's true curve.
|
||||
@@ -2194,6 +2205,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_background_image_percent_encodes_and_keeps_straight_alpha_stops() {
|
||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||
gradient.set_color(1, Color::from_rgbaf32_unchecked(1., 1., 1., 0.5));
|
||||
|
||||
let image = gradient.to_svg_background_image(GradientSettings::default());
|
||||
|
||||
assert!(image.starts_with("url(\"data:image/svg+xml,"), "the value should be an SVG data URI: {image}");
|
||||
assert!(image.contains("stop-opacity='0.5'"), "a transparent stop should emit its straight alpha: {image}");
|
||||
assert!(!image.contains(['#', '<', '>']), "URI-hostile characters should be percent-encoded: {image}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_background_image_paints_a_stopless_gradient_black() {
|
||||
let image = Gradient::from(Vec::new()).to_svg_background_image(GradientSettings::default());
|
||||
|
||||
// The hex color's `#` arrives percent-encoded
|
||||
assert!(image.contains("stop-color='%23000000'"), "a gradient with no stops should paint black rather than nothing: {image}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
|
||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||
|
||||
@@ -67,7 +67,7 @@ impl<C> FillChoice<C> {
|
||||
}
|
||||
|
||||
impl FillChoice<SRGBA8> {
|
||||
/// Build a CSS `background-image` string (always a `linear-gradient(...)`) representing this fill, or `None` if the fill is [`FillChoice::None`].
|
||||
/// Build a CSS `background-image` string representing this fill, or `None` if the fill is [`FillChoice::None`].
|
||||
/// Solid colors become a degenerate gradient between the same color so the CSS variable can always be assigned to a `background-image`.
|
||||
pub fn to_css_background_image(&self) -> Option<String> {
|
||||
match self {
|
||||
@@ -76,7 +76,7 @@ impl FillChoice<SRGBA8> {
|
||||
let hex = srgba.to_rgba_hex();
|
||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||
}
|
||||
Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient(ramp.into())),
|
||||
Self::Gradient(ramp) => Some(ramp.stops.to_svg_background_image(ramp.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user