Skip to main content

denise_render/
arc.rs

1//! Anti-aliased circles and arcs.
2//!
3//! The same machinery as the rounded rectangles — per-scanline extents in fixed
4//! point, sampled at [`SUBSAMPLES`] sub-rows, integer square roots — with one
5//! addition: an angular cut. An arc is the ring between two radii, intersected
6//! with the sector between two rays, and on any one sub-row both of those are
7//! just intervals of x. No trigonometry runs per pixel; the two ray directions
8//! are looked up once per call.
9//!
10//! # Angles are binary turns
11//!
12//! A full revolution is [`TURN`] = 65536, angle 0 points at twelve o'clock, and
13//! positive sweeps go clockwise. This is the unit a panel actually computes in:
14//! a progress ring is `done * TURN / total` with no floating point and no π, and
15//! wrap-around is `& (TURN - 1)` rather than a comparison nobody remembers to
16//! write. Radians would put `f32` and a libm dependency in the one crate that
17//! has neither; degrees would make the common quarters 90/180/270 but leave the
18//! progress ring with a rounding remainder. Negative sweeps go anticlockwise.
19//!
20//! # The sine table
21//!
22//! `sin` and `cos` are not in `core`. The two ray directions come from a
23//! 257-entry quarter-wave table in Q16, linearly interpolated — the worst error
24//! against the real function is under 2 parts in 65536, which at a radius of a
25//! thousand pixels misplaces a ray endpoint by a fortieth of a pixel. The table
26//! is checked exhaustively by tests: every one of the 65536 angles must satisfy
27//! the Pythagorean identity to within a part in a thousand, and the quarters
28//! must be exact.
29
30use denise::{Point, Rect};
31
32pub use denise::TURN;
33
34use crate::blend::Paint;
35use crate::canvas::Canvas;
36use crate::rounded::{COORD_LIMIT, ONE, SUB_STEP, SUBSAMPLES, Scan, ceil_px, floor_px, to_fx};
37use denise::angle::direction;
38
39/// Sentinel for an unbounded side of a row interval. Far beyond any coordinate
40/// fixed point can carry, and far from overflowing anything it is added to.
41const UNBOUNDED: i64 = i64::MAX / 4;
42
43/// floor(b / a) for any sign of `a`.
44fn floor_div(b: i64, a: i64) -> i64 {
45    if a < 0 {
46        (-b).div_euclid(-a)
47    } else {
48        b.div_euclid(a)
49    }
50}
51
52/// ceil(b / a) for any sign of `a`.
53fn ceil_div(b: i64, a: i64) -> i64 {
54    -floor_div(-b, a)
55}
56
57/// The x-interval of one row that a half-plane through the centre keeps.
58///
59/// The half-plane is `cross(d, p - c) >= 0` (or `<= 0` for `keep_ge = false`)
60/// with `cross(d, r) = d.x·ry - d.y·rx`. On the row at height `ry` (fixed point,
61/// relative to the centre) that is linear in `rx`, so it keeps a half-line —
62/// or the whole row, or none of it, when the boundary ray is horizontal.
63fn half_plane(d: (i32, i32), ry: i64, keep_ge: bool) -> Option<(i64, i64)> {
64    let b = d.0 as i64 * ry;
65    let a = d.1 as i64;
66    if a == 0 {
67        let keeps = if keep_ge { b >= 0 } else { b <= 0 };
68        return keeps.then_some((-UNBOUNDED, UNBOUNDED));
69    }
70    // keep_ge:  a·rx <= b.  Otherwise:  a·rx >= b.  Dividing flips with a's sign.
71    let bounded_above = (a > 0) == keep_ge;
72    if bounded_above {
73        Some((-UNBOUNDED, floor_div(b, a)))
74    } else {
75        Some((ceil_div(b, a), UNBOUNDED))
76    }
77}
78
79/// The x-interval of one row inside the sector from `s` clockwise to `e`.
80///
81/// Only valid for sweeps of at most half a turn, where a sector is the
82/// intersection of two half-planes; a wider sweep is handled by its caller as
83/// the complement of the narrower one.
84fn sector_row(s: (i32, i32), e: (i32, i32), ry: i64) -> Option<(i64, i64)> {
85    let (lo_a, hi_a) = half_plane(s, ry, true)?;
86    let (lo_b, hi_b) = half_plane(e, ry, false)?;
87    let lo = lo_a.max(lo_b);
88    let hi = hi_a.min(hi_b);
89    (lo <= hi).then_some((lo, hi))
90}
91
92/// How the angular cut applies to a row interval.
93enum Cut {
94    /// Keep what falls inside the sector: a sweep of at most half a turn.
95    Keep,
96    /// Remove what falls inside the sector: the complement, for wider sweeps.
97    Remove,
98}
99
100/// The spans of one scanline, per sub-row, after every cut. At most four per
101/// sub-row: the ring contributes two, and removing a wedge can split one.
102struct RowSpans {
103    spans: [[(i32, i32); 4]; SUBSAMPLES],
104    counts: [usize; SUBSAMPLES],
105}
106
107impl RowSpans {
108    /// Total coverage of pixel column `x`, `0..=255` — the same rounding as the
109    /// rounded rectangles, so the two primitives meet without seams.
110    fn coverage(&self, x: i32) -> u32 {
111        let px0 = to_fx(x);
112        let px1 = px0 + ONE;
113        let mut covered: i32 = 0;
114        for k in 0..SUBSAMPLES {
115            for &(l, r) in &self.spans[k][..self.counts[k]] {
116                covered += (r.min(px1) - l.max(px0)).max(0);
117            }
118        }
119        let total = ONE as u32 * SUBSAMPLES as u32;
120        ((covered as u32 * 255 + total / 2) / total).min(255)
121    }
122}
123
124/// The pixel-column ranges a row's spans touch, merged so no column is visited
125/// twice — visiting one twice would composite translucent paint twice.
126struct Clusters {
127    runs: [(i32, i32); 16],
128    count: usize,
129}
130
131impl Clusters {
132    fn new() -> Self {
133        Self {
134            runs: [(0, 0); 16],
135            count: 0,
136        }
137    }
138
139    fn push(&mut self, from: i32, to: i32) {
140        if from >= to {
141            return;
142        }
143        // Merge with anything overlapping or adjacent, repeatedly: absorbing one
144        // run can bring the result into contact with another.
145        let mut from = from;
146        let mut to = to;
147        let mut i = 0;
148        while i < self.count {
149            let (a, b) = self.runs[i];
150            if from <= b && to >= a {
151                from = from.min(a);
152                to = to.max(b);
153                self.count -= 1;
154                self.runs[i] = self.runs[self.count];
155            } else {
156                i += 1;
157            }
158        }
159        if self.count < self.runs.len() {
160            self.runs[self.count] = (from, to);
161            self.count += 1;
162        }
163    }
164}
165
166impl Canvas<'_> {
167    /// Fills a circle of `radius` around `centre`, anti-aliased.
168    ///
169    /// Exactly [`Canvas::fill_rounded_rect`] on the bounding square — this name
170    /// exists so the intent is readable and the radius impossible to get wrong.
171    /// The painted diameter is `2 * radius` pixels.
172    pub fn fill_circle(&mut self, centre: Point, radius: i32, color: impl Into<Paint>) {
173        let r = radius.clamp(0, COORD_LIMIT);
174        let square = bounding_square(centre, r);
175        self.fill_rounded_rect(square, r, color);
176    }
177
178    /// Draws a ring of `thickness` pixels just inside the circle of `radius`
179    /// around `centre`, anti-aliased.
180    pub fn stroke_circle(
181        &mut self,
182        centre: Point,
183        radius: i32,
184        thickness: i32,
185        color: impl Into<Paint>,
186    ) {
187        let r = radius.clamp(0, COORD_LIMIT);
188        let square = bounding_square(centre, r);
189        self.stroke_rounded_rect(square, r, thickness.min(COORD_LIMIT), color);
190    }
191
192    /// Draws part of a ring: from `start`, sweeping `sweep`, both in units of
193    /// [`TURN`]. Angle 0 is twelve o'clock and positive sweeps go clockwise;
194    /// a negative sweep goes the other way. The ends are cut flat along the
195    /// radius — butt caps.
196    ///
197    /// A sweep of at least a full [`TURN`] is exactly [`Canvas::stroke_circle`],
198    /// which is what lets a progress ring pass `done * TURN / total` without
199    /// special-casing 100%. A `thickness` of at least `radius` fills to the
200    /// centre, which makes a pie slice.
201    ///
202    /// The cost is proportional to the pixels the arc actually covers, not to
203    /// its bounding square — a thin spinner touches a thin ring of pixels. What
204    /// to *damage* for an animated arc is the caller's business, but the same
205    /// property means a conservative bounding rectangle only costs rasterising
206    /// the ring inside it, not the square.
207    pub fn stroke_arc(
208        &mut self,
209        centre: Point,
210        radius: i32,
211        thickness: i32,
212        start: i32,
213        sweep: i32,
214        color: impl Into<Paint>,
215    ) {
216        let paint = color.into();
217        let r = radius.clamp(0, COORD_LIMIT);
218        let t = thickness.clamp(0, COORD_LIMIT).min(r);
219        if r == 0 || t == 0 || sweep == 0 || paint.is_invisible() {
220            return;
221        }
222
223        // Widened before normalising: negating `i32::MIN` overflows, and a
224        // sweep is allowed to be anything.
225        let mut start = start as i64;
226        let mut sweep = sweep as i64;
227        if sweep < 0 {
228            start += sweep;
229            sweep = -sweep;
230        }
231        if sweep >= TURN as i64 {
232            self.stroke_circle(centre, r, t, paint);
233            return;
234        }
235        let start = start.rem_euclid(TURN as i64) as i32;
236        let sweep = sweep as i32;
237
238        // A sector of at most half a turn is the intersection of two
239        // half-planes. A wider one is not — but its complement is, so the wider
240        // arc keeps what the complementary wedge does not claim.
241        let (cut, from, to) = if sweep <= TURN / 2 {
242            (Cut::Keep, start, start + sweep)
243        } else {
244            (Cut::Remove, start + sweep, start + TURN)
245        };
246        let s_dir = direction(from);
247        let e_dir = direction(to);
248
249        let square = bounding_square(centre, r);
250        let Some(vis) = self.visible(square) else {
251            return;
252        };
253        let inner_r = r - t;
254        let inner_square = bounding_square(centre, inner_r);
255        let centre_y = to_fx(centre.y);
256        // The sector arithmetic is relative to the centre; the chords are
257        // absolute. This is the shift that reconciles them.
258        let centre_x = to_fx(centre.x) as i64;
259
260        for y in vis.y..vis.bottom() {
261            let outer = Scan::new(square, r, y);
262            let hole = inner_r > 0 && y >= inner_square.y && y < inner_square.bottom();
263            let inner = if hole {
264                Some(Scan::new(inner_square, inner_r, y))
265            } else {
266                None
267            };
268
269            let mut row = RowSpans {
270                spans: [[(0, 0); 4]; SUBSAMPLES],
271                counts: [0; SUBSAMPLES],
272            };
273            let mut clusters = Clusters::new();
274
275            for k in 0..SUBSAMPLES {
276                let sy = to_fx(y) + k as i32 * SUB_STEP + SUB_STEP / 2;
277                let ry = (sy - centre_y) as i64;
278
279                // The ring on this sub-row: the outer chord, minus the inner
280                // one when it exists.
281                let (ol, or_) = (outer.left[k], outer.right[k]);
282                if ol >= or_ {
283                    continue;
284                }
285                let ring: [(i32, i32); 2] = match &inner {
286                    Some(inner) if inner.left[k] < inner.right[k] => {
287                        [(ol, inner.left[k]), (inner.right[k], or_)]
288                    }
289                    _ => [(ol, or_), (0, 0)],
290                };
291
292                let sector = sector_row(s_dir, e_dir, ry)
293                    .map(|(lo, hi)| (lo.saturating_add(centre_x), hi.saturating_add(centre_x)));
294                let mut push = |l: i64, r: i64| {
295                    // Clamped into the chord *before* narrowing: these values
296                    // can carry the UNBOUNDED sentinel, and `as i32` on that
297                    // truncates — which once turned "no piece at all" into
298                    // "the whole chord".
299                    let l = l.clamp(ol as i64, or_ as i64) as i32;
300                    let r = r.clamp(ol as i64, or_ as i64) as i32;
301                    if l < r {
302                        let n = &mut row.counts[k];
303                        row.spans[k][*n] = (l, r);
304                        *n += 1;
305                        clusters.push(floor_px(l), ceil_px(r));
306                    }
307                };
308
309                for &(l, r) in ring.iter().filter(|(l, r)| l < r) {
310                    match (&cut, sector) {
311                        (Cut::Keep, None) => {}
312                        (Cut::Keep, Some((cl, ch))) => {
313                            push((l as i64).max(cl), (r as i64).min(ch));
314                        }
315                        (Cut::Remove, None) => push(l as i64, r as i64),
316                        (Cut::Remove, Some((wl, wh))) => {
317                            push(l as i64, (r as i64).min(wl));
318                            push((l as i64).max(wh), r as i64);
319                        }
320                    }
321                }
322            }
323
324            let clip = self.clip();
325            for &(from, to) in &clusters.runs[..clusters.count] {
326                let from = from.max(clip.x);
327                let to = to.min(clip.right());
328                for x in from..to {
329                    self.blend_at(x, y, paint, row.coverage(x));
330                }
331            }
332        }
333    }
334}
335
336/// The square a circle of `radius` around `centre` fits in, saturating so an
337/// absurd centre cannot overflow — the coordinates are clamped again on their
338/// way into fixed point.
339fn bounding_square(centre: Point, radius: i32) -> Rect {
340    Rect::new(
341        centre.x.saturating_sub(radius),
342        centre.y.saturating_sub(radius),
343        radius.saturating_mul(2),
344        radius.saturating_mul(2),
345    )
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use crate::testing::TestCanvas;
352    use denise::Color;
353
354    fn alpha_of(px: u32) -> u32 {
355        // Opaque white on a black canvas, so any channel reads back as coverage.
356        px & 0xFF
357    }
358
359    // ------------------------------------------------------------ the shapes
360
361    /// The named wrappers are exactly the rounded-rect primitives on the
362    /// bounding square — same pixels, no second implementation to drift.
363    #[test]
364    fn a_circle_is_exactly_a_fully_rounded_square() {
365        let mut circle = TestCanvas::new(48, 48);
366        circle
367            .canvas()
368            .fill_circle(Point::new(24, 24), 20, Color::WHITE);
369        let mut square = TestCanvas::new(48, 48);
370        square
371            .canvas()
372            .fill_rounded_rect(Rect::new(4, 4, 40, 40), 20, Color::WHITE);
373        assert_eq!(circle.pixels(), square.pixels());
374
375        let mut ring = TestCanvas::new(48, 48);
376        ring.canvas()
377            .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
378        let mut band = TestCanvas::new(48, 48);
379        band.canvas()
380            .stroke_rounded_rect(Rect::new(4, 4, 40, 40), 20, 4, Color::WHITE);
381        assert_eq!(ring.pixels(), band.pixels());
382    }
383
384    /// A full sweep is the circle, bit for bit — the equality the issue asked
385    /// for, and what lets a progress ring pass `TURN` at 100% unspecial-cased.
386    #[test]
387    fn a_full_sweep_matches_the_circle_exactly() {
388        for start in [0, TURN / 8, -TURN / 3] {
389            let mut arc = TestCanvas::new(48, 48);
390            arc.canvas()
391                .stroke_arc(Point::new(24, 24), 20, 4, start, TURN, Color::WHITE);
392            let mut circle = TestCanvas::new(48, 48);
393            circle
394                .canvas()
395                .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
396            assert_eq!(arc.pixels(), circle.pixels(), "start {start}");
397        }
398        // And more than a full turn is still just the circle.
399        let mut over = TestCanvas::new(48, 48);
400        over.canvas()
401            .stroke_arc(Point::new(24, 24), 20, 4, 0, TURN * 3, Color::WHITE);
402        let mut circle = TestCanvas::new(48, 48);
403        circle
404            .canvas()
405            .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
406        assert_eq!(over.pixels(), circle.pixels());
407    }
408
409    /// A zero sweep draws nothing at all.
410    #[test]
411    fn a_zero_sweep_draws_nothing() {
412        let mut t = TestCanvas::new(48, 48);
413        t.canvas()
414            .stroke_arc(Point::new(24, 24), 20, 4, TURN / 8, 0, Color::WHITE);
415        assert!(t.pixels().iter().all(|&px| px == 0));
416    }
417
418    /// A quarter sweep from twelve o'clock lives in the top-right quadrant and
419    /// nowhere else — one pixel of slack along the cut edges, where the
420    /// anti-aliasing genuinely straddles the ray.
421    #[test]
422    fn a_quarter_arc_stays_in_its_quadrant() {
423        let (cx, cy) = (24, 24);
424        let mut t = TestCanvas::new(48, 48);
425        t.canvas()
426            .stroke_arc(Point::new(cx, cy), 20, 4, 0, TURN / 4, Color::WHITE);
427
428        let mut painted = 0;
429        for y in 0..48 {
430            for x in 0..48 {
431                if alpha_of(t.at(x, y)) > 0 {
432                    painted += 1;
433                    assert!(
434                        x >= cx - 1 && y <= cy,
435                        "quarter arc escaped its quadrant at {x},{y}"
436                    );
437                }
438            }
439        }
440        assert!(painted > 50, "only {painted} pixels for a quarter arc");
441        // The twelve o'clock cap is at the top, the three o'clock cap on the
442        // right: both ends of the sweep actually drew. Sampled one pixel up
443        // from the axis, because the centre of an even-diameter circle is a
444        // pixel *boundary* — the cap at three o'clock runs between rows.
445        assert!(alpha_of(t.at(cx, cy - 20 + 2)) > 0, "no paint at the start");
446        assert!(
447            alpha_of(t.at(cx + 20 - 2, cy - 1)) > 0,
448            "no paint at the end"
449        );
450    }
451
452    /// A sweep crossing the wrap point paints both sides of twelve o'clock.
453    #[test]
454    fn a_sweep_across_the_wrap_point_paints_both_sides_of_the_top() {
455        let (cx, cy) = (24, 24);
456        let mut t = TestCanvas::new(48, 48);
457        // From 315° round to 45°, crossing zero.
458        t.canvas().stroke_arc(
459            Point::new(cx, cy),
460            20,
461            4,
462            7 * TURN / 8,
463            TURN / 4,
464            Color::WHITE,
465        );
466        assert!(alpha_of(t.at(cx - 8, cy - 17)) > 0, "left of the top");
467        assert!(alpha_of(t.at(cx + 8, cy - 17)) > 0, "right of the top");
468        assert_eq!(alpha_of(t.at(cx, cy + 18)), 0, "nothing at the bottom");
469        assert_eq!(alpha_of(t.at(cx - 18, cy)), 0, "nothing at nine o'clock");
470        assert_eq!(alpha_of(t.at(cx + 18, cy)), 0, "nothing at three o'clock");
471    }
472
473    /// A negative sweep is the same arc as the positive one that ends where it
474    /// starts.
475    #[test]
476    fn a_negative_sweep_goes_the_other_way() {
477        let mut negative = TestCanvas::new(48, 48);
478        negative
479            .canvas()
480            .stroke_arc(Point::new(24, 24), 20, 4, 0, -TURN / 4, Color::WHITE);
481        let mut positive = TestCanvas::new(48, 48);
482        positive.canvas().stroke_arc(
483            Point::new(24, 24),
484            20,
485            4,
486            3 * TURN / 4,
487            TURN / 4,
488            Color::WHITE,
489        );
490        assert_eq!(negative.pixels(), positive.pixels());
491    }
492
493    /// Sweeps wider than half a turn go through the complement path; the two
494    /// paths have to agree about where an edge is. A three-quarter arc and the
495    /// quarter arc that completes it must tile the ring: everywhere the circle
496    /// is solid, the two together must account for it, and where the circle is
497    /// empty both must be empty.
498    #[test]
499    fn a_wide_arc_and_its_complement_tile_the_ring() {
500        let mut wide = TestCanvas::new(48, 48);
501        wide.canvas().stroke_arc(
502            Point::new(24, 24),
503            20,
504            4,
505            TURN / 4,
506            3 * TURN / 4,
507            Color::WHITE,
508        );
509        let mut narrow = TestCanvas::new(48, 48);
510        narrow
511            .canvas()
512            .stroke_arc(Point::new(24, 24), 20, 4, 0, TURN / 4, Color::WHITE);
513        let mut circle = TestCanvas::new(48, 48);
514        circle
515            .canvas()
516            .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
517
518        for y in 0..48 {
519            for x in 0..48 {
520                let whole = alpha_of(circle.at(x, y));
521                let sum = alpha_of(wide.at(x, y)) + alpha_of(narrow.at(x, y));
522                if whole == 0 {
523                    assert_eq!(sum, 0, "painted outside the ring at {x},{y}");
524                } else if whole == 255 {
525                    // Butt caps overlap by at most the anti-aliased edge, so the
526                    // sum can exceed a full pixel but never fall short of one.
527                    assert!(
528                        (255..=510).contains(&sum),
529                        "the two arcs left a hole at {x},{y}: {sum}"
530                    );
531                }
532            }
533        }
534    }
535
536    /// The interior of a ring is untouched, and a thickness of at least the
537    /// radius fills to the centre — the pie-slice case.
538    #[test]
539    fn thickness_decides_between_a_ring_and_a_pie() {
540        let mut ring = TestCanvas::new(48, 48);
541        ring.canvas()
542            .stroke_arc(Point::new(24, 24), 20, 4, 0, TURN / 2, Color::WHITE);
543        assert_eq!(alpha_of(ring.at(24, 24)), 0, "ring centre must be empty");
544        assert_eq!(alpha_of(ring.at(30, 24)), 0, "ring interior must be empty");
545
546        let mut pie = TestCanvas::new(48, 48);
547        pie.canvas()
548            .stroke_arc(Point::new(24, 24), 20, 99, 0, TURN / 2, Color::WHITE);
549        assert_eq!(alpha_of(pie.at(30, 24)), 255, "pie interior must be solid");
550        assert_eq!(alpha_of(pie.at(17, 24)), 0, "outside the pie's half");
551    }
552
553    /// Translucent paint composites once per pixel, however the spans and
554    /// clusters carve the row up.
555    #[test]
556    fn translucent_arcs_never_composite_a_pixel_twice() {
557        for sweep in [TURN / 4, TURN / 2, 3 * TURN / 4, TURN - TURN / 16] {
558            let mut t = TestCanvas::new(48, 48);
559            t.canvas().stroke_arc(
560                Point::new(24, 24),
561                20,
562                4,
563                TURN / 16,
564                sweep,
565                Color::rgba(255, 255, 255, 128),
566            );
567            let ceiling = 128;
568            for y in 0..48 {
569                for x in 0..48 {
570                    assert!(
571                        alpha_of(t.at(x, y)) <= ceiling,
572                        "double-composited at {x},{y} with sweep {sweep}"
573                    );
574                }
575            }
576        }
577    }
578
579    /// Clipping changes which pixels are written, never what is written.
580    #[test]
581    fn clipping_an_arc_matches_the_unclipped_result() {
582        let region = Rect::new(10, 6, 20, 22);
583        let mut full = TestCanvas::new(48, 48);
584        full.canvas()
585            .stroke_arc(Point::new(24, 24), 18, 5, 0, 3 * TURN / 4, Color::WHITE);
586        let mut clipped = TestCanvas::new(48, 48);
587        {
588            let mut c = clipped.canvas();
589            c.clip_to(region);
590            c.stroke_arc(Point::new(24, 24), 18, 5, 0, 3 * TURN / 4, Color::WHITE);
591        }
592        for y in 0..48 {
593            for x in 0..48 {
594                let expected = if region.contains(Point::new(x, y)) {
595                    full.at(x, y)
596                } else {
597                    0
598                };
599                assert_eq!(clipped.at(x, y), expected, "at {x},{y}");
600            }
601        }
602    }
603
604    /// Degenerate and absurd inputs draw nothing or something, but never panic —
605    /// a panic inside a paint loop on a kiosk is a black screen.
606    #[test]
607    fn degenerate_arcs_do_not_panic() {
608        let mut t = TestCanvas::new(16, 16);
609        let mut c = t.canvas();
610        c.stroke_arc(Point::new(8, 8), 0, 4, 0, TURN, Color::WHITE);
611        c.stroke_arc(Point::new(8, 8), 6, 0, 0, TURN, Color::WHITE);
612        c.stroke_arc(Point::new(8, 8), -5, 3, 0, TURN, Color::WHITE);
613        c.stroke_arc(Point::new(8, 8), 6, -2, 0, TURN, Color::WHITE);
614        c.stroke_arc(Point::new(8, 8), 6, 3, i32::MIN, i32::MIN, Color::WHITE);
615        c.stroke_arc(Point::new(8, 8), 6, 3, i32::MAX, i32::MAX, Color::WHITE);
616        c.stroke_arc(
617            Point::new(i32::MIN, i32::MAX),
618            i32::MAX,
619            i32::MAX,
620            1,
621            1,
622            Color::WHITE,
623        );
624        c.fill_circle(Point::new(8, 8), 0, Color::WHITE);
625        c.fill_circle(Point::new(-1000, 8), i32::MAX, Color::WHITE);
626        c.stroke_circle(Point::new(8, 8), 6, i32::MAX, Color::WHITE);
627    }
628
629    /// The rasteriser against an independent oracle: 16×16 supersampled
630    /// point-in-annulus ∧ point-in-sector membership, in the same integer
631    /// arithmetic but sharing none of the scanline code. Interior and exterior
632    /// pixels must agree exactly; edge pixels within the difference between
633    /// 4-sub-row coverage and true area.
634    #[test]
635    fn coverage_agrees_with_a_supersampled_oracle() {
636        let (cx, cy) = (24, 24);
637        let (r, t) = (18, 5);
638        for (start, sweep) in [
639            (0, TURN / 4),
640            (TURN / 8, TURN / 2),
641            (7 * TURN / 8, TURN / 4),
642            (TURN / 4, 3 * TURN / 4),
643            (0, TURN / 2),
644        ] {
645            let mut canvas = TestCanvas::new(48, 48);
646            canvas
647                .canvas()
648                .stroke_arc(Point::new(cx, cy), r, t, start, sweep, Color::WHITE);
649
650            let s_dir = direction(start);
651            let e_dir = direction(start + sweep);
652            let wide = sweep > TURN / 2;
653
654            for py in 0..48 {
655                for px in 0..48 {
656                    let mut hits = 0u32;
657                    for sy in 0..16 {
658                        for sx in 0..16 {
659                            // Sample point in 1/32ths of a pixel, relative to
660                            // the centre.
661                            let dx = (px - cx) * 32 + sx * 2 + 1;
662                            let dy = (py - cy) * 32 + sy * 2 + 1;
663                            let d2 = (dx as i64) * (dx as i64) + (dy as i64) * (dy as i64);
664                            let outer = (r as i64 * 32).pow(2);
665                            let inner = ((r - t) as i64 * 32).pow(2);
666                            if d2 > outer || d2 <= inner {
667                                continue;
668                            }
669                            let cross_s = s_dir.0 as i64 * dy as i64 - s_dir.1 as i64 * dx as i64;
670                            let cross_e = e_dir.0 as i64 * dy as i64 - e_dir.1 as i64 * dx as i64;
671                            let in_sector = if wide {
672                                // Complement of the narrow sector from end to
673                                // start.
674                                !(cross_e >= 0 && cross_s <= 0)
675                            } else {
676                                cross_s >= 0 && cross_e <= 0
677                            };
678                            if in_sector {
679                                hits += 1;
680                            }
681                        }
682                    }
683                    let expected = (hits * 255 + 128) / 256;
684                    let actual = alpha_of(canvas.at(px, py));
685                    let error = expected.abs_diff(actual);
686                    assert!(
687                        error <= 72,
688                        "start {start} sweep {sweep} at {px},{py}: \
689                         oracle {expected}, rasteriser {actual}"
690                    );
691                    if expected == 0 {
692                        assert!(
693                            actual <= 16,
694                            "start {start} sweep {sweep}: painted well outside \
695                             the arc at {px},{py}: {actual}"
696                        );
697                    }
698                    if expected == 255 {
699                        assert!(
700                            actual >= 240,
701                            "start {start} sweep {sweep}: hole inside the arc \
702                             at {px},{py}: {actual}"
703                        );
704                    }
705                }
706            }
707        }
708    }
709}