mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
The canvas built its peniko gradients premultiplied (peniko's default), while Gradient::sample, the PDF export (shading plus SMask) and SVG viewers blend colour and opacity separately: a transparent white stop into black drew grey 127 halfway instead of 191. Opaque gradients are unchanged.
59 lines
3.0 KiB
Rust
59 lines
3.0 KiB
Rust
//! Gradient stops with opacity: colour and opacity blend each on its own (not premultiplied), as
|
|
//! the model samples them ([`Gradient::sample`]) and as the SVG and PDF exports draw them. The colour
|
|
//! of a transparent stop counts: red fading into transparent black darkens as it fades.
|
|
|
|
use vectorcraft_color::{Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
|
|
use vectorcraft_doc::{Appearance, Document};
|
|
use vectorcraft_geom::{Point, Rect, shapes};
|
|
use vectorcraft_testkit::fixtures::DocBuilder;
|
|
use vectorcraft_testkit::raster::{Image, render_view};
|
|
|
|
/// A 100 pt square filled with `gradient` on `geom`.
|
|
fn filled(gradient: &Gradient, geom: GradientGeom) -> Document {
|
|
let mut gp = GradientPaint::new(gradient.clone());
|
|
gp.geom = Some(geom);
|
|
let mut b = DocBuilder::new(100.0, 100.0);
|
|
b.path(shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(Paint::Gradient(Box::new(gp)), Paint::None, 0.0), |_| {});
|
|
b.build()
|
|
}
|
|
|
|
fn stop(offset: f32, color: Color, opacity: f32, midpoint: f32) -> GradientStop {
|
|
GradientStop { opacity, midpoint, ..GradientStop::new(offset, color) }
|
|
}
|
|
|
|
/// Every 7th pixel is the model's colour at its opacity over white (pixel centres).
|
|
fn assert_follows_model(img: &Image, gradient: &Gradient, geom: &GradientGeom) {
|
|
for y in (3..100).step_by(7) {
|
|
for x in (3..100).step_by(7) {
|
|
let t = geom.param_at(gradient.kind, Point::new(x as f64 + 0.5, y as f64 + 0.5)).clamp(0.0, 1.0);
|
|
let (c, o) = gradient.sample(t as f32);
|
|
let want = c.to_rgb().map(|v| ((v * o + 1.0 - o) * 255.0).round() as u8);
|
|
let got = img.over_white(x, y);
|
|
let off = (0..3).map(|k| got[k].abs_diff(want[k])).max().unwrap_or(0);
|
|
assert!(off <= 4, "{:?} ({x},{y}) t={t:.3}: got {got:?}, want {want:?}", gradient.kind);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn transparent_stops_blend_colour_and_opacity_apart() {
|
|
let red = Color::rgb(1.0, 0.0, 0.0);
|
|
let fades = [
|
|
// Transparent white into black: mid-grey at half opacity halfway, light over white.
|
|
vec![stop(0.0, Color::WHITE, 0.0, 0.5), stop(1.0, Color::BLACK, 1.0, 0.5)],
|
|
// Red into transparent black: darker red as it fades.
|
|
vec![stop(0.0, red, 1.0, 0.5), stop(1.0, Color::BLACK, 0.0, 0.5)],
|
|
// A midpoint off centre (drawn as an extra stop) follows the same rule.
|
|
vec![stop(0.0, red, 1.0, 0.3), stop(1.0, Color::BLACK, 0.0, 0.5)],
|
|
];
|
|
for stops in fades {
|
|
for (kind, geom) in [
|
|
(GradientKind::Linear, GradientGeom { start: Point::new(0.0, 50.0), end: Point::new(100.0, 50.0), aspect: 1.0, focal: None }),
|
|
(GradientKind::Radial, GradientGeom { start: Point::new(50.0, 50.0), end: Point::new(100.0, 50.0), aspect: 1.0, focal: None }),
|
|
] {
|
|
let gradient = Gradient { kind, stops: stops.clone() };
|
|
assert_follows_model(&render_view(&filled(&gradient, geom), 100, 100), &gradient, &geom);
|
|
}
|
|
}
|
|
}
|