Skip to main content

devpage_design/
shadow.rs

1//! Shadows: a box's shape blurred by a true Gaussian, as display-list
2//! commands both backends replay.
3//!
4//! Neither vello's blurred rounded rectangle — one radius for all four corners
5//! — nor any filter both backends share draws that, so the engine computes the
6//! blur itself, sampling the shape every half standard deviation, which is fine
7//! enough that the samples, drawn smoothly, are the blur at any zoom.
8//!
9//! A small shadow is one image of those samples. A large one would need too
10//! many — a 16,000-pixel box blurred by 2 has a billion — but away from its
11//! corners a blurred rounded rectangle doesn't vary along its edges, so it is
12//! drawn as pieces instead: solid where the blur can't reach, each edge's
13//! one-dimensional profile stretched along it, and each corner on its own —
14//! sampled in two dimensions, or for a corner far rounder than the blur, the
15//! exact blur of a disc's edge as a radial gradient. The pieces meet edge to
16//! edge, each added to what is under it, so no seam shows where two meet at any
17//! zoom; together, in one layer at the shadow color's opacity, they are the
18//! shadow. An ellipse that isn't a circle has no straight edges, so its shadow
19//! is tiled: solid tiles inside, sampled tiles along its edge, nothing outside.
20
21use std::collections::BTreeMap;
22use std::sync::Arc;
23
24use vello_cpu::kurbo::{Affine, BezPath, Ellipse as KEllipse, Point, Rect, RoundedRect, Shape};
25use vello_cpu::peniko::{Blob, Color as PColor, Gradient, ImageAlphaType, ImageData, ImageFormat};
26use vello_cpu::{Pixmap, RenderContext, Resources};
27
28use crate::render::{Draw, Paint};
29use crate::resolve::Rgba;
30
31/// How far a blur reaches, in samples: three standard deviations at two
32/// samples a standard deviation. Past it the Gaussian is under 0.15%, less
33/// than half of one step of 255.
34const REACH: usize = 6;
35/// The most samples a shadow drawn as one image may have, and its longest side.
36pub const SINGLE: usize = 1 << 16;
37const SINGLE_SIDE: usize = 1024;
38/// A corner at least this many standard deviations round is drawn as a disc's
39/// blurred edge, within 0.1 of 255 of the exact blur; a tighter one is sampled.
40const ROUND: f64 = 16.0;
41/// The side, in samples, of a tile of an ellipse's shadow.
42const TILE: usize = 128;
43/// How far the layer a piece is added from reaches past the piece, in scene
44/// pixels: so far that no pixel the layer's own edge crosses holds any of the
45/// piece, at any zoom out to 1/128.
46const MARGIN: f64 = 128.0;
47
48/// The shape a shadow blurs: a rounded rectangle — each corner's radius as
49/// kurbo clamps it — or an ellipse.
50#[derive(Clone, Copy)]
51pub enum Outline {
52    Rounded(Rect, [f64; 4]),
53    Ellipse(Rect),
54}
55
56impl Outline {
57    /// A rounded rectangle with radii `[top-left, top-right, bottom-right,
58    /// bottom-left]`, or a circle as one with every radius half its side.
59    pub fn rounded(rect: Rect, radii: [f64; 4]) -> Outline {
60        let r = RoundedRect::from_rect(rect, (radii[0], radii[1], radii[2], radii[3])).radii();
61        Outline::Rounded(
62            rect,
63            [r.top_left, r.top_right, r.bottom_right, r.bottom_left],
64        )
65    }
66
67    pub fn ellipse(rect: Rect) -> Outline {
68        if rect.width() == rect.height() {
69            Outline::rounded(rect, [rect.width() / 2.0; 4])
70        } else {
71            Outline::Ellipse(rect)
72        }
73    }
74
75    pub fn path(&self) -> BezPath {
76        match *self {
77            Outline::Rounded(r, [tl, tr, br, bl]) => {
78                RoundedRect::from_rect(r, (tl, tr, br, bl)).to_path(0.1)
79            }
80            Outline::Ellipse(r) => KEllipse::from_rect(r).to_path(0.1),
81        }
82    }
83
84    /// The outline as straight lines within `tolerance` of it. The
85    /// rasterizer would flatten a curve itself, within a quarter of a sample
86    /// — inside it, as a chord is — which moves a blurred curve's edge by as
87    /// much as 2% of its alpha.
88    fn lines(&self, tolerance: f64) -> BezPath {
89        let mut out = BezPath::new();
90        let shape = match *self {
91            Outline::Rounded(r, [tl, tr, br, bl]) => {
92                RoundedRect::from_rect(r, (tl, tr, br, bl)).to_path(tolerance / 10.0)
93            }
94            Outline::Ellipse(r) => KEllipse::from_rect(r).to_path(tolerance / 10.0),
95        };
96        vello_cpu::kurbo::flatten(shape, tolerance, |el| out.push(el));
97        out
98    }
99
100    fn rect(&self) -> Rect {
101        match *self {
102            Outline::Rounded(r, _) | Outline::Ellipse(r) => r,
103        }
104    }
105
106    /// The signed distance from `p` to the edge, negative inside.
107    fn distance(&self, p: Point) -> f64 {
108        let r = self.rect();
109        let c = r.center();
110        let (hx, hy) = (r.width() / 2.0, r.height() / 2.0);
111        let (dx, dy) = (p.x - c.x, p.y - c.y);
112        match *self {
113            Outline::Rounded(_, [tl, tr, br, bl]) => {
114                let radius = match (dx >= 0.0, dy >= 0.0) {
115                    (false, false) => tl,
116                    (true, false) => tr,
117                    (true, true) => br,
118                    (false, true) => bl,
119                };
120                let (qx, qy) = (dx.abs() - hx + radius, dy.abs() - hy + radius);
121                qx.max(qy).min(0.0) + qx.max(0.0).hypot(qy.max(0.0)) - radius
122            }
123            Outline::Ellipse(_) => ellipse_distance(hx, hy, dx, dy),
124        }
125    }
126
127    /// Whether all of `r` is inside — for a convex shape, its four corners.
128    fn holds(&self, r: Rect) -> bool {
129        [(r.x0, r.y0), (r.x1, r.y0), (r.x1, r.y1), (r.x0, r.y1)]
130            .iter()
131            .all(|&(x, y)| self.distance(Point::new(x, y)) <= 0.0)
132    }
133
134    /// Whether `r` certainly lies outside.
135    fn misses(&self, r: Rect) -> bool {
136        if let Outline::Ellipse(e) = *self {
137            // Scaled to a unit circle the rectangle is still one, and its
138            // point nearest the center is the center clamped into it.
139            let c = e.center();
140            let (a, b) = (e.width() / 2.0, e.height() / 2.0);
141            let nx = (c.x.clamp(r.x0, r.x1) - c.x) / a;
142            let ny = (c.y.clamp(r.y0, r.y1) - c.y) / b;
143            return nx.hypot(ny) > 1.0;
144        }
145        self.distance(r.center()) > r.width().hypot(r.height()) / 2.0
146    }
147}
148
149/// The normalized Gaussian over `-REACH..=REACH` samples, two samples a
150/// standard deviation.
151fn kernel() -> [f32; 2 * REACH + 1] {
152    let mut k = [0f32; 2 * REACH + 1];
153    for (i, w) in k.iter_mut().enumerate() {
154        let x = i as f64 - REACH as f64;
155        *w = (-x * x / 8.0).exp() as f32;
156    }
157    let total: f32 = k.iter().sum();
158    k.map(|w| w / total)
159}
160
161/// The grid a shadow is sampled on: sample `(i, j)` covers the square of side
162/// `step` whose top-left is `origin + (i, j) × step`.
163#[derive(Clone, Copy)]
164struct Grid {
165    origin: Point,
166    step: f64,
167}
168
169impl Grid {
170    fn x(&self, i: isize) -> f64 {
171        self.origin.x + i as f64 * self.step
172    }
173    fn y(&self, j: isize) -> f64 {
174        self.origin.y + j as f64 * self.step
175    }
176    fn rect(&self, i0: isize, j0: isize, i1: isize, j1: isize) -> Rect {
177        Rect::new(self.x(i0), self.y(j0), self.x(i1), self.y(j1))
178    }
179    /// The transform from an image whose pixel `(0, 0)` is sample `(i, j)`.
180    fn place(&self, i: isize, j: isize) -> Affine {
181        Affine::translate((self.x(i), self.y(j))) * Affine::scale(self.step)
182    }
183}
184
185/// The blurred shape at samples `i0..i0 + w` × `j0..j0 + h`, row by row: its
186/// coverage of each sample out to the blur's reach, blurred down each column
187/// and then across each row.
188fn blurred(path: &BezPath, grid: Grid, i0: isize, j0: isize, w: usize, h: usize) -> Vec<f32> {
189    let (cw, ch) = (w + 2 * REACH, h + 2 * REACH);
190    let mut ctx = RenderContext::new(cw as u16, ch as u16);
191    ctx.set_transform(
192        Affine::scale(1.0 / grid.step)
193            * Affine::translate((-grid.x(i0 - REACH as isize), -grid.y(j0 - REACH as isize))),
194    );
195    ctx.set_paint(PColor::from_rgba8(0, 0, 0, 255));
196    ctx.fill_path(path);
197    ctx.flush();
198    let mut coverage = Pixmap::new(cw as u16, ch as u16);
199    ctx.render(&mut coverage, &mut Resources::default());
200    let cov: Vec<f32> = coverage
201        .data_as_u8_slice()
202        .iter()
203        .skip(3)
204        .step_by(4)
205        .map(|&a| a as f32 / 255.0)
206        .collect();
207    let k = kernel();
208    let mut down = vec![0f32; h * cw];
209    for y in 0..h {
210        let row = &mut down[y * cw..][..cw];
211        for (t, weight) in k.iter().enumerate() {
212            let from = &cov[(y + t) * cw..][..cw];
213            for (o, c) in row.iter_mut().zip(from) {
214                *o += weight * c;
215            }
216        }
217    }
218    let mut out = vec![0f32; w * h];
219    for y in 0..h {
220        let from = &down[y * cw..][..cw];
221        for (x, o) in out[y * w..][..w].iter_mut().enumerate() {
222            *o = k.iter().zip(&from[x..]).map(|(a, b)| a * b).sum();
223        }
224    }
225    out
226}
227
228/// The blur of the interval `lo..hi` at samples `from..from + len` of a line
229/// whose sample `0` starts at `origin`: a straight edge's profile.
230fn profile(lo: f64, hi: f64, origin: f64, step: f64, from: isize, len: usize) -> Vec<f32> {
231    let cov: Vec<f32> = (0..len + 2 * REACH)
232        .map(|n| {
233            let a = origin + (from + n as isize - REACH as isize) as f64 * step;
234            ((hi.min(a + step) - lo.max(a)) / step).clamp(0.0, 1.0) as f32
235        })
236        .collect();
237    let k = kernel();
238    (0..len)
239        .map(|i| k.iter().zip(&cov[i..]).map(|(a, b)| a * b).sum())
240        .collect()
241}
242
243/// `values`, `w` × `h`, as an image of `c` whose alpha is each value times
244/// `alpha`.
245fn image(values: &[f32], w: usize, h: usize, c: Rgba, alpha: f32) -> ImageData {
246    let pixels: Vec<u8> = values
247        .iter()
248        .flat_map(|v| [c.r, c.g, c.b, (v.clamp(0.0, 1.0) * alpha).round() as u8])
249        .collect();
250    ImageData {
251        data: Blob::new(Arc::new(pixels)),
252        format: ImageFormat::Rgba8,
253        alpha_type: ImageAlphaType::Alpha,
254        width: w as u32,
255        height: h as u32,
256    }
257}
258
259/// The normal distribution's cumulative function, to 1.5 × 10⁻⁷
260/// (Abramowitz and Stegun, 7.1.26).
261fn phi_cdf(x: f64) -> f64 {
262    let z = x.abs() / std::f64::consts::SQRT_2;
263    let t = 1.0 / (1.0 + 0.3275911 * z);
264    let poly = t
265        * (0.254829592
266            + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
267    let erf = 1.0 - poly * (-z * z).exp();
268    0.5 * (1.0 + if x < 0.0 { -erf } else { erf })
269}
270
271/// One piece of a large shadow: the area it covers and how it is painted.
272enum Piece {
273    /// Every sample inside the blur's reach of the shape: solid.
274    Solid(BezPath),
275    /// Where the blur varies only down: the profile of rows `j0..j1`.
276    Down(BezPath, isize, isize),
277    /// Where it varies only across: the profile of columns `i0..i1`.
278    Across(BezPath, isize, isize),
279    /// A round corner: the blurred edge of the disc centered at the point,
280    /// of the radius.
281    Disc(BezPath, Point, f64),
282    /// Samples `i0..i1` × `j0..j1`, blurred in two dimensions, cut to the
283    /// path, or else to those samples.
284    Patch(Option<BezPath>, isize, isize, isize, isize),
285}
286
287/// Adds `outline`'s shadow — blurred by a Gaussian of `std_dev` pixels and
288/// colored `c` — to `list`, and each image it paints, in order, to `images`.
289pub fn shadow(
290    outline: Outline,
291    std_dev: f64,
292    c: Rgba,
293    list: &mut Vec<Draw>,
294    images: &mut Vec<ImageData>,
295) {
296    shadow_with(outline, std_dev, c, list, images, SINGLE)
297}
298
299/// [`shadow`], drawing one image when it takes no more than `single` samples.
300pub fn shadow_with(
301    outline: Outline,
302    std_dev: f64,
303    c: Rgba,
304    list: &mut Vec<Draw>,
305    images: &mut Vec<ImageData>,
306    single: usize,
307) {
308    let rect = outline.rect();
309    if rect.width() <= 0.0 || rect.height() <= 0.0 {
310        return;
311    }
312    let color = PColor::from_rgba8(c.r, c.g, c.b, c.a);
313    if std_dev <= 0.0 {
314        list.push(Draw::Fill(outline.path(), Paint::Solid(color)));
315        return;
316    }
317    let step = std_dev / 2.0;
318    let reach = REACH as f64 * step;
319    let grid = Grid {
320        origin: Point::new(rect.x0 - reach, rect.y0 - reach),
321        step,
322    };
323    let nx = ((rect.width() + 2.0 * reach) / step).ceil() as usize;
324    let ny = ((rect.height() + 2.0 * reach) / step).ceil() as usize;
325    // A hundredth of a sample.
326    let path = outline.lines(step / 100.0);
327    if nx * ny <= single && nx <= SINGLE_SIDE && ny <= SINGLE_SIDE {
328        let values = blurred(&path, grid, 0, 0, nx, ny);
329        let data = image(&values, nx, ny, c, c.a as f32);
330        images.push(data.clone());
331        list.push(Draw::Fill(
332            grid.rect(0, 0, nx as isize, ny as isize).to_path(0.1),
333            Paint::Image(data, grid.place(0, 0)),
334        ));
335        return;
336    }
337    let (nx, ny) = (nx as isize, ny as isize);
338    let pieces = match outline {
339        Outline::Rounded(r, radii) => rounded(r, radii, std_dev, grid, nx, ny),
340        Outline::Ellipse(e) => ellipse(e, std_dev, grid, nx, ny),
341    };
342    let extent = grid.rect(0, 0, nx, ny);
343    list.push(Draw::PushOpacity(
344        c.a as f32 / 255.0,
345        Some(extent.to_path(0.1)),
346    ));
347    let opaque = Rgba { a: 255, ..c };
348    let paint_over = |clip: &BezPath| clip.bounding_box().inflate(MARGIN, MARGIN).to_path(0.1);
349    for piece in pieces {
350        let (clip, paint) = match piece {
351            Piece::Solid(clip) => (clip, Paint::Solid(color.with_alpha(1.0))),
352            Piece::Down(clip, j0, j1) => {
353                let len = (j1 - j0 + 2) as usize;
354                let values = profile(rect.y0, rect.y1, grid.origin.y, step, j0 - 1, len);
355                let data = image(&values, 1, len, opaque, 255.0);
356                images.push(data.clone());
357                let place = Affine::translate((0.0, grid.y(j0 - 1))) * Affine::scale(step);
358                (clip, Paint::Image(data, place))
359            }
360            Piece::Across(clip, i0, i1) => {
361                let len = (i1 - i0 + 2) as usize;
362                let values = profile(rect.x0, rect.x1, grid.origin.x, step, i0 - 1, len);
363                let data = image(&values, len, 1, opaque, 255.0);
364                images.push(data.clone());
365                let place = Affine::translate((grid.x(i0 - 1), 0.0)) * Affine::scale(step);
366                (clip, Paint::Image(data, place))
367            }
368            Piece::Disc(clip, center, radius) => {
369                (clip, Paint::Gradient(disc(center, radius, std_dev, c)))
370            }
371            Piece::Patch(clip, i0, j0, i1, j1) => {
372                let (w, h) = ((i1 - i0 + 2) as usize, (j1 - j0 + 2) as usize);
373                let values = blurred(&path, grid, i0 - 1, j0 - 1, w, h);
374                let data = image(&values, w, h, opaque, 255.0);
375                images.push(data.clone());
376                (
377                    clip.unwrap_or_else(|| grid.rect(i0, j0, i1, j1).to_path(0.1)),
378                    Paint::Image(data, grid.place(i0 - 1, j0 - 1)),
379                )
380            }
381        };
382        // The piece's own edge cuts it, anti-aliased, so where two meet each
383        // adds its share of the pixel; the layer reaches well past it.
384        list.push(Draw::PushAdd(paint_over(&clip)));
385        list.push(Draw::Fill(clip, paint));
386        list.push(Draw::Pop);
387    }
388    list.push(Draw::Pop);
389}
390
391/// The blurred edge of a disc of `radius` around `center`, as a radial
392/// gradient from where it is solid to where it is gone. A Gaussian of σ
393/// across the edge of a disc of radius R is, to within 0.1 of 255 once R is
394/// [`ROUND`] σ, the straight edge's minus φ(d)·σ/2R — the disc's curve taking
395/// a little from it.
396fn disc(center: Point, radius: f64, std_dev: f64, c: Rgba) -> Gradient {
397    let reach = REACH as f64 * std_dev / 2.0;
398    let (r0, r1) = (radius - reach, radius + reach);
399    const STOPS: usize = 49;
400    let stops: Vec<(f32, PColor)> = (0..STOPS)
401        .map(|k| {
402            let t = k as f64 / (STOPS - 1) as f64;
403            let d = (r0 + t * (r1 - r0) - radius) / std_dev;
404            let density = (-d * d / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
405            let v = (phi_cdf(-d) - density * std_dev / (2.0 * radius)).clamp(0.0, 1.0);
406            (
407                t as f32,
408                PColor::new([
409                    c.r as f32 / 255.0,
410                    c.g as f32 / 255.0,
411                    c.b as f32 / 255.0,
412                    v as f32,
413                ]),
414            )
415        })
416        .collect();
417    Gradient::new_two_point_radial(center, r0 as f32, center, r1 as f32)
418        .with_stops(stops.as_slice())
419}
420
421/// The pieces of a rounded rectangle's shadow over samples `0..nx` ×
422/// `0..ny`. Away from a corner's reach the blur depends on one coordinate
423/// only: down where the column lies a blur's reach inside both sides, across
424/// where the row does, and is solid where both do.
425fn rounded(
426    rect: Rect,
427    radii: [f64; 4],
428    std_dev: f64,
429    grid: Grid,
430    nx: isize,
431    ny: isize,
432) -> Vec<Piece> {
433    let reach = REACH as f64 * grid.step;
434    let (sx, sy) = (
435        |x: f64| (x - grid.origin.x) / grid.step,
436        |y: f64| (y - grid.origin.y) / grid.step,
437    );
438    let clamp_x = |v: f64| (v as isize).clamp(0, nx);
439    let clamp_y = |v: f64| (v as isize).clamp(0, ny);
440    // Where the blur no longer reaches either side.
441    let (xl, xr) = (2 * REACH as isize, clamp_x(sx(rect.x1 - reach).floor()));
442    let (yt, yb) = (2 * REACH as isize, clamp_y(sy(rect.y1 - reach).floor()));
443    let [tl, tr, br, bl] = radii;
444    let (w, h) = (rect.width(), rect.height());
445    // The straight run between each pair of corners.
446    let (top, right, bottom, left) = (w - tl - tr, h - tr - br, w - br - bl, h - tl - bl);
447    // A corner is a disc's edge when it is round enough and each side
448    // leaving it runs straight for the blur's reach — or into a corner of
449    // the same circle.
450    let disc_ok = |r: f64, a: (f64, f64), b: (f64, f64)| {
451        let run = |(len, other): (f64, f64)| len >= reach || (len <= 1e-9 && other == r);
452        r >= ROUND * std_dev && run(a) && run(b)
453    };
454    struct Corner {
455        zone: [isize; 4],
456        disc: Option<Point>,
457        radius: f64,
458    }
459    let mut corners = Vec::new();
460    for (r, at_right, at_bottom, a, b) in [
461        (tl, false, false, (top, tr), (left, bl)),
462        (tr, true, false, (top, tl), (right, br)),
463        (br, true, true, (bottom, bl), (right, tr)),
464        (bl, false, true, (bottom, br), (left, tl)),
465    ] {
466        let disc = disc_ok(r, a, b);
467        // A disc's quadrant reaches its center; a sampled corner, the blur's
468        // reach past the corner's square.
469        let inner = if disc { r } else { r + reach };
470        let (x0, x1) = if at_right {
471            let x = sx(rect.x1 - inner);
472            (clamp_x(if disc { x.round() } else { x.floor() }), nx)
473        } else {
474            let x = sx(rect.x0 + inner);
475            (0, clamp_x(if disc { x.round() } else { x.ceil() }))
476        };
477        let (y0, y1) = if at_bottom {
478            let y = sy(rect.y1 - inner);
479            (clamp_y(if disc { y.round() } else { y.floor() }), ny)
480        } else {
481            let y = sy(rect.y0 + inner);
482            (0, clamp_y(if disc { y.round() } else { y.ceil() }))
483        };
484        let center = Point::new(
485            if at_right { rect.x1 - r } else { rect.x0 + r },
486            if at_bottom { rect.y1 - r } else { rect.y0 + r },
487        );
488        corners.push(Corner {
489            zone: [x0, y0, x1, y1],
490            disc: disc.then_some(center),
491            radius: r,
492        });
493    }
494    let mut xs = vec![0, nx, xl.min(nx), xr];
495    let mut ys = vec![0, ny, yt.min(ny), yb];
496    for c in &corners {
497        xs.extend([c.zone[0], c.zone[2]]);
498        ys.extend([c.zone[1], c.zone[3]]);
499    }
500    xs.sort_unstable();
501    xs.dedup();
502    ys.sort_unstable();
503    ys.dedup();
504    let mut solid = BezPath::new();
505    let mut down: BTreeMap<(isize, isize), BezPath> = BTreeMap::new();
506    let mut across: BTreeMap<(isize, isize), BezPath> = BTreeMap::new();
507    let mut discs: BTreeMap<usize, BezPath> = BTreeMap::new();
508    let mut patches = Vec::new();
509    // A sampled corner whose zone meets no other's is one patch.
510    let meets =
511        |a: &[isize; 4], b: &[isize; 4]| a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3];
512    let whole: Vec<bool> = corners
513        .iter()
514        .enumerate()
515        .map(|(n, c)| {
516            c.disc.is_none()
517                && corners
518                    .iter()
519                    .enumerate()
520                    .all(|(m, o)| m == n || !meets(&c.zone, &o.zone))
521        })
522        .collect();
523    for (c, _) in corners.iter().zip(&whole).filter(|(_, w)| **w) {
524        let [i0, j0, i1, j1] = c.zone;
525        if i0 < i1 && j0 < j1 {
526            patches.push((i0, j0, i1, j1));
527        }
528    }
529    let add = |path: &mut BezPath, r: Rect| path.extend(r.path_elements(0.1));
530    for ys in ys.windows(2) {
531        let (j0, j1) = (ys[0], ys[1]);
532        for xs in xs.windows(2) {
533            let (i0, i1) = (xs[0], xs[1]);
534            let cell = grid.rect(i0, j0, i1, j1);
535            let within = |z: &[isize; 4]| z[0] <= i0 && i1 <= z[2] && z[1] <= j0 && j1 <= z[3];
536            if corners
537                .iter()
538                .zip(&whole)
539                .any(|(c, w)| *w && within(&c.zone))
540            {
541                // Already a patch of its own.
542            } else if corners.iter().any(|c| c.disc.is_none() && within(&c.zone)) {
543                patches.push((i0, j0, i1, j1));
544            } else if let Some(n) = corners
545                .iter()
546                .position(|c| c.disc.is_some() && within(&c.zone))
547            {
548                add(discs.entry(n).or_default(), cell);
549            } else {
550                let central_x = xl <= i0 && i1 <= xr;
551                let central_y = yt <= j0 && j1 <= yb;
552                match (central_x, central_y) {
553                    (true, true) => add(&mut solid, cell),
554                    (true, false) => add(down.entry((j0, j1)).or_default(), cell),
555                    (false, true) => add(across.entry((i0, i1)).or_default(), cell),
556                    // Never, as a corner's zone covers where neither holds;
557                    // if it did, sampling it is still exact.
558                    (false, false) => patches.push((i0, j0, i1, j1)),
559                }
560            }
561        }
562    }
563    let outline = Outline::Rounded(rect, radii);
564    let mut pieces = Vec::new();
565    if !solid.elements().is_empty() {
566        pieces.push(Piece::Solid(solid));
567    }
568    // Bands that meet are one piece, with one image: every join between two
569    // pieces inside a pixel costs a rounding in each, so a straight run has
570    // none.
571    pieces.extend(runs(down).map(|((j0, j1), p)| Piece::Down(p, j0, j1)));
572    pieces.extend(runs(across).map(|((i0, i1), p)| Piece::Across(p, i0, i1)));
573    pieces.extend(
574        discs
575            .into_iter()
576            .map(|(n, p)| Piece::Disc(p, corners[n].disc.expect("a disc"), corners[n].radius)),
577    );
578    for (i0, j0, i1, j1) in merged(patches) {
579        pieces.extend(tiles(&outline, grid, i0, j0, i1, j1));
580    }
581    pieces
582}
583
584/// Bands `(from, to)` of cells, merged where one ends as the next begins.
585fn runs(
586    bands: BTreeMap<(isize, isize), BezPath>,
587) -> impl Iterator<Item = ((isize, isize), BezPath)> {
588    let mut out: Vec<((isize, isize), BezPath)> = Vec::new();
589    for ((from, to), path) in bands {
590        match out.last_mut() {
591            Some(((_, end), run)) if *end == from => {
592                *end = to;
593                run.extend(path);
594            }
595            _ => out.push(((from, to), path)),
596        }
597    }
598    out.into_iter()
599}
600
601/// Cells `(i0, j0, i1, j1)` of a grid, none overlapping, merged into fewer
602/// rectangles: along each band first, then down where the runs line up.
603fn merged(mut cells: Vec<(isize, isize, isize, isize)>) -> Vec<(isize, isize, isize, isize)> {
604    cells.sort_unstable_by_key(|&(i0, j0, _, j1)| (j0, j1, i0));
605    let mut rows: Vec<(isize, isize, isize, isize)> = Vec::new();
606    for c in cells {
607        match rows.last_mut() {
608            Some(r) if (r.1, r.3, r.2) == (c.1, c.3, c.0) => r.2 = c.2,
609            _ => rows.push(c),
610        }
611    }
612    rows.sort_unstable_by_key(|&(i0, j0, i1, _)| (i0, i1, j0));
613    let mut out: Vec<(isize, isize, isize, isize)> = Vec::new();
614    for c in rows {
615        match out.last_mut() {
616            Some(r) if (r.0, r.2, r.3) == (c.0, c.2, c.1) => r.3 = c.3,
617            _ => out.push(c),
618        }
619    }
620    out
621}
622
623/// Samples `i0..i1` × `j0..j1` of `outline`'s shadow as tiles of at most
624/// [`TILE`] samples a side: none where the blur can't reach the shape, one
625/// solid piece where the shape fills all it reaches, and each other tile
626/// sampled.
627fn tiles(outline: &Outline, grid: Grid, i0: isize, j0: isize, i1: isize, j1: isize) -> Vec<Piece> {
628    let tile = TILE as isize;
629    if (i1 - i0) <= tile && (j1 - j0) <= tile {
630        return vec![Piece::Patch(None, i0, j0, i1, j1)];
631    }
632    // A tile's samples, its border and how far the blur reaches.
633    let reach = REACH as isize + 1;
634    let mut solid = BezPath::new();
635    let mut pieces = Vec::new();
636    let mut j = j0;
637    while j < j1 {
638        let je = (j + tile).min(j1);
639        let mut run: Option<isize> = None;
640        let mut i = i0;
641        while i < i1 {
642            let ie = (i + tile).min(i1);
643            let reached = grid.rect(i - reach, j - reach, ie + reach, je + reach);
644            let full = outline.holds(reached);
645            if full {
646                run.get_or_insert(i);
647            } else {
648                if let Some(start) = run.take() {
649                    solid.extend(grid.rect(start, j, i, je).path_elements(0.1));
650                }
651                if !outline.misses(reached) {
652                    pieces.push(Piece::Patch(None, i, j, ie, je));
653                }
654            }
655            i = ie;
656        }
657        if let Some(start) = run {
658            solid.extend(grid.rect(start, j, i1, je).path_elements(0.1));
659        }
660        j = je;
661    }
662    if !solid.elements().is_empty() {
663        pieces.insert(0, Piece::Solid(solid));
664    }
665    pieces
666}
667
668/// The signed distance from `(x, y)` to the ellipse of semi-axes `a` and `b`
669/// around the origin: from its nearest point, found by Newton's method on the
670/// angle where the offset to it is normal to the curve.
671fn ellipse_distance(a: f64, b: f64, x: f64, y: f64) -> f64 {
672    // By symmetry, in the first quadrant, where the nearest point is too.
673    let (px, py) = (x.abs(), y.abs());
674    let mut t = (a * py).atan2(b * px);
675    for _ in 0..40 {
676        let (sin, cos) = t.sin_cos();
677        let (ex, ey) = (a * cos, b * sin);
678        // f(t) = (p − e(t)) · e'(t), and its derivative.
679        let f = (px - ex) * -a * sin + (py - ey) * b * cos;
680        let df =
681            (a * a * sin * sin + b * b * cos * cos) - (px - ex) * a * cos - (py - ey) * b * sin;
682        if df.abs() < 1e-12 {
683            break;
684        }
685        let next = (t - f / df).clamp(0.0, std::f64::consts::FRAC_PI_2);
686        if (next - t).abs() < 1e-12 {
687            t = next;
688            break;
689        }
690        t = next;
691    }
692    let (sin, cos) = t.sin_cos();
693    let d = (px - a * cos).hypot(py - b * sin);
694    if (px / a).hypot(py / b) <= 1.0 { -d } else { d }
695}
696
697/// The pieces of an ellipse's shadow. Cut by rays from its center into
698/// wedges, each short enough that the ellipse stays within a hundredth of a
699/// standard deviation of its circle of curvature there, a wedge is that
700/// circle's blurred edge — or, where the ellipse curves too tightly for that,
701/// sampled; inside the wedges it is solid. An ellipse too thin to be solid
702/// anywhere is tiled instead.
703fn ellipse(e: Rect, std_dev: f64, grid: Grid, nx: isize, ny: isize) -> Vec<Piece> {
704    let outline = Outline::Ellipse(e);
705    let step = grid.step;
706    let (a, b) = (e.width() / 2.0, e.height() / 2.0);
707    let c = e.center();
708    // How far inside every sample a blur reaches is covered, and how far
709    // outside none is.
710    let inside = (REACH as f64 * step + step) * std::f64::consts::SQRT_2;
711    let outside = inside + step;
712    if a.min(b) <= 4.0 * inside {
713        return tiles(&outline, grid, 0, 0, nx, ny);
714    }
715    let at = |t: f64| Point::new(c.x + a * t.cos(), c.y + b * t.sin());
716    let normal = |t: f64| {
717        let n = vello_cpu::kurbo::Vec2::new(b * t.cos(), a * t.sin());
718        n / n.hypot()
719    };
720    let rho = |t: f64| {
721        let (sin, cos) = t.sin_cos();
722        (a * a * sin * sin + b * b * cos * cos).powf(1.5) / (a * b)
723    };
724    let distance = |p: Point| ellipse_distance(a, b, p.x - c.x, p.y - c.y);
725    // Where the ray through the point at `t` crosses `depth` inside and
726    // `outside` outside, or `None` when no point on it lies that deep.
727    let cut = |t: f64| -> Option<(Point, Point)> {
728        let p = at(t);
729        let ray = p - c;
730        let u = ray / ray.hypot();
731        let cos = u.dot(normal(t)).max(1e-6);
732        let mut s = inside / cos;
733        while distance(p - u * s) > -inside {
734            s += step;
735            if s >= ray.hypot() {
736                return None;
737            }
738        }
739        Some((p - u * s, p + u * (outside / cos)))
740    };
741    let tolerance = std_dev / 100.0;
742    let tile = TILE as f64 * step;
743    // Whether the wedge from `t0` to `t1` is short enough.
744    let fine = |t0: f64, t1: f64| -> bool {
745        let tm = (t0 + t1) / 2.0;
746        let (p0, p1) = (at(t0), at(t1));
747        let chord = (p1 - p0).hypot();
748        let tightest = rho(t0).min(rho(t1)).min(rho(tm));
749        // The outer edge, a chord, may come no nearer than a sample.
750        if chord * chord / (8.0 * (tightest + outside)) > step {
751            return false;
752        }
753        if tightest >= ROUND * std_dev {
754            let center = at(tm) - normal(tm) * rho(tm);
755            let r = rho(tm);
756            [t0, (t0 + tm) / 2.0, (tm + t1) / 2.0, t1]
757                .iter()
758                .all(|&t| ((at(t) - center).hypot() - r).abs() <= tolerance)
759        } else {
760            chord <= tile
761        }
762    };
763    // Cut at every axis, so the curvature only grows or shrinks in a wedge,
764    // then halve each wedge until it is fine.
765    let mut ts = Vec::new();
766    let quarter = std::f64::consts::FRAC_PI_2;
767    for q in 0..4 {
768        let mut todo = vec![(q as f64 * quarter, (q + 1) as f64 * quarter, 0)];
769        let mut done = Vec::new();
770        while let Some((t0, t1, depth)) = todo.pop() {
771            if depth >= 24 || fine(t0, t1) {
772                done.push(t0);
773            } else {
774                let tm = (t0 + t1) / 2.0;
775                todo.push((tm, t1, depth + 1));
776                todo.push((t0, tm, depth + 1));
777            }
778        }
779        done.sort_by(f64::total_cmp);
780        ts.extend(done);
781    }
782    let mut cuts = Vec::with_capacity(ts.len());
783    for &t in &ts {
784        match cut(t) {
785            Some(c) => cuts.push(c),
786            None => return tiles(&outline, grid, 0, 0, nx, ny),
787        }
788    }
789    let mut pieces = Vec::new();
790    let mut solid = BezPath::new();
791    for (n, (inner, _)) in cuts.iter().enumerate() {
792        if n == 0 {
793            solid.move_to(*inner);
794        } else {
795            solid.line_to(*inner);
796        }
797    }
798    solid.close_path();
799    pieces.push(Piece::Solid(solid));
800    let tau = std::f64::consts::TAU;
801    for n in 0..ts.len() {
802        let (t0, t1) = (ts[n], if n + 1 < ts.len() { ts[n + 1] } else { tau });
803        let ((i0, o0), (i1, o1)) = (cuts[n], cuts[(n + 1) % cuts.len()]);
804        let mut wedge = BezPath::new();
805        wedge.move_to(i0);
806        wedge.line_to(i1);
807        wedge.line_to(o1);
808        wedge.line_to(o0);
809        wedge.close_path();
810        let tm = (t0 + t1) / 2.0;
811        if rho(t0).min(rho(t1)).min(rho(tm)) >= ROUND * std_dev {
812            pieces.push(Piece::Disc(wedge, at(tm) - normal(tm) * rho(tm), rho(tm)));
813        } else {
814            let bounds = wedge.bounding_box();
815            let i = |x: f64| ((x - grid.origin.x) / step).floor() as isize;
816            let j = |y: f64| ((y - grid.origin.y) / step).floor() as isize;
817            pieces.push(Piece::Patch(
818                Some(wedge),
819                i(bounds.x0),
820                j(bounds.y0),
821                i(bounds.x1) + 1,
822                j(bounds.y1) + 1,
823            ));
824        }
825    }
826    pieces
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832    use crate::render::{Frame, compare, paint_list};
833
834    /// The most a shadow in pieces may differ from the same shadow as one
835    /// image, in any channel of 255, by how wide its blur is on the screen:
836    /// σ at the zoom, in device pixels. Measured over 3,000 random outlines
837    /// at 0.25×, 0.5×, 0.73×, 1×, 1.37× and 2× — what `random` makes, with
838    /// any seed. Where σ is a pixel or more, only the last bit of each
839    /// piece's 8-bit rounding is left; a narrower blur's edge is steeper than
840    /// a pixel, and vello_cpu's 8-bit gradient and layers round a disc
841    /// corner's a few hundredths of a pixel away from the sampled one.
842    fn bound(std_dev: f64, scale: f32) -> u8 {
843        match std_dev * scale as f64 {
844            s if s >= 1.0 => 4,
845            s if s >= 0.5 => 6,
846            s if s >= 0.25 => 11,
847            _ => 18,
848        }
849    }
850
851    /// Paints `outline`'s shadow at each scale as one image and in pieces,
852    /// and holds the two to the engine's tolerance — no seam — and to
853    /// [`bound`].
854    fn pieces_match_one_image(outline: Outline, std_dev: f64, case: &str, scales: &[f32]) {
855        let color = Rgba {
856            r: 20,
857            g: 30,
858            b: 40,
859            a: 200,
860        };
861        let paint = |single: usize, scale: f32| {
862            let mut list = Vec::new();
863            shadow_with(outline, std_dev, color, &mut list, &mut Vec::new(), single);
864            let area = Frame {
865                x: 0.0,
866                y: 0.0,
867                width: 360.0,
868                height: 220.0,
869            };
870            let p = paint_list(area, &list, scale);
871            (
872                p.data_as_u8_slice().to_vec(),
873                p.width() as usize,
874                list.len(),
875            )
876        };
877        for &scale in scales {
878            let (one, width, single_list) = paint(usize::MAX, scale);
879            let (pieces, _, pieced_list) = paint(0, scale);
880            assert_eq!(single_list, 1, "{case}");
881            assert!(pieced_list > 3, "{case} isn't in pieces");
882            let d = compare(&one, &pieces, width).unwrap();
883            assert!(
884                d.within_tolerance() && d.max <= bound(std_dev, scale),
885                "{case}, σ {std_dev}, at {scale}: {d:?}"
886            );
887        }
888    }
889
890    /// A shadow drawn in pieces is the shadow drawn as one image — no seam
891    /// where pieces meet, no step at a corner — at 1× and at scales that put
892    /// every edge between pieces part way across a pixel, for square
893    /// corners, sampled corners, disc corners, a pill, a thin box, a thin
894    /// ellipse, a circle, and ellipses in wedges with and without sampled
895    /// tips; the box 1.48 pixels tall blurred by less than 2 that once
896    /// differed by 7 at 1×; and random outlines.
897    #[test]
898    fn a_shadow_in_pieces_is_the_shadow_in_one() {
899        let r = |x0, y0, x1, y1| Rect::new(x0, y0, x1, y1);
900        let cases = [
901            (Outline::rounded(r(30.3, 20.7, 250.1, 170.9), [0.0; 4]), 4.0),
902            (
903                Outline::rounded(r(30.3, 20.7, 250.1, 170.9), [3.0, 80.0, 12.0, 45.0]),
904                4.0,
905            ),
906            (
907                Outline::rounded(r(30.0, 40.0, 330.0, 120.0), [40.0; 4]),
908                2.0,
909            ),
910            (Outline::rounded(r(30.0, 40.0, 330.0, 43.0), [1.0; 4]), 6.0),
911            (Outline::ellipse(r(25.5, 30.25, 300.5, 140.75)), 3.0),
912            (Outline::ellipse(r(25.0, 30.0, 185.0, 190.0)), 2.0),
913            (Outline::ellipse(r(15.3, 30.6, 345.2, 190.1)), 2.0),
914            (Outline::ellipse(r(20.0, 60.0, 340.0, 150.0)), 1.0),
915            (
916                Outline::rounded(
917                    r(50.0986, 43.2465, 329.1681, 44.7226),
918                    [0.738, 0.738, 0.738, 0.0],
919                ),
920                0.869,
921            ),
922            (
923                Outline::rounded(r(63.287, 18.532, 289.892, 187.314), [80.0; 4]),
924                0.584,
925            ),
926        ];
927        for (n, (outline, std_dev)) in cases.into_iter().enumerate() {
928            pieces_match_one_image(outline, std_dev, &format!("case {n}"), &[1.0, 0.73, 1.37]);
929        }
930        let mut seed = 0x2545_f491_4f6c_dd1du64;
931        for n in 0..12 {
932            let (outline, std_dev) = random(&mut seed);
933            pieces_match_one_image(outline, std_dev, &format!("random {n}"), &[0.5, 1.0]);
934        }
935    }
936
937    /// A random outline that fits the test's 360 × 220 area with its blur,
938    /// and one image of its shadow's samples, and its σ, from `seed`: a box with square, sampled, nearly-disc,
939    /// pill-round or random corners — all alike or each its own — or an
940    /// ellipse, blurred by σ from half a pixel to six.
941    fn random(seed: &mut u64) -> (Outline, f64) {
942        let mut next = || {
943            *seed = seed
944                .wrapping_mul(6364136223846793005)
945                .wrapping_add(1442695040888963407);
946            (*seed >> 11) as f64 / (1u64 << 53) as f64
947        };
948        let (aw, ah) = (360.0, 220.0);
949        let sd = 0.5 + next() * 5.5;
950        let reach = 3.0 * sd;
951        // No wider than one image of samples can show, with its blur.
952        let most = SINGLE_SIDE as f64 * sd / 2.0 - 2.0 * reach - sd - 1.0;
953        let w = 1.0 + next() * (aw - 2.0 * reach - 20.0).min(most);
954        let h = 1.0 + next() * (ah - 2.0 * reach - 20.0).min(most);
955        let x0 = reach + 5.0 + next() * (aw - 2.0 * reach - 10.0 - w);
956        let y0 = reach + 5.0 + next() * (ah - 2.0 * reach - 10.0 - h);
957        let rect = Rect::new(x0, y0, x0 + w, y0 + h);
958        let half = w.min(h) / 2.0;
959        let kind = (next() * 6.0) as usize;
960        if kind == 5 {
961            return (Outline::ellipse(rect), sd);
962        }
963        let mut corners = [(0, 0.0); 4];
964        for c in &mut corners {
965            *c = ((next() * 5.0) as usize, next());
966        }
967        let alike = next() < 0.5;
968        let radius = |(kind, u): (usize, f64)| match kind {
969            0 => 0.0,
970            1 => u * ROUND * sd,
971            2 => ROUND * sd * (0.98 + 0.04 * u),
972            3 => half,
973            4 => half - u * 3.0 * sd,
974            _ => u * half,
975        };
976        let radii = if alike {
977            [radius(corners[0]); 4]
978        } else {
979            corners.map(radius)
980        };
981        (Outline::rounded(rect, radii), sd)
982    }
983}