1use denise::{Point, Rect};
31
32use crate::blend::Paint;
33use crate::canvas::Canvas;
34use crate::rounded::{COORD_LIMIT, ONE, SUB_STEP, SUBSAMPLES, Scan, ceil_px, floor_px, to_fx};
35
36pub const TURN: i32 = 1 << 16;
42
43#[rustfmt::skip]
48const SIN_QUARTER: [i32; 257] = [
49 0, 402, 804, 1206, 1608, 2010, 2412, 2814,
50 3216, 3617, 4019, 4420, 4821, 5222, 5623, 6023,
51 6424, 6824, 7224, 7623, 8022, 8421, 8820, 9218,
52 9616, 10014, 10411, 10808, 11204, 11600, 11996, 12391,
53 12785, 13180, 13573, 13966, 14359, 14751, 15143, 15534,
54 15924, 16314, 16703, 17091, 17479, 17867, 18253, 18639,
55 19024, 19409, 19792, 20175, 20557, 20939, 21320, 21699,
56 22078, 22457, 22834, 23210, 23586, 23961, 24335, 24708,
57 25080, 25451, 25821, 26190, 26558, 26925, 27291, 27656,
58 28020, 28383, 28745, 29106, 29466, 29824, 30182, 30538,
59 30893, 31248, 31600, 31952, 32303, 32652, 33000, 33347,
60 33692, 34037, 34380, 34721, 35062, 35401, 35738, 36075,
61 36410, 36744, 37076, 37407, 37736, 38064, 38391, 38716,
62 39040, 39362, 39683, 40002, 40320, 40636, 40951, 41264,
63 41576, 41886, 42194, 42501, 42806, 43110, 43412, 43713,
64 44011, 44308, 44604, 44898, 45190, 45480, 45769, 46056,
65 46341, 46624, 46906, 47186, 47464, 47741, 48015, 48288,
66 48559, 48828, 49095, 49361, 49624, 49886, 50146, 50404,
67 50660, 50914, 51166, 51417, 51665, 51911, 52156, 52398,
68 52639, 52878, 53114, 53349, 53581, 53812, 54040, 54267,
69 54491, 54714, 54934, 55152, 55368, 55582, 55794, 56004,
70 56212, 56418, 56621, 56823, 57022, 57219, 57414, 57607,
71 57798, 57986, 58172, 58356, 58538, 58718, 58896, 59071,
72 59244, 59415, 59583, 59750, 59914, 60075, 60235, 60392,
73 60547, 60700, 60851, 60999, 61145, 61288, 61429, 61568,
74 61705, 61839, 61971, 62101, 62228, 62353, 62476, 62596,
75 62714, 62830, 62943, 63054, 63162, 63268, 63372, 63473,
76 63572, 63668, 63763, 63854, 63944, 64031, 64115, 64197,
77 64277, 64354, 64429, 64501, 64571, 64639, 64704, 64766,
78 64827, 64884, 64940, 64993, 65043, 65091, 65137, 65180,
79 65220, 65259, 65294, 65328, 65358, 65387, 65413, 65436,
80 65457, 65476, 65492, 65505, 65516, 65525, 65531, 65535,
81 65536,
82];
83
84fn sin_bam(angle: i32) -> i32 {
86 let a = angle.rem_euclid(TURN);
87 let quarter = TURN / 4;
88 let (quadrant, q) = (a / quarter, a % quarter);
89 let lookup = |q: i32| -> i32 {
92 let idx = (q >> 6) as usize;
93 let frac = q & 63;
94 if frac == 0 {
95 SIN_QUARTER[idx]
96 } else {
97 SIN_QUARTER[idx] + (SIN_QUARTER[idx + 1] - SIN_QUARTER[idx]) * frac / 64
98 }
99 };
100 match quadrant {
101 0 => lookup(q),
102 1 => lookup(quarter - q),
103 2 => -lookup(q),
104 _ => -lookup(quarter - q),
105 }
106}
107
108fn direction(angle: i32) -> (i32, i32) {
112 (sin_bam(angle), -sin_bam(angle + TURN / 4))
113}
114
115const UNBOUNDED: i64 = i64::MAX / 4;
118
119fn floor_div(b: i64, a: i64) -> i64 {
121 if a < 0 {
122 (-b).div_euclid(-a)
123 } else {
124 b.div_euclid(a)
125 }
126}
127
128fn ceil_div(b: i64, a: i64) -> i64 {
130 -floor_div(-b, a)
131}
132
133fn half_plane(d: (i32, i32), ry: i64, keep_ge: bool) -> Option<(i64, i64)> {
140 let b = d.0 as i64 * ry;
141 let a = d.1 as i64;
142 if a == 0 {
143 let keeps = if keep_ge { b >= 0 } else { b <= 0 };
144 return keeps.then_some((-UNBOUNDED, UNBOUNDED));
145 }
146 let bounded_above = (a > 0) == keep_ge;
148 if bounded_above {
149 Some((-UNBOUNDED, floor_div(b, a)))
150 } else {
151 Some((ceil_div(b, a), UNBOUNDED))
152 }
153}
154
155fn sector_row(s: (i32, i32), e: (i32, i32), ry: i64) -> Option<(i64, i64)> {
161 let (lo_a, hi_a) = half_plane(s, ry, true)?;
162 let (lo_b, hi_b) = half_plane(e, ry, false)?;
163 let lo = lo_a.max(lo_b);
164 let hi = hi_a.min(hi_b);
165 (lo <= hi).then_some((lo, hi))
166}
167
168enum Cut {
170 Keep,
172 Remove,
174}
175
176struct RowSpans {
179 spans: [[(i32, i32); 4]; SUBSAMPLES],
180 counts: [usize; SUBSAMPLES],
181}
182
183impl RowSpans {
184 fn coverage(&self, x: i32) -> u32 {
187 let px0 = to_fx(x);
188 let px1 = px0 + ONE;
189 let mut covered: i32 = 0;
190 for k in 0..SUBSAMPLES {
191 for &(l, r) in &self.spans[k][..self.counts[k]] {
192 covered += (r.min(px1) - l.max(px0)).max(0);
193 }
194 }
195 let total = ONE as u32 * SUBSAMPLES as u32;
196 ((covered as u32 * 255 + total / 2) / total).min(255)
197 }
198}
199
200struct Clusters {
203 runs: [(i32, i32); 16],
204 count: usize,
205}
206
207impl Clusters {
208 fn new() -> Self {
209 Self {
210 runs: [(0, 0); 16],
211 count: 0,
212 }
213 }
214
215 fn push(&mut self, from: i32, to: i32) {
216 if from >= to {
217 return;
218 }
219 let mut from = from;
222 let mut to = to;
223 let mut i = 0;
224 while i < self.count {
225 let (a, b) = self.runs[i];
226 if from <= b && to >= a {
227 from = from.min(a);
228 to = to.max(b);
229 self.count -= 1;
230 self.runs[i] = self.runs[self.count];
231 } else {
232 i += 1;
233 }
234 }
235 if self.count < self.runs.len() {
236 self.runs[self.count] = (from, to);
237 self.count += 1;
238 }
239 }
240}
241
242impl Canvas<'_> {
243 pub fn fill_circle(&mut self, centre: Point, radius: i32, color: impl Into<Paint>) {
249 let r = radius.clamp(0, COORD_LIMIT);
250 let square = bounding_square(centre, r);
251 self.fill_rounded_rect(square, r, color);
252 }
253
254 pub fn stroke_circle(
257 &mut self,
258 centre: Point,
259 radius: i32,
260 thickness: i32,
261 color: impl Into<Paint>,
262 ) {
263 let r = radius.clamp(0, COORD_LIMIT);
264 let square = bounding_square(centre, r);
265 self.stroke_rounded_rect(square, r, thickness.min(COORD_LIMIT), color);
266 }
267
268 pub fn stroke_arc(
284 &mut self,
285 centre: Point,
286 radius: i32,
287 thickness: i32,
288 start: i32,
289 sweep: i32,
290 color: impl Into<Paint>,
291 ) {
292 let paint = color.into();
293 let r = radius.clamp(0, COORD_LIMIT);
294 let t = thickness.clamp(0, COORD_LIMIT).min(r);
295 if r == 0 || t == 0 || sweep == 0 || paint.is_invisible() {
296 return;
297 }
298
299 let mut start = start as i64;
302 let mut sweep = sweep as i64;
303 if sweep < 0 {
304 start += sweep;
305 sweep = -sweep;
306 }
307 if sweep >= TURN as i64 {
308 self.stroke_circle(centre, r, t, paint);
309 return;
310 }
311 let start = start.rem_euclid(TURN as i64) as i32;
312 let sweep = sweep as i32;
313
314 let (cut, from, to) = if sweep <= TURN / 2 {
318 (Cut::Keep, start, start + sweep)
319 } else {
320 (Cut::Remove, start + sweep, start + TURN)
321 };
322 let s_dir = direction(from);
323 let e_dir = direction(to);
324
325 let square = bounding_square(centre, r);
326 let Some(vis) = self.visible(square) else {
327 return;
328 };
329 let inner_r = r - t;
330 let inner_square = bounding_square(centre, inner_r);
331 let centre_y = to_fx(centre.y);
332 let centre_x = to_fx(centre.x) as i64;
335
336 for y in vis.y..vis.bottom() {
337 let outer = Scan::new(square, r, y);
338 let hole = inner_r > 0 && y >= inner_square.y && y < inner_square.bottom();
339 let inner = if hole {
340 Some(Scan::new(inner_square, inner_r, y))
341 } else {
342 None
343 };
344
345 let mut row = RowSpans {
346 spans: [[(0, 0); 4]; SUBSAMPLES],
347 counts: [0; SUBSAMPLES],
348 };
349 let mut clusters = Clusters::new();
350
351 for k in 0..SUBSAMPLES {
352 let sy = to_fx(y) + k as i32 * SUB_STEP + SUB_STEP / 2;
353 let ry = (sy - centre_y) as i64;
354
355 let (ol, or_) = (outer.left[k], outer.right[k]);
358 if ol >= or_ {
359 continue;
360 }
361 let ring: [(i32, i32); 2] = match &inner {
362 Some(inner) if inner.left[k] < inner.right[k] => {
363 [(ol, inner.left[k]), (inner.right[k], or_)]
364 }
365 _ => [(ol, or_), (0, 0)],
366 };
367
368 let sector = sector_row(s_dir, e_dir, ry)
369 .map(|(lo, hi)| (lo.saturating_add(centre_x), hi.saturating_add(centre_x)));
370 let mut push = |l: i64, r: i64| {
371 let l = l.clamp(ol as i64, or_ as i64) as i32;
376 let r = r.clamp(ol as i64, or_ as i64) as i32;
377 if l < r {
378 let n = &mut row.counts[k];
379 row.spans[k][*n] = (l, r);
380 *n += 1;
381 clusters.push(floor_px(l), ceil_px(r));
382 }
383 };
384
385 for &(l, r) in ring.iter().filter(|(l, r)| l < r) {
386 match (&cut, sector) {
387 (Cut::Keep, None) => {}
388 (Cut::Keep, Some((cl, ch))) => {
389 push((l as i64).max(cl), (r as i64).min(ch));
390 }
391 (Cut::Remove, None) => push(l as i64, r as i64),
392 (Cut::Remove, Some((wl, wh))) => {
393 push(l as i64, (r as i64).min(wl));
394 push((l as i64).max(wh), r as i64);
395 }
396 }
397 }
398 }
399
400 let clip = self.clip();
401 for &(from, to) in &clusters.runs[..clusters.count] {
402 let from = from.max(clip.x);
403 let to = to.min(clip.right());
404 for x in from..to {
405 self.blend_at(x, y, paint, row.coverage(x));
406 }
407 }
408 }
409 }
410}
411
412fn bounding_square(centre: Point, radius: i32) -> Rect {
416 Rect::new(
417 centre.x.saturating_sub(radius),
418 centre.y.saturating_sub(radius),
419 radius.saturating_mul(2),
420 radius.saturating_mul(2),
421 )
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::testing::TestCanvas;
428 use denise::Color;
429
430 fn alpha_of(px: u32) -> u32 {
431 px & 0xFF
433 }
434
435 #[test]
441 fn every_angle_satisfies_the_pythagorean_identity() {
442 for a in 0..TURN {
443 let s = sin_bam(a) as i64;
444 let c = sin_bam(a + TURN / 4) as i64;
445 let one = (s * s + c * c) >> 16;
446 assert!(
447 (one - 65536).abs() < 64,
448 "angle {a}: sin²+cos² is {one}, not 65536"
449 );
450 }
451 }
452
453 #[test]
456 fn the_cardinal_directions_are_exact() {
457 assert_eq!(direction(0), (0, -65536), "twelve o'clock");
458 assert_eq!(direction(TURN / 4), (65536, 0), "three o'clock");
459 assert_eq!(direction(TURN / 2), (0, 65536), "six o'clock");
460 assert_eq!(direction(3 * TURN / 4), (-65536, 0), "nine o'clock");
461 assert_eq!(direction(TURN), (0, -65536), "and round again");
462 assert_eq!(direction(-TURN / 4), (-65536, 0), "negative wraps too");
463 }
464
465 #[test]
468 fn sine_rises_monotonically_over_the_first_quarter() {
469 let mut previous = -1;
470 for a in 0..=TURN / 4 {
471 let s = sin_bam(a);
472 assert!(s >= previous, "sin fell at angle {a}");
473 previous = s;
474 }
475 }
476
477 #[test]
482 fn a_circle_is_exactly_a_fully_rounded_square() {
483 let mut circle = TestCanvas::new(48, 48);
484 circle
485 .canvas()
486 .fill_circle(Point::new(24, 24), 20, Color::WHITE);
487 let mut square = TestCanvas::new(48, 48);
488 square
489 .canvas()
490 .fill_rounded_rect(Rect::new(4, 4, 40, 40), 20, Color::WHITE);
491 assert_eq!(circle.pixels(), square.pixels());
492
493 let mut ring = TestCanvas::new(48, 48);
494 ring.canvas()
495 .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
496 let mut band = TestCanvas::new(48, 48);
497 band.canvas()
498 .stroke_rounded_rect(Rect::new(4, 4, 40, 40), 20, 4, Color::WHITE);
499 assert_eq!(ring.pixels(), band.pixels());
500 }
501
502 #[test]
505 fn a_full_sweep_matches_the_circle_exactly() {
506 for start in [0, TURN / 8, -TURN / 3] {
507 let mut arc = TestCanvas::new(48, 48);
508 arc.canvas()
509 .stroke_arc(Point::new(24, 24), 20, 4, start, TURN, Color::WHITE);
510 let mut circle = TestCanvas::new(48, 48);
511 circle
512 .canvas()
513 .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
514 assert_eq!(arc.pixels(), circle.pixels(), "start {start}");
515 }
516 let mut over = TestCanvas::new(48, 48);
518 over.canvas()
519 .stroke_arc(Point::new(24, 24), 20, 4, 0, TURN * 3, Color::WHITE);
520 let mut circle = TestCanvas::new(48, 48);
521 circle
522 .canvas()
523 .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
524 assert_eq!(over.pixels(), circle.pixels());
525 }
526
527 #[test]
529 fn a_zero_sweep_draws_nothing() {
530 let mut t = TestCanvas::new(48, 48);
531 t.canvas()
532 .stroke_arc(Point::new(24, 24), 20, 4, TURN / 8, 0, Color::WHITE);
533 assert!(t.pixels().iter().all(|&px| px == 0));
534 }
535
536 #[test]
540 fn a_quarter_arc_stays_in_its_quadrant() {
541 let (cx, cy) = (24, 24);
542 let mut t = TestCanvas::new(48, 48);
543 t.canvas()
544 .stroke_arc(Point::new(cx, cy), 20, 4, 0, TURN / 4, Color::WHITE);
545
546 let mut painted = 0;
547 for y in 0..48 {
548 for x in 0..48 {
549 if alpha_of(t.at(x, y)) > 0 {
550 painted += 1;
551 assert!(
552 x >= cx - 1 && y <= cy,
553 "quarter arc escaped its quadrant at {x},{y}"
554 );
555 }
556 }
557 }
558 assert!(painted > 50, "only {painted} pixels for a quarter arc");
559 assert!(alpha_of(t.at(cx, cy - 20 + 2)) > 0, "no paint at the start");
564 assert!(
565 alpha_of(t.at(cx + 20 - 2, cy - 1)) > 0,
566 "no paint at the end"
567 );
568 }
569
570 #[test]
572 fn a_sweep_across_the_wrap_point_paints_both_sides_of_the_top() {
573 let (cx, cy) = (24, 24);
574 let mut t = TestCanvas::new(48, 48);
575 t.canvas().stroke_arc(
577 Point::new(cx, cy),
578 20,
579 4,
580 7 * TURN / 8,
581 TURN / 4,
582 Color::WHITE,
583 );
584 assert!(alpha_of(t.at(cx - 8, cy - 17)) > 0, "left of the top");
585 assert!(alpha_of(t.at(cx + 8, cy - 17)) > 0, "right of the top");
586 assert_eq!(alpha_of(t.at(cx, cy + 18)), 0, "nothing at the bottom");
587 assert_eq!(alpha_of(t.at(cx - 18, cy)), 0, "nothing at nine o'clock");
588 assert_eq!(alpha_of(t.at(cx + 18, cy)), 0, "nothing at three o'clock");
589 }
590
591 #[test]
594 fn a_negative_sweep_goes_the_other_way() {
595 let mut negative = TestCanvas::new(48, 48);
596 negative
597 .canvas()
598 .stroke_arc(Point::new(24, 24), 20, 4, 0, -TURN / 4, Color::WHITE);
599 let mut positive = TestCanvas::new(48, 48);
600 positive.canvas().stroke_arc(
601 Point::new(24, 24),
602 20,
603 4,
604 3 * TURN / 4,
605 TURN / 4,
606 Color::WHITE,
607 );
608 assert_eq!(negative.pixels(), positive.pixels());
609 }
610
611 #[test]
617 fn a_wide_arc_and_its_complement_tile_the_ring() {
618 let mut wide = TestCanvas::new(48, 48);
619 wide.canvas().stroke_arc(
620 Point::new(24, 24),
621 20,
622 4,
623 TURN / 4,
624 3 * TURN / 4,
625 Color::WHITE,
626 );
627 let mut narrow = TestCanvas::new(48, 48);
628 narrow
629 .canvas()
630 .stroke_arc(Point::new(24, 24), 20, 4, 0, TURN / 4, Color::WHITE);
631 let mut circle = TestCanvas::new(48, 48);
632 circle
633 .canvas()
634 .stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
635
636 for y in 0..48 {
637 for x in 0..48 {
638 let whole = alpha_of(circle.at(x, y));
639 let sum = alpha_of(wide.at(x, y)) + alpha_of(narrow.at(x, y));
640 if whole == 0 {
641 assert_eq!(sum, 0, "painted outside the ring at {x},{y}");
642 } else if whole == 255 {
643 assert!(
646 (255..=510).contains(&sum),
647 "the two arcs left a hole at {x},{y}: {sum}"
648 );
649 }
650 }
651 }
652 }
653
654 #[test]
657 fn thickness_decides_between_a_ring_and_a_pie() {
658 let mut ring = TestCanvas::new(48, 48);
659 ring.canvas()
660 .stroke_arc(Point::new(24, 24), 20, 4, 0, TURN / 2, Color::WHITE);
661 assert_eq!(alpha_of(ring.at(24, 24)), 0, "ring centre must be empty");
662 assert_eq!(alpha_of(ring.at(30, 24)), 0, "ring interior must be empty");
663
664 let mut pie = TestCanvas::new(48, 48);
665 pie.canvas()
666 .stroke_arc(Point::new(24, 24), 20, 99, 0, TURN / 2, Color::WHITE);
667 assert_eq!(alpha_of(pie.at(30, 24)), 255, "pie interior must be solid");
668 assert_eq!(alpha_of(pie.at(17, 24)), 0, "outside the pie's half");
669 }
670
671 #[test]
674 fn translucent_arcs_never_composite_a_pixel_twice() {
675 for sweep in [TURN / 4, TURN / 2, 3 * TURN / 4, TURN - TURN / 16] {
676 let mut t = TestCanvas::new(48, 48);
677 t.canvas().stroke_arc(
678 Point::new(24, 24),
679 20,
680 4,
681 TURN / 16,
682 sweep,
683 Color::rgba(255, 255, 255, 128),
684 );
685 let ceiling = 128;
686 for y in 0..48 {
687 for x in 0..48 {
688 assert!(
689 alpha_of(t.at(x, y)) <= ceiling,
690 "double-composited at {x},{y} with sweep {sweep}"
691 );
692 }
693 }
694 }
695 }
696
697 #[test]
699 fn clipping_an_arc_matches_the_unclipped_result() {
700 let region = Rect::new(10, 6, 20, 22);
701 let mut full = TestCanvas::new(48, 48);
702 full.canvas()
703 .stroke_arc(Point::new(24, 24), 18, 5, 0, 3 * TURN / 4, Color::WHITE);
704 let mut clipped = TestCanvas::new(48, 48);
705 {
706 let mut c = clipped.canvas();
707 c.clip_to(region);
708 c.stroke_arc(Point::new(24, 24), 18, 5, 0, 3 * TURN / 4, Color::WHITE);
709 }
710 for y in 0..48 {
711 for x in 0..48 {
712 let expected = if region.contains(Point::new(x, y)) {
713 full.at(x, y)
714 } else {
715 0
716 };
717 assert_eq!(clipped.at(x, y), expected, "at {x},{y}");
718 }
719 }
720 }
721
722 #[test]
725 fn degenerate_arcs_do_not_panic() {
726 let mut t = TestCanvas::new(16, 16);
727 let mut c = t.canvas();
728 c.stroke_arc(Point::new(8, 8), 0, 4, 0, TURN, Color::WHITE);
729 c.stroke_arc(Point::new(8, 8), 6, 0, 0, TURN, Color::WHITE);
730 c.stroke_arc(Point::new(8, 8), -5, 3, 0, TURN, Color::WHITE);
731 c.stroke_arc(Point::new(8, 8), 6, -2, 0, TURN, Color::WHITE);
732 c.stroke_arc(Point::new(8, 8), 6, 3, i32::MIN, i32::MIN, Color::WHITE);
733 c.stroke_arc(Point::new(8, 8), 6, 3, i32::MAX, i32::MAX, Color::WHITE);
734 c.stroke_arc(
735 Point::new(i32::MIN, i32::MAX),
736 i32::MAX,
737 i32::MAX,
738 1,
739 1,
740 Color::WHITE,
741 );
742 c.fill_circle(Point::new(8, 8), 0, Color::WHITE);
743 c.fill_circle(Point::new(-1000, 8), i32::MAX, Color::WHITE);
744 c.stroke_circle(Point::new(8, 8), 6, i32::MAX, Color::WHITE);
745 }
746
747 #[test]
753 fn coverage_agrees_with_a_supersampled_oracle() {
754 let (cx, cy) = (24, 24);
755 let (r, t) = (18, 5);
756 for (start, sweep) in [
757 (0, TURN / 4),
758 (TURN / 8, TURN / 2),
759 (7 * TURN / 8, TURN / 4),
760 (TURN / 4, 3 * TURN / 4),
761 (0, TURN / 2),
762 ] {
763 let mut canvas = TestCanvas::new(48, 48);
764 canvas
765 .canvas()
766 .stroke_arc(Point::new(cx, cy), r, t, start, sweep, Color::WHITE);
767
768 let s_dir = direction(start);
769 let e_dir = direction(start + sweep);
770 let wide = sweep > TURN / 2;
771
772 for py in 0..48 {
773 for px in 0..48 {
774 let mut hits = 0u32;
775 for sy in 0..16 {
776 for sx in 0..16 {
777 let dx = (px - cx) * 32 + sx * 2 + 1;
780 let dy = (py - cy) * 32 + sy * 2 + 1;
781 let d2 = (dx as i64) * (dx as i64) + (dy as i64) * (dy as i64);
782 let outer = (r as i64 * 32).pow(2);
783 let inner = ((r - t) as i64 * 32).pow(2);
784 if d2 > outer || d2 <= inner {
785 continue;
786 }
787 let cross_s = s_dir.0 as i64 * dy as i64 - s_dir.1 as i64 * dx as i64;
788 let cross_e = e_dir.0 as i64 * dy as i64 - e_dir.1 as i64 * dx as i64;
789 let in_sector = if wide {
790 !(cross_e >= 0 && cross_s <= 0)
793 } else {
794 cross_s >= 0 && cross_e <= 0
795 };
796 if in_sector {
797 hits += 1;
798 }
799 }
800 }
801 let expected = (hits * 255 + 128) / 256;
802 let actual = alpha_of(canvas.at(px, py));
803 let error = expected.abs_diff(actual);
804 assert!(
805 error <= 72,
806 "start {start} sweep {sweep} at {px},{py}: \
807 oracle {expected}, rasteriser {actual}"
808 );
809 if expected == 0 {
810 assert!(
811 actual <= 16,
812 "start {start} sweep {sweep}: painted well outside \
813 the arc at {px},{py}: {actual}"
814 );
815 }
816 if expected == 255 {
817 assert!(
818 actual >= 240,
819 "start {start} sweep {sweep}: hole inside the arc \
820 at {px},{py}: {actual}"
821 );
822 }
823 }
824 }
825 }
826 }
827}