1use 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
39const UNBOUNDED: i64 = i64::MAX / 4;
42
43fn 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
52fn ceil_div(b: i64, a: i64) -> i64 {
54 -floor_div(-b, a)
55}
56
57fn 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 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
79fn 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
92enum Cut {
94 Keep,
96 Remove,
98}
99
100struct RowSpans {
103 spans: [[(i32, i32); 4]; SUBSAMPLES],
104 counts: [usize; SUBSAMPLES],
105}
106
107impl RowSpans {
108 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
124struct 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 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 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 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 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 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 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 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 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 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
336fn 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 px & 0xFF
357 }
358
359 #[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 #[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 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 #[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 #[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 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 #[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 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 #[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 #[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 assert!(
528 (255..=510).contains(&sum),
529 "the two arcs left a hole at {x},{y}: {sum}"
530 );
531 }
532 }
533 }
534 }
535
536 #[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 #[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 #[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 #[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 #[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 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 !(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}