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 kurbo::{Affine, BezPath, Ellipse as KEllipse, Point, Rect, RoundedRect, Shape};
25use peniko::{Blob, Color as PColor, Gradient, ImageAlphaType, ImageData, ImageFormat};
26
27use crate::display::{Blend, Builder};
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        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/// How much of each of the `w` × `h` unit squares the closed polygon `path`
186/// — straight lines only — covers under `to`, row by row, as an 8-bit
187/// rasterizer gives it: the exact area, to the nearest 255th. Signed area is
188/// accumulated cell by cell along each edge and summed along each row, so a
189/// sample costs the same however the shape crosses it. What lies outside the
190/// squares counts as a rasterizer clips it: left of them, wholly in the first
191/// column; right of them, not at all.
192fn coverage(path: &BezPath, to: Affine, w: usize, h: usize) -> Vec<f32> {
193    // Each row has two cells past its end, which take what lies right of it.
194    let stride = w + 2;
195    let mut acc = vec![0f64; stride * h + 2];
196    let mut cell = |p0: Point, p1: Point| {
197        if (p0.y - p1.y).abs() <= 1e-12 {
198            return;
199        }
200        let (dir, p0, p1) = if p0.y < p1.y {
201            (1.0, p0, p1)
202        } else {
203            (-1.0, p1, p0)
204        };
205        let dxdy = (p1.x - p0.x) / (p1.y - p0.y);
206        let mut x = p0.x;
207        if p0.y < 0.0 {
208            x -= p0.y * dxdy;
209        }
210        let first = p0.y.max(0.0) as usize;
211        for y in first..h.min(p1.y.ceil() as usize) {
212            let start = (y * stride) as isize;
213            let dy = ((y + 1) as f64).min(p1.y) - (y as f64).max(p0.y);
214            let xnext = x + dxdy * dy;
215            let d = dy * dir;
216            let (x0, x1) = if x < xnext { (x, xnext) } else { (xnext, x) };
217            let x0floor = x0.floor();
218            let x0i = x0floor as isize;
219            let x1ceil = x1.ceil();
220            let x1i = x1ceil as isize;
221            let at = start + x0i;
222            if at < 0 {
223                x = xnext;
224                continue;
225            }
226            let at = at as usize;
227            if x1i <= x0i + 1 {
228                let xmf = 0.5 * (x + xnext) - x0floor;
229                acc[at] += d - d * xmf;
230                acc[at + 1] += d * xmf;
231            } else {
232                let s = (x1 - x0).recip();
233                let x0f = x0 - x0floor;
234                let a0 = 0.5 * s * (1.0 - x0f) * (1.0 - x0f);
235                let x1f = x1 - x1ceil + 1.0;
236                let am = 0.5 * s * x1f * x1f;
237                acc[at] += d * a0;
238                if x1i == x0i + 2 {
239                    acc[at + 1] += d * (1.0 - a0 - am);
240                } else {
241                    let a1 = s * (1.5 - x0f);
242                    acc[at + 1] += d * (a1 - a0);
243                    for xi in x0i + 2..x1i - 1 {
244                        acc[(start + xi) as usize] += d * s;
245                    }
246                    let a2 = a1 + (x1i - x0i - 3) as f64 * s;
247                    acc[(start + x1i - 1) as usize] += d * (1.0 - a2 - am);
248                }
249                acc[(start + x1i) as usize] += d * am;
250            }
251            x = xnext;
252        }
253    };
254    // An edge split where it crosses either side, each part moved inside.
255    let mut line = |p0: Point, p1: Point| {
256        let mut cuts = vec![0.0, 1.0];
257        for edge in [0.0, w as f64] {
258            if (p0.x - edge) * (p1.x - edge) < 0.0 {
259                cuts.push((edge - p0.x) / (p1.x - p0.x));
260            }
261        }
262        cuts.sort_by(f64::total_cmp);
263        let at = |t: f64| {
264            let p = p0.lerp(p1, t);
265            Point::new(p.x.clamp(0.0, w as f64), p.y)
266        };
267        for pair in cuts.windows(2) {
268            cell(at(pair[0]), at(pair[1]));
269        }
270    };
271    let (mut first, mut last) = (None, None);
272    for el in path.elements() {
273        match *el {
274            kurbo::PathEl::MoveTo(p) => {
275                if let (Some(a), Some(b)) = (last, first) {
276                    line(a, b);
277                }
278                let p = to * p;
279                first = Some(p);
280                last = Some(p);
281            }
282            kurbo::PathEl::LineTo(p) => {
283                let p = to * p;
284                if let Some(a) = last {
285                    line(a, p);
286                }
287                last = Some(p);
288            }
289            kurbo::PathEl::ClosePath => {
290                if let (Some(a), Some(b)) = (last, first) {
291                    line(a, b);
292                }
293                last = first;
294            }
295            _ => unreachable!("an outline flattened to lines"),
296        }
297    }
298    if let (Some(a), Some(b)) = (last, first) {
299        line(a, b);
300    }
301    let mut out = Vec::with_capacity(w * h);
302    for row in acc.chunks(stride).take(h) {
303        let mut sum = 0.0;
304        out.extend(row[..w].iter().map(|a| {
305            sum += a;
306            ((sum.abs().min(1.0) * 255.0).round() / 255.0) as f32
307        }));
308    }
309    out
310}
311
312/// The blurred shape at samples `i0..i0 + w` × `j0..j0 + h`, row by row: its
313/// coverage of each sample out to the blur's reach, blurred down each column
314/// and then across each row.
315fn blurred(path: &BezPath, grid: Grid, i0: isize, j0: isize, w: usize, h: usize) -> Vec<f32> {
316    let (cw, ch) = (w + 2 * REACH, h + 2 * REACH);
317    let to = Affine::scale(1.0 / grid.step)
318        * Affine::translate((-grid.x(i0 - REACH as isize), -grid.y(j0 - REACH as isize)));
319    let cov = coverage(path, to, cw, ch);
320    let k = kernel();
321    let mut down = vec![0f32; h * cw];
322    for y in 0..h {
323        let row = &mut down[y * cw..][..cw];
324        for (t, weight) in k.iter().enumerate() {
325            let from = &cov[(y + t) * cw..][..cw];
326            for (o, c) in row.iter_mut().zip(from) {
327                *o += weight * c;
328            }
329        }
330    }
331    let mut out = vec![0f32; w * h];
332    for y in 0..h {
333        let from = &down[y * cw..][..cw];
334        for (x, o) in out[y * w..][..w].iter_mut().enumerate() {
335            *o = k.iter().zip(&from[x..]).map(|(a, b)| a * b).sum();
336        }
337    }
338    out
339}
340
341/// The blur of the interval `lo..hi` at samples `from..from + len` of a line
342/// whose sample `0` starts at `origin`: a straight edge's profile.
343fn profile(lo: f64, hi: f64, origin: f64, step: f64, from: isize, len: usize) -> Vec<f32> {
344    let cov: Vec<f32> = (0..len + 2 * REACH)
345        .map(|n| {
346            let a = origin + (from + n as isize - REACH as isize) as f64 * step;
347            ((hi.min(a + step) - lo.max(a)) / step).clamp(0.0, 1.0) as f32
348        })
349        .collect();
350    let k = kernel();
351    (0..len)
352        .map(|i| k.iter().zip(&cov[i..]).map(|(a, b)| a * b).sum())
353        .collect()
354}
355
356/// `values`, `w` × `h`, as an image of `c` whose alpha is each value times
357/// `alpha`.
358fn image(values: &[f32], w: usize, h: usize, c: Rgba, alpha: f32) -> ImageData {
359    let pixels: Vec<u8> = values
360        .iter()
361        .flat_map(|v| [c.r, c.g, c.b, (v.clamp(0.0, 1.0) * alpha).round() as u8])
362        .collect();
363    ImageData {
364        data: Blob::new(Arc::new(pixels)),
365        format: ImageFormat::Rgba8,
366        alpha_type: ImageAlphaType::Alpha,
367        width: w as u32,
368        height: h as u32,
369    }
370}
371
372/// The normal distribution's cumulative function, to 1.5 × 10⁻⁷
373/// (Abramowitz and Stegun, 7.1.26).
374fn phi_cdf(x: f64) -> f64 {
375    let z = x.abs() / std::f64::consts::SQRT_2;
376    let t = 1.0 / (1.0 + 0.3275911 * z);
377    let poly = t
378        * (0.254829592
379            + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
380    let erf = 1.0 - poly * (-z * z).exp();
381    0.5 * (1.0 + if x < 0.0 { -erf } else { erf })
382}
383
384/// One piece of a large shadow: the area it covers and how it is painted.
385enum Piece {
386    /// Every sample inside the blur's reach of the shape: solid.
387    Solid(BezPath),
388    /// Where the blur varies only down: the profile of rows `j0..j1`.
389    Down(BezPath, isize, isize),
390    /// Where it varies only across: the profile of columns `i0..i1`.
391    Across(BezPath, isize, isize),
392    /// A round corner: the blurred edge of the disc centered at the point,
393    /// of the radius.
394    Disc(BezPath, Point, f64),
395    /// Samples `i0..i1` × `j0..j1`, blurred in two dimensions, cut to the
396    /// path, or else to those samples.
397    Patch(Option<BezPath>, isize, isize, isize, isize),
398}
399
400/// Adds `outline`'s shadow — blurred by a Gaussian of `std_dev` pixels and
401/// colored `c` — to `list`, and each image it paints, in order, to `images`.
402pub fn shadow(
403    outline: Outline,
404    std_dev: f64,
405    c: Rgba,
406    list: &mut Builder,
407    images: &mut Vec<ImageData>,
408) {
409    shadow_with(outline, std_dev, c, list, images, SINGLE)
410}
411
412/// [`shadow`], drawing one image when it takes no more than `single` samples.
413pub fn shadow_with(
414    outline: Outline,
415    std_dev: f64,
416    c: Rgba,
417    list: &mut Builder,
418    images: &mut Vec<ImageData>,
419    single: usize,
420) {
421    let rect = outline.rect();
422    if rect.width() <= 0.0 || rect.height() <= 0.0 {
423        return;
424    }
425    let color = PColor::from_rgba8(c.r, c.g, c.b, c.a);
426    if std_dev <= 0.0 {
427        list.draw(Draw::Fill(outline.path(), Paint::Solid(color)));
428        return;
429    }
430    let step = std_dev / 2.0;
431    let reach = REACH as f64 * step;
432    let grid = Grid {
433        origin: Point::new(rect.x0 - reach, rect.y0 - reach),
434        step,
435    };
436    let nx = ((rect.width() + 2.0 * reach) / step).ceil() as usize;
437    let ny = ((rect.height() + 2.0 * reach) / step).ceil() as usize;
438    // A hundredth of a sample.
439    let path = outline.lines(step / 100.0);
440    if nx * ny <= single && nx <= SINGLE_SIDE && ny <= SINGLE_SIDE {
441        let values = blurred(&path, grid, 0, 0, nx, ny);
442        let data = image(&values, nx, ny, c, c.a as f32);
443        images.push(data.clone());
444        list.draw(Draw::Fill(
445            grid.rect(0, 0, nx as isize, ny as isize).to_path(0.1),
446            Paint::Image(data, grid.place(0, 0)),
447        ));
448        return;
449    }
450    let (nx, ny) = (nx as isize, ny as isize);
451    let pieces = match outline {
452        Outline::Rounded(r, radii) => rounded(r, radii, std_dev, grid, nx, ny),
453        Outline::Ellipse(e) => ellipse(e, std_dev, grid, nx, ny),
454    };
455    let extent = grid.rect(0, 0, nx, ny);
456    list.push_layer(
457        Blend::Opacity(c.a as f32 / 255.0),
458        Some(extent.to_path(0.1)),
459    );
460    let opaque = Rgba { a: 255, ..c };
461    let paint_over = |clip: &BezPath| clip.bounding_box().inflate(MARGIN, MARGIN).to_path(0.1);
462    for piece in pieces {
463        let (clip, paint) = match piece {
464            Piece::Solid(clip) => (clip, Paint::Solid(color.with_alpha(1.0))),
465            Piece::Down(clip, j0, j1) => {
466                let len = (j1 - j0 + 2) as usize;
467                let values = profile(rect.y0, rect.y1, grid.origin.y, step, j0 - 1, len);
468                let data = image(&values, 1, len, opaque, 255.0);
469                images.push(data.clone());
470                let place = Affine::translate((0.0, grid.y(j0 - 1))) * Affine::scale(step);
471                (clip, Paint::Image(data, place))
472            }
473            Piece::Across(clip, i0, i1) => {
474                let len = (i1 - i0 + 2) as usize;
475                let values = profile(rect.x0, rect.x1, grid.origin.x, step, i0 - 1, len);
476                let data = image(&values, len, 1, opaque, 255.0);
477                images.push(data.clone());
478                let place = Affine::translate((grid.x(i0 - 1), 0.0)) * Affine::scale(step);
479                (clip, Paint::Image(data, place))
480            }
481            Piece::Disc(clip, center, radius) => {
482                (clip, Paint::Gradient(disc(center, radius, std_dev, c)))
483            }
484            Piece::Patch(clip, i0, j0, i1, j1) => {
485                let (w, h) = ((i1 - i0 + 2) as usize, (j1 - j0 + 2) as usize);
486                let values = blurred(&path, grid, i0 - 1, j0 - 1, w, h);
487                let data = image(&values, w, h, opaque, 255.0);
488                images.push(data.clone());
489                (
490                    clip.unwrap_or_else(|| grid.rect(i0, j0, i1, j1).to_path(0.1)),
491                    Paint::Image(data, grid.place(i0 - 1, j0 - 1)),
492                )
493            }
494        };
495        // The piece's own edge cuts it, anti-aliased, so where two meet each
496        // adds its share of the pixel; the layer reaches well past it.
497        list.push_layer(Blend::Add, Some(paint_over(&clip)));
498        list.draw(Draw::Fill(clip, paint));
499        list.pop();
500    }
501    list.pop();
502}
503
504/// The blurred edge of a disc of `radius` around `center`, as a radial
505/// gradient from where it is solid to where it is gone. A Gaussian of σ
506/// across the edge of a disc of radius R is, to within 0.1 of 255 once R is
507/// [`ROUND`] σ, the straight edge's minus φ(d)·σ/2R — the disc's curve taking
508/// a little from it.
509fn disc(center: Point, radius: f64, std_dev: f64, c: Rgba) -> Gradient {
510    let reach = REACH as f64 * std_dev / 2.0;
511    let (r0, r1) = (radius - reach, radius + reach);
512    const STOPS: usize = 49;
513    let stops: Vec<(f32, PColor)> = (0..STOPS)
514        .map(|k| {
515            let t = k as f64 / (STOPS - 1) as f64;
516            let d = (r0 + t * (r1 - r0) - radius) / std_dev;
517            let density = (-d * d / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
518            let v = (phi_cdf(-d) - density * std_dev / (2.0 * radius)).clamp(0.0, 1.0);
519            (
520                t as f32,
521                PColor::new([
522                    c.r as f32 / 255.0,
523                    c.g as f32 / 255.0,
524                    c.b as f32 / 255.0,
525                    v as f32,
526                ]),
527            )
528        })
529        .collect();
530    Gradient::new_two_point_radial(center, r0 as f32, center, r1 as f32)
531        .with_stops(stops.as_slice())
532}
533
534/// The pieces of a rounded rectangle's shadow over samples `0..nx` ×
535/// `0..ny`. Away from a corner's reach the blur depends on one coordinate
536/// only: down where the column lies a blur's reach inside both sides, across
537/// where the row does, and is solid where both do.
538fn rounded(
539    rect: Rect,
540    radii: [f64; 4],
541    std_dev: f64,
542    grid: Grid,
543    nx: isize,
544    ny: isize,
545) -> Vec<Piece> {
546    let reach = REACH as f64 * grid.step;
547    let (sx, sy) = (
548        |x: f64| (x - grid.origin.x) / grid.step,
549        |y: f64| (y - grid.origin.y) / grid.step,
550    );
551    let clamp_x = |v: f64| (v as isize).clamp(0, nx);
552    let clamp_y = |v: f64| (v as isize).clamp(0, ny);
553    // Where the blur no longer reaches either side.
554    let (xl, xr) = (2 * REACH as isize, clamp_x(sx(rect.x1 - reach).floor()));
555    let (yt, yb) = (2 * REACH as isize, clamp_y(sy(rect.y1 - reach).floor()));
556    let [tl, tr, br, bl] = radii;
557    let (w, h) = (rect.width(), rect.height());
558    // The straight run between each pair of corners.
559    let (top, right, bottom, left) = (w - tl - tr, h - tr - br, w - br - bl, h - tl - bl);
560    // A corner is a disc's edge when it is round enough and each side
561    // leaving it runs straight for the blur's reach — or into a corner of
562    // the same circle.
563    let disc_ok = |r: f64, a: (f64, f64), b: (f64, f64)| {
564        let run = |(len, other): (f64, f64)| len >= reach || (len <= 1e-9 && other == r);
565        r >= ROUND * std_dev && run(a) && run(b)
566    };
567    struct Corner {
568        zone: [isize; 4],
569        disc: Option<Point>,
570        radius: f64,
571    }
572    let mut corners = Vec::new();
573    for (r, at_right, at_bottom, a, b) in [
574        (tl, false, false, (top, tr), (left, bl)),
575        (tr, true, false, (top, tl), (right, br)),
576        (br, true, true, (bottom, bl), (right, tr)),
577        (bl, false, true, (bottom, br), (left, tl)),
578    ] {
579        let disc = disc_ok(r, a, b);
580        // A disc's quadrant reaches its center; a sampled corner, the blur's
581        // reach past the corner's square.
582        let inner = if disc { r } else { r + reach };
583        let (x0, x1) = if at_right {
584            let x = sx(rect.x1 - inner);
585            (clamp_x(if disc { x.round() } else { x.floor() }), nx)
586        } else {
587            let x = sx(rect.x0 + inner);
588            (0, clamp_x(if disc { x.round() } else { x.ceil() }))
589        };
590        let (y0, y1) = if at_bottom {
591            let y = sy(rect.y1 - inner);
592            (clamp_y(if disc { y.round() } else { y.floor() }), ny)
593        } else {
594            let y = sy(rect.y0 + inner);
595            (0, clamp_y(if disc { y.round() } else { y.ceil() }))
596        };
597        let center = Point::new(
598            if at_right { rect.x1 - r } else { rect.x0 + r },
599            if at_bottom { rect.y1 - r } else { rect.y0 + r },
600        );
601        corners.push(Corner {
602            zone: [x0, y0, x1, y1],
603            disc: disc.then_some(center),
604            radius: r,
605        });
606    }
607    let mut xs = vec![0, nx, xl.min(nx), xr];
608    let mut ys = vec![0, ny, yt.min(ny), yb];
609    for c in &corners {
610        xs.extend([c.zone[0], c.zone[2]]);
611        ys.extend([c.zone[1], c.zone[3]]);
612    }
613    xs.sort_unstable();
614    xs.dedup();
615    ys.sort_unstable();
616    ys.dedup();
617    let mut solid = BezPath::new();
618    let mut down: BTreeMap<(isize, isize), BezPath> = BTreeMap::new();
619    let mut across: BTreeMap<(isize, isize), BezPath> = BTreeMap::new();
620    let mut discs: BTreeMap<usize, BezPath> = BTreeMap::new();
621    let mut patches = Vec::new();
622    // A sampled corner whose zone meets no other's is one patch.
623    let meets =
624        |a: &[isize; 4], b: &[isize; 4]| a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3];
625    let whole: Vec<bool> = corners
626        .iter()
627        .enumerate()
628        .map(|(n, c)| {
629            c.disc.is_none()
630                && corners
631                    .iter()
632                    .enumerate()
633                    .all(|(m, o)| m == n || !meets(&c.zone, &o.zone))
634        })
635        .collect();
636    for (c, _) in corners.iter().zip(&whole).filter(|(_, w)| **w) {
637        let [i0, j0, i1, j1] = c.zone;
638        if i0 < i1 && j0 < j1 {
639            patches.push((i0, j0, i1, j1));
640        }
641    }
642    let add = |path: &mut BezPath, r: Rect| path.extend(r.path_elements(0.1));
643    for ys in ys.windows(2) {
644        let (j0, j1) = (ys[0], ys[1]);
645        for xs in xs.windows(2) {
646            let (i0, i1) = (xs[0], xs[1]);
647            let cell = grid.rect(i0, j0, i1, j1);
648            let within = |z: &[isize; 4]| z[0] <= i0 && i1 <= z[2] && z[1] <= j0 && j1 <= z[3];
649            if corners
650                .iter()
651                .zip(&whole)
652                .any(|(c, w)| *w && within(&c.zone))
653            {
654                // Already a patch of its own.
655            } else if corners.iter().any(|c| c.disc.is_none() && within(&c.zone)) {
656                patches.push((i0, j0, i1, j1));
657            } else if let Some(n) = corners
658                .iter()
659                .position(|c| c.disc.is_some() && within(&c.zone))
660            {
661                add(discs.entry(n).or_default(), cell);
662            } else {
663                let central_x = xl <= i0 && i1 <= xr;
664                let central_y = yt <= j0 && j1 <= yb;
665                match (central_x, central_y) {
666                    (true, true) => add(&mut solid, cell),
667                    (true, false) => add(down.entry((j0, j1)).or_default(), cell),
668                    (false, true) => add(across.entry((i0, i1)).or_default(), cell),
669                    // Never, as a corner's zone covers where neither holds;
670                    // if it did, sampling it is still exact.
671                    (false, false) => patches.push((i0, j0, i1, j1)),
672                }
673            }
674        }
675    }
676    let outline = Outline::Rounded(rect, radii);
677    let mut pieces = Vec::new();
678    if !solid.elements().is_empty() {
679        pieces.push(Piece::Solid(solid));
680    }
681    // Bands that meet are one piece, with one image: every join between two
682    // pieces inside a pixel costs a rounding in each, so a straight run has
683    // none.
684    pieces.extend(runs(down).map(|((j0, j1), p)| Piece::Down(p, j0, j1)));
685    pieces.extend(runs(across).map(|((i0, i1), p)| Piece::Across(p, i0, i1)));
686    pieces.extend(
687        discs
688            .into_iter()
689            .map(|(n, p)| Piece::Disc(p, corners[n].disc.expect("a disc"), corners[n].radius)),
690    );
691    for (i0, j0, i1, j1) in merged(patches) {
692        pieces.extend(tiles(&outline, grid, i0, j0, i1, j1));
693    }
694    pieces
695}
696
697/// Bands `(from, to)` of cells, merged where one ends as the next begins.
698fn runs(
699    bands: BTreeMap<(isize, isize), BezPath>,
700) -> impl Iterator<Item = ((isize, isize), BezPath)> {
701    let mut out: Vec<((isize, isize), BezPath)> = Vec::new();
702    for ((from, to), path) in bands {
703        match out.last_mut() {
704            Some(((_, end), run)) if *end == from => {
705                *end = to;
706                run.extend(path);
707            }
708            _ => out.push(((from, to), path)),
709        }
710    }
711    out.into_iter()
712}
713
714/// Cells `(i0, j0, i1, j1)` of a grid, none overlapping, merged into fewer
715/// rectangles: along each band first, then down where the runs line up.
716fn merged(mut cells: Vec<(isize, isize, isize, isize)>) -> Vec<(isize, isize, isize, isize)> {
717    cells.sort_unstable_by_key(|&(i0, j0, _, j1)| (j0, j1, i0));
718    let mut rows: Vec<(isize, isize, isize, isize)> = Vec::new();
719    for c in cells {
720        match rows.last_mut() {
721            Some(r) if (r.1, r.3, r.2) == (c.1, c.3, c.0) => r.2 = c.2,
722            _ => rows.push(c),
723        }
724    }
725    rows.sort_unstable_by_key(|&(i0, j0, i1, _)| (i0, i1, j0));
726    let mut out: Vec<(isize, isize, isize, isize)> = Vec::new();
727    for c in rows {
728        match out.last_mut() {
729            Some(r) if (r.0, r.2, r.3) == (c.0, c.2, c.1) => r.3 = c.3,
730            _ => out.push(c),
731        }
732    }
733    out
734}
735
736/// Samples `i0..i1` × `j0..j1` of `outline`'s shadow as tiles of at most
737/// [`TILE`] samples a side: none where the blur can't reach the shape, one
738/// solid piece where the shape fills all it reaches, and each other tile
739/// sampled.
740fn tiles(outline: &Outline, grid: Grid, i0: isize, j0: isize, i1: isize, j1: isize) -> Vec<Piece> {
741    let tile = TILE as isize;
742    if (i1 - i0) <= tile && (j1 - j0) <= tile {
743        return vec![Piece::Patch(None, i0, j0, i1, j1)];
744    }
745    // A tile's samples, its border and how far the blur reaches.
746    let reach = REACH as isize + 1;
747    let mut solid = BezPath::new();
748    let mut pieces = Vec::new();
749    let mut j = j0;
750    while j < j1 {
751        let je = (j + tile).min(j1);
752        let mut run: Option<isize> = None;
753        let mut i = i0;
754        while i < i1 {
755            let ie = (i + tile).min(i1);
756            let reached = grid.rect(i - reach, j - reach, ie + reach, je + reach);
757            let full = outline.holds(reached);
758            if full {
759                run.get_or_insert(i);
760            } else {
761                if let Some(start) = run.take() {
762                    solid.extend(grid.rect(start, j, i, je).path_elements(0.1));
763                }
764                if !outline.misses(reached) {
765                    pieces.push(Piece::Patch(None, i, j, ie, je));
766                }
767            }
768            i = ie;
769        }
770        if let Some(start) = run {
771            solid.extend(grid.rect(start, j, i1, je).path_elements(0.1));
772        }
773        j = je;
774    }
775    if !solid.elements().is_empty() {
776        pieces.insert(0, Piece::Solid(solid));
777    }
778    pieces
779}
780
781/// The signed distance from `(x, y)` to the ellipse of semi-axes `a` and `b`
782/// around the origin: from its nearest point, found by Newton's method on the
783/// angle where the offset to it is normal to the curve.
784fn ellipse_distance(a: f64, b: f64, x: f64, y: f64) -> f64 {
785    // By symmetry, in the first quadrant, where the nearest point is too.
786    let (px, py) = (x.abs(), y.abs());
787    let mut t = (a * py).atan2(b * px);
788    for _ in 0..40 {
789        let (sin, cos) = t.sin_cos();
790        let (ex, ey) = (a * cos, b * sin);
791        // f(t) = (p − e(t)) · e'(t), and its derivative.
792        let f = (px - ex) * -a * sin + (py - ey) * b * cos;
793        let df =
794            (a * a * sin * sin + b * b * cos * cos) - (px - ex) * a * cos - (py - ey) * b * sin;
795        if df.abs() < 1e-12 {
796            break;
797        }
798        let next = (t - f / df).clamp(0.0, std::f64::consts::FRAC_PI_2);
799        if (next - t).abs() < 1e-12 {
800            t = next;
801            break;
802        }
803        t = next;
804    }
805    let (sin, cos) = t.sin_cos();
806    let d = (px - a * cos).hypot(py - b * sin);
807    if (px / a).hypot(py / b) <= 1.0 { -d } else { d }
808}
809
810/// The pieces of an ellipse's shadow. Cut by rays from its center into
811/// wedges, each short enough that the ellipse stays within a hundredth of a
812/// standard deviation of its circle of curvature there, a wedge is that
813/// circle's blurred edge — or, where the ellipse curves too tightly for that,
814/// sampled; inside the wedges it is solid. An ellipse too thin to be solid
815/// anywhere is tiled instead.
816fn ellipse(e: Rect, std_dev: f64, grid: Grid, nx: isize, ny: isize) -> Vec<Piece> {
817    let outline = Outline::Ellipse(e);
818    let step = grid.step;
819    let (a, b) = (e.width() / 2.0, e.height() / 2.0);
820    let c = e.center();
821    // How far inside every sample a blur reaches is covered, and how far
822    // outside none is.
823    let inside = (REACH as f64 * step + step) * std::f64::consts::SQRT_2;
824    let outside = inside + step;
825    if a.min(b) <= 4.0 * inside {
826        return tiles(&outline, grid, 0, 0, nx, ny);
827    }
828    let at = |t: f64| Point::new(c.x + a * t.cos(), c.y + b * t.sin());
829    let normal = |t: f64| {
830        let n = kurbo::Vec2::new(b * t.cos(), a * t.sin());
831        n / n.hypot()
832    };
833    let rho = |t: f64| {
834        let (sin, cos) = t.sin_cos();
835        (a * a * sin * sin + b * b * cos * cos).powf(1.5) / (a * b)
836    };
837    let distance = |p: Point| ellipse_distance(a, b, p.x - c.x, p.y - c.y);
838    // Where the ray through the point at `t` crosses `depth` inside and
839    // `outside` outside, or `None` when no point on it lies that deep.
840    let cut = |t: f64| -> Option<(Point, Point)> {
841        let p = at(t);
842        let ray = p - c;
843        let u = ray / ray.hypot();
844        let cos = u.dot(normal(t)).max(1e-6);
845        let mut s = inside / cos;
846        while distance(p - u * s) > -inside {
847            s += step;
848            if s >= ray.hypot() {
849                return None;
850            }
851        }
852        Some((p - u * s, p + u * (outside / cos)))
853    };
854    let tolerance = std_dev / 100.0;
855    let tile = TILE as f64 * step;
856    // Whether the wedge from `t0` to `t1` is short enough.
857    let fine = |t0: f64, t1: f64| -> bool {
858        let tm = (t0 + t1) / 2.0;
859        let (p0, p1) = (at(t0), at(t1));
860        let chord = (p1 - p0).hypot();
861        let tightest = rho(t0).min(rho(t1)).min(rho(tm));
862        // The outer edge, a chord, may come no nearer than a sample.
863        if chord * chord / (8.0 * (tightest + outside)) > step {
864            return false;
865        }
866        if tightest >= ROUND * std_dev {
867            let center = at(tm) - normal(tm) * rho(tm);
868            let r = rho(tm);
869            [t0, (t0 + tm) / 2.0, (tm + t1) / 2.0, t1]
870                .iter()
871                .all(|&t| ((at(t) - center).hypot() - r).abs() <= tolerance)
872        } else {
873            chord <= tile
874        }
875    };
876    // Cut at every axis, so the curvature only grows or shrinks in a wedge,
877    // then halve each wedge until it is fine.
878    let mut ts = Vec::new();
879    let quarter = std::f64::consts::FRAC_PI_2;
880    for q in 0..4 {
881        let mut todo = vec![(q as f64 * quarter, (q + 1) as f64 * quarter, 0)];
882        let mut done = Vec::new();
883        while let Some((t0, t1, depth)) = todo.pop() {
884            if depth >= 24 || fine(t0, t1) {
885                done.push(t0);
886            } else {
887                let tm = (t0 + t1) / 2.0;
888                todo.push((tm, t1, depth + 1));
889                todo.push((t0, tm, depth + 1));
890            }
891        }
892        done.sort_by(f64::total_cmp);
893        ts.extend(done);
894    }
895    let mut cuts = Vec::with_capacity(ts.len());
896    for &t in &ts {
897        match cut(t) {
898            Some(c) => cuts.push(c),
899            None => return tiles(&outline, grid, 0, 0, nx, ny),
900        }
901    }
902    let mut pieces = Vec::new();
903    let mut solid = BezPath::new();
904    for (n, (inner, _)) in cuts.iter().enumerate() {
905        if n == 0 {
906            solid.move_to(*inner);
907        } else {
908            solid.line_to(*inner);
909        }
910    }
911    solid.close_path();
912    pieces.push(Piece::Solid(solid));
913    let tau = std::f64::consts::TAU;
914    for n in 0..ts.len() {
915        let (t0, t1) = (ts[n], if n + 1 < ts.len() { ts[n + 1] } else { tau });
916        let ((i0, o0), (i1, o1)) = (cuts[n], cuts[(n + 1) % cuts.len()]);
917        let mut wedge = BezPath::new();
918        wedge.move_to(i0);
919        wedge.line_to(i1);
920        wedge.line_to(o1);
921        wedge.line_to(o0);
922        wedge.close_path();
923        let tm = (t0 + t1) / 2.0;
924        if rho(t0).min(rho(t1)).min(rho(tm)) >= ROUND * std_dev {
925            pieces.push(Piece::Disc(wedge, at(tm) - normal(tm) * rho(tm), rho(tm)));
926        } else {
927            let bounds = wedge.bounding_box();
928            let i = |x: f64| ((x - grid.origin.x) / step).floor() as isize;
929            let j = |y: f64| ((y - grid.origin.y) / step).floor() as isize;
930            pieces.push(Piece::Patch(
931                Some(wedge),
932                i(bounds.x0),
933                j(bounds.y0),
934                i(bounds.x1) + 1,
935                j(bounds.y1) + 1,
936            ));
937        }
938    }
939    pieces
940}
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945    use crate::compare::compare;
946    use crate::cpu::paint_area;
947    use crate::display::List;
948    use crate::render::Frame;
949
950    /// The most a shadow in pieces may differ from the same shadow as one
951    /// image, in any channel of 255, by how wide its blur is on the screen:
952    /// σ at the zoom, in device pixels. Measured over 3,000 random outlines
953    /// at 0.25×, 0.5×, 0.73×, 1×, 1.37× and 2× — what `random` makes, with
954    /// any seed. Where σ is a pixel or more, only the last bit of each
955    /// piece's 8-bit rounding is left; a narrower blur's edge is steeper than
956    /// a pixel, and vello_cpu's 8-bit gradient and layers round a disc
957    /// corner's a few hundredths of a pixel away from the sampled one.
958    fn bound(std_dev: f64, scale: f32) -> u8 {
959        match std_dev * scale as f64 {
960            s if s >= 1.0 => 4,
961            s if s >= 0.5 => 6,
962            s if s >= 0.25 => 11,
963            _ => 18,
964        }
965    }
966
967    /// Paints `outline`'s shadow at each scale as one image and in pieces,
968    /// and holds the two to the engine's tolerance — no seam — and to
969    /// [`bound`].
970    fn pieces_match_one_image(outline: Outline, std_dev: f64, case: &str, scales: &[f32]) {
971        let color = Rgba {
972            r: 20,
973            g: 30,
974            b: 40,
975            a: 200,
976        };
977        let paint = |single: usize, scale: f32| {
978            let area = Frame {
979                x: 0.0,
980                y: 0.0,
981                width: 360.0,
982                height: 220.0,
983            };
984            let list = List::of(area, |b| {
985                shadow_with(outline, std_dev, color, b, &mut Vec::new(), single)
986            });
987            let p = paint_area(&list, Rect::new(0.0, 0.0, 360.0, 220.0), scale as f64);
988            // How many commands, and how many layers they are drawn in.
989            let chunk = list.chunk(&Vec::new()).unwrap();
990            let shape = (chunk.commands.len(), chunk.made.effects.len());
991            (p.data_as_u8_slice().to_vec(), p.width() as usize, shape)
992        };
993        for &scale in scales {
994            let (one, width, single_list) = paint(usize::MAX, scale);
995            let (pieces, _, pieced_list) = paint(0, scale);
996            assert_eq!(single_list, (1, 0), "{case}");
997            assert!(pieced_list.1 > 1, "{case} isn't in pieces");
998            let d = compare(&one, &pieces, width).unwrap();
999            assert!(
1000                d.within_tolerance() && d.max <= bound(std_dev, scale),
1001                "{case}, σ {std_dev}, at {scale}: {d:?}"
1002            );
1003        }
1004    }
1005
1006    /// A shadow drawn in pieces is the shadow drawn as one image — no seam
1007    /// where pieces meet, no step at a corner — at 1× and at scales that put
1008    /// every edge between pieces part way across a pixel, for square
1009    /// corners, sampled corners, disc corners, a pill, a thin box, a thin
1010    /// ellipse, a circle, and ellipses in wedges with and without sampled
1011    /// tips; the box 1.48 pixels tall blurred by less than 2 that once
1012    /// differed by 7 at 1×; and random outlines.
1013    #[test]
1014    fn a_shadow_in_pieces_is_the_shadow_in_one() {
1015        let r = |x0, y0, x1, y1| Rect::new(x0, y0, x1, y1);
1016        let cases = [
1017            (Outline::rounded(r(30.3, 20.7, 250.1, 170.9), [0.0; 4]), 4.0),
1018            (
1019                Outline::rounded(r(30.3, 20.7, 250.1, 170.9), [3.0, 80.0, 12.0, 45.0]),
1020                4.0,
1021            ),
1022            (
1023                Outline::rounded(r(30.0, 40.0, 330.0, 120.0), [40.0; 4]),
1024                2.0,
1025            ),
1026            (Outline::rounded(r(30.0, 40.0, 330.0, 43.0), [1.0; 4]), 6.0),
1027            (Outline::ellipse(r(25.5, 30.25, 300.5, 140.75)), 3.0),
1028            (Outline::ellipse(r(25.0, 30.0, 185.0, 190.0)), 2.0),
1029            (Outline::ellipse(r(15.3, 30.6, 345.2, 190.1)), 2.0),
1030            (Outline::ellipse(r(20.0, 60.0, 340.0, 150.0)), 1.0),
1031            (
1032                Outline::rounded(
1033                    r(50.0986, 43.2465, 329.1681, 44.7226),
1034                    [0.738, 0.738, 0.738, 0.0],
1035                ),
1036                0.869,
1037            ),
1038            (
1039                Outline::rounded(r(63.287, 18.532, 289.892, 187.314), [80.0; 4]),
1040                0.584,
1041            ),
1042        ];
1043        for (n, (outline, std_dev)) in cases.into_iter().enumerate() {
1044            pieces_match_one_image(outline, std_dev, &format!("case {n}"), &[1.0, 0.73, 1.37]);
1045        }
1046        let mut seed = 0x2545_f491_4f6c_dd1du64;
1047        for n in 0..12 {
1048            let (outline, std_dev) = random(&mut seed);
1049            pieces_match_one_image(outline, std_dev, &format!("random {n}"), &[0.5, 1.0]);
1050        }
1051    }
1052
1053    /// A random outline that fits the test's 360 × 220 area with its blur,
1054    /// and one image of its shadow's samples, and its σ, from `seed`: a box with square, sampled, nearly-disc,
1055    /// pill-round or random corners — all alike or each its own — or an
1056    /// ellipse, blurred by σ from half a pixel to six.
1057    fn random(seed: &mut u64) -> (Outline, f64) {
1058        let mut next = || {
1059            *seed = seed
1060                .wrapping_mul(6364136223846793005)
1061                .wrapping_add(1442695040888963407);
1062            (*seed >> 11) as f64 / (1u64 << 53) as f64
1063        };
1064        let (aw, ah) = (360.0, 220.0);
1065        let sd = 0.5 + next() * 5.5;
1066        let reach = 3.0 * sd;
1067        // No wider than one image of samples can show, with its blur.
1068        let most = SINGLE_SIDE as f64 * sd / 2.0 - 2.0 * reach - sd - 1.0;
1069        let w = 1.0 + next() * (aw - 2.0 * reach - 20.0).min(most);
1070        let h = 1.0 + next() * (ah - 2.0 * reach - 20.0).min(most);
1071        let x0 = reach + 5.0 + next() * (aw - 2.0 * reach - 10.0 - w);
1072        let y0 = reach + 5.0 + next() * (ah - 2.0 * reach - 10.0 - h);
1073        let rect = Rect::new(x0, y0, x0 + w, y0 + h);
1074        let half = w.min(h) / 2.0;
1075        let kind = (next() * 6.0) as usize;
1076        if kind == 5 {
1077            return (Outline::ellipse(rect), sd);
1078        }
1079        let mut corners = [(0, 0.0); 4];
1080        for c in &mut corners {
1081            *c = ((next() * 5.0) as usize, next());
1082        }
1083        let alike = next() < 0.5;
1084        let radius = |(kind, u): (usize, f64)| match kind {
1085            0 => 0.0,
1086            1 => u * ROUND * sd,
1087            2 => ROUND * sd * (0.98 + 0.04 * u),
1088            3 => half,
1089            4 => half - u * 3.0 * sd,
1090            _ => u * half,
1091        };
1092        let radii = if alike {
1093            [radius(corners[0]); 4]
1094        } else {
1095            corners.map(radius)
1096        };
1097        (Outline::rounded(rect, radii), sd)
1098    }
1099}