use denise::{Point, Rect};
use crate::blend::Paint;
use crate::canvas::Canvas;
use crate::rounded::{COORD_LIMIT, ONE, SUB_STEP, SUBSAMPLES, Scan, ceil_px, floor_px, to_fx};
pub const TURN: i32 = 1 << 16;
#[rustfmt::skip]
const SIN_QUARTER: [i32; 257] = [
0, 402, 804, 1206, 1608, 2010, 2412, 2814,
3216, 3617, 4019, 4420, 4821, 5222, 5623, 6023,
6424, 6824, 7224, 7623, 8022, 8421, 8820, 9218,
9616, 10014, 10411, 10808, 11204, 11600, 11996, 12391,
12785, 13180, 13573, 13966, 14359, 14751, 15143, 15534,
15924, 16314, 16703, 17091, 17479, 17867, 18253, 18639,
19024, 19409, 19792, 20175, 20557, 20939, 21320, 21699,
22078, 22457, 22834, 23210, 23586, 23961, 24335, 24708,
25080, 25451, 25821, 26190, 26558, 26925, 27291, 27656,
28020, 28383, 28745, 29106, 29466, 29824, 30182, 30538,
30893, 31248, 31600, 31952, 32303, 32652, 33000, 33347,
33692, 34037, 34380, 34721, 35062, 35401, 35738, 36075,
36410, 36744, 37076, 37407, 37736, 38064, 38391, 38716,
39040, 39362, 39683, 40002, 40320, 40636, 40951, 41264,
41576, 41886, 42194, 42501, 42806, 43110, 43412, 43713,
44011, 44308, 44604, 44898, 45190, 45480, 45769, 46056,
46341, 46624, 46906, 47186, 47464, 47741, 48015, 48288,
48559, 48828, 49095, 49361, 49624, 49886, 50146, 50404,
50660, 50914, 51166, 51417, 51665, 51911, 52156, 52398,
52639, 52878, 53114, 53349, 53581, 53812, 54040, 54267,
54491, 54714, 54934, 55152, 55368, 55582, 55794, 56004,
56212, 56418, 56621, 56823, 57022, 57219, 57414, 57607,
57798, 57986, 58172, 58356, 58538, 58718, 58896, 59071,
59244, 59415, 59583, 59750, 59914, 60075, 60235, 60392,
60547, 60700, 60851, 60999, 61145, 61288, 61429, 61568,
61705, 61839, 61971, 62101, 62228, 62353, 62476, 62596,
62714, 62830, 62943, 63054, 63162, 63268, 63372, 63473,
63572, 63668, 63763, 63854, 63944, 64031, 64115, 64197,
64277, 64354, 64429, 64501, 64571, 64639, 64704, 64766,
64827, 64884, 64940, 64993, 65043, 65091, 65137, 65180,
65220, 65259, 65294, 65328, 65358, 65387, 65413, 65436,
65457, 65476, 65492, 65505, 65516, 65525, 65531, 65535,
65536,
];
fn sin_bam(angle: i32) -> i32 {
let a = angle.rem_euclid(TURN);
let quarter = TURN / 4;
let (quadrant, q) = (a / quarter, a % quarter);
let lookup = |q: i32| -> i32 {
let idx = (q >> 6) as usize;
let frac = q & 63;
if frac == 0 {
SIN_QUARTER[idx]
} else {
SIN_QUARTER[idx] + (SIN_QUARTER[idx + 1] - SIN_QUARTER[idx]) * frac / 64
}
};
match quadrant {
0 => lookup(q),
1 => lookup(quarter - q),
2 => -lookup(q),
_ => -lookup(quarter - q),
}
}
fn direction(angle: i32) -> (i32, i32) {
(sin_bam(angle), -sin_bam(angle + TURN / 4))
}
const UNBOUNDED: i64 = i64::MAX / 4;
fn floor_div(b: i64, a: i64) -> i64 {
if a < 0 {
(-b).div_euclid(-a)
} else {
b.div_euclid(a)
}
}
fn ceil_div(b: i64, a: i64) -> i64 {
-floor_div(-b, a)
}
fn half_plane(d: (i32, i32), ry: i64, keep_ge: bool) -> Option<(i64, i64)> {
let b = d.0 as i64 * ry;
let a = d.1 as i64;
if a == 0 {
let keeps = if keep_ge { b >= 0 } else { b <= 0 };
return keeps.then_some((-UNBOUNDED, UNBOUNDED));
}
let bounded_above = (a > 0) == keep_ge;
if bounded_above {
Some((-UNBOUNDED, floor_div(b, a)))
} else {
Some((ceil_div(b, a), UNBOUNDED))
}
}
fn sector_row(s: (i32, i32), e: (i32, i32), ry: i64) -> Option<(i64, i64)> {
let (lo_a, hi_a) = half_plane(s, ry, true)?;
let (lo_b, hi_b) = half_plane(e, ry, false)?;
let lo = lo_a.max(lo_b);
let hi = hi_a.min(hi_b);
(lo <= hi).then_some((lo, hi))
}
enum Cut {
Keep,
Remove,
}
struct RowSpans {
spans: [[(i32, i32); 4]; SUBSAMPLES],
counts: [usize; SUBSAMPLES],
}
impl RowSpans {
fn coverage(&self, x: i32) -> u32 {
let px0 = to_fx(x);
let px1 = px0 + ONE;
let mut covered: i32 = 0;
for k in 0..SUBSAMPLES {
for &(l, r) in &self.spans[k][..self.counts[k]] {
covered += (r.min(px1) - l.max(px0)).max(0);
}
}
let total = ONE as u32 * SUBSAMPLES as u32;
((covered as u32 * 255 + total / 2) / total).min(255)
}
}
struct Clusters {
runs: [(i32, i32); 16],
count: usize,
}
impl Clusters {
fn new() -> Self {
Self {
runs: [(0, 0); 16],
count: 0,
}
}
fn push(&mut self, from: i32, to: i32) {
if from >= to {
return;
}
let mut from = from;
let mut to = to;
let mut i = 0;
while i < self.count {
let (a, b) = self.runs[i];
if from <= b && to >= a {
from = from.min(a);
to = to.max(b);
self.count -= 1;
self.runs[i] = self.runs[self.count];
} else {
i += 1;
}
}
if self.count < self.runs.len() {
self.runs[self.count] = (from, to);
self.count += 1;
}
}
}
impl Canvas<'_> {
pub fn fill_circle(&mut self, centre: Point, radius: i32, color: impl Into<Paint>) {
let r = radius.clamp(0, COORD_LIMIT);
let square = bounding_square(centre, r);
self.fill_rounded_rect(square, r, color);
}
pub fn stroke_circle(
&mut self,
centre: Point,
radius: i32,
thickness: i32,
color: impl Into<Paint>,
) {
let r = radius.clamp(0, COORD_LIMIT);
let square = bounding_square(centre, r);
self.stroke_rounded_rect(square, r, thickness.min(COORD_LIMIT), color);
}
pub fn stroke_arc(
&mut self,
centre: Point,
radius: i32,
thickness: i32,
start: i32,
sweep: i32,
color: impl Into<Paint>,
) {
let paint = color.into();
let r = radius.clamp(0, COORD_LIMIT);
let t = thickness.clamp(0, COORD_LIMIT).min(r);
if r == 0 || t == 0 || sweep == 0 || paint.is_invisible() {
return;
}
let mut start = start as i64;
let mut sweep = sweep as i64;
if sweep < 0 {
start += sweep;
sweep = -sweep;
}
if sweep >= TURN as i64 {
self.stroke_circle(centre, r, t, paint);
return;
}
let start = start.rem_euclid(TURN as i64) as i32;
let sweep = sweep as i32;
let (cut, from, to) = if sweep <= TURN / 2 {
(Cut::Keep, start, start + sweep)
} else {
(Cut::Remove, start + sweep, start + TURN)
};
let s_dir = direction(from);
let e_dir = direction(to);
let square = bounding_square(centre, r);
let Some(vis) = self.visible(square) else {
return;
};
let inner_r = r - t;
let inner_square = bounding_square(centre, inner_r);
let centre_y = to_fx(centre.y);
let centre_x = to_fx(centre.x) as i64;
for y in vis.y..vis.bottom() {
let outer = Scan::new(square, r, y);
let hole = inner_r > 0 && y >= inner_square.y && y < inner_square.bottom();
let inner = if hole {
Some(Scan::new(inner_square, inner_r, y))
} else {
None
};
let mut row = RowSpans {
spans: [[(0, 0); 4]; SUBSAMPLES],
counts: [0; SUBSAMPLES],
};
let mut clusters = Clusters::new();
for k in 0..SUBSAMPLES {
let sy = to_fx(y) + k as i32 * SUB_STEP + SUB_STEP / 2;
let ry = (sy - centre_y) as i64;
let (ol, or_) = (outer.left[k], outer.right[k]);
if ol >= or_ {
continue;
}
let ring: [(i32, i32); 2] = match &inner {
Some(inner) if inner.left[k] < inner.right[k] => {
[(ol, inner.left[k]), (inner.right[k], or_)]
}
_ => [(ol, or_), (0, 0)],
};
let sector = sector_row(s_dir, e_dir, ry)
.map(|(lo, hi)| (lo.saturating_add(centre_x), hi.saturating_add(centre_x)));
let mut push = |l: i64, r: i64| {
let l = l.clamp(ol as i64, or_ as i64) as i32;
let r = r.clamp(ol as i64, or_ as i64) as i32;
if l < r {
let n = &mut row.counts[k];
row.spans[k][*n] = (l, r);
*n += 1;
clusters.push(floor_px(l), ceil_px(r));
}
};
for &(l, r) in ring.iter().filter(|(l, r)| l < r) {
match (&cut, sector) {
(Cut::Keep, None) => {}
(Cut::Keep, Some((cl, ch))) => {
push((l as i64).max(cl), (r as i64).min(ch));
}
(Cut::Remove, None) => push(l as i64, r as i64),
(Cut::Remove, Some((wl, wh))) => {
push(l as i64, (r as i64).min(wl));
push((l as i64).max(wh), r as i64);
}
}
}
}
let clip = self.clip();
for &(from, to) in &clusters.runs[..clusters.count] {
let from = from.max(clip.x);
let to = to.min(clip.right());
for x in from..to {
self.blend_at(x, y, paint, row.coverage(x));
}
}
}
}
}
fn bounding_square(centre: Point, radius: i32) -> Rect {
Rect::new(
centre.x.saturating_sub(radius),
centre.y.saturating_sub(radius),
radius.saturating_mul(2),
radius.saturating_mul(2),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestCanvas;
use denise::Color;
fn alpha_of(px: u32) -> u32 {
px & 0xFF
}
#[test]
fn every_angle_satisfies_the_pythagorean_identity() {
for a in 0..TURN {
let s = sin_bam(a) as i64;
let c = sin_bam(a + TURN / 4) as i64;
let one = (s * s + c * c) >> 16;
assert!(
(one - 65536).abs() < 64,
"angle {a}: sin²+cos² is {one}, not 65536"
);
}
}
#[test]
fn the_cardinal_directions_are_exact() {
assert_eq!(direction(0), (0, -65536), "twelve o'clock");
assert_eq!(direction(TURN / 4), (65536, 0), "three o'clock");
assert_eq!(direction(TURN / 2), (0, 65536), "six o'clock");
assert_eq!(direction(3 * TURN / 4), (-65536, 0), "nine o'clock");
assert_eq!(direction(TURN), (0, -65536), "and round again");
assert_eq!(direction(-TURN / 4), (-65536, 0), "negative wraps too");
}
#[test]
fn sine_rises_monotonically_over_the_first_quarter() {
let mut previous = -1;
for a in 0..=TURN / 4 {
let s = sin_bam(a);
assert!(s >= previous, "sin fell at angle {a}");
previous = s;
}
}
#[test]
fn a_circle_is_exactly_a_fully_rounded_square() {
let mut circle = TestCanvas::new(48, 48);
circle
.canvas()
.fill_circle(Point::new(24, 24), 20, Color::WHITE);
let mut square = TestCanvas::new(48, 48);
square
.canvas()
.fill_rounded_rect(Rect::new(4, 4, 40, 40), 20, Color::WHITE);
assert_eq!(circle.pixels(), square.pixels());
let mut ring = TestCanvas::new(48, 48);
ring.canvas()
.stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
let mut band = TestCanvas::new(48, 48);
band.canvas()
.stroke_rounded_rect(Rect::new(4, 4, 40, 40), 20, 4, Color::WHITE);
assert_eq!(ring.pixels(), band.pixels());
}
#[test]
fn a_full_sweep_matches_the_circle_exactly() {
for start in [0, TURN / 8, -TURN / 3] {
let mut arc = TestCanvas::new(48, 48);
arc.canvas()
.stroke_arc(Point::new(24, 24), 20, 4, start, TURN, Color::WHITE);
let mut circle = TestCanvas::new(48, 48);
circle
.canvas()
.stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
assert_eq!(arc.pixels(), circle.pixels(), "start {start}");
}
let mut over = TestCanvas::new(48, 48);
over.canvas()
.stroke_arc(Point::new(24, 24), 20, 4, 0, TURN * 3, Color::WHITE);
let mut circle = TestCanvas::new(48, 48);
circle
.canvas()
.stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
assert_eq!(over.pixels(), circle.pixels());
}
#[test]
fn a_zero_sweep_draws_nothing() {
let mut t = TestCanvas::new(48, 48);
t.canvas()
.stroke_arc(Point::new(24, 24), 20, 4, TURN / 8, 0, Color::WHITE);
assert!(t.pixels().iter().all(|&px| px == 0));
}
#[test]
fn a_quarter_arc_stays_in_its_quadrant() {
let (cx, cy) = (24, 24);
let mut t = TestCanvas::new(48, 48);
t.canvas()
.stroke_arc(Point::new(cx, cy), 20, 4, 0, TURN / 4, Color::WHITE);
let mut painted = 0;
for y in 0..48 {
for x in 0..48 {
if alpha_of(t.at(x, y)) > 0 {
painted += 1;
assert!(
x >= cx - 1 && y <= cy,
"quarter arc escaped its quadrant at {x},{y}"
);
}
}
}
assert!(painted > 50, "only {painted} pixels for a quarter arc");
assert!(alpha_of(t.at(cx, cy - 20 + 2)) > 0, "no paint at the start");
assert!(
alpha_of(t.at(cx + 20 - 2, cy - 1)) > 0,
"no paint at the end"
);
}
#[test]
fn a_sweep_across_the_wrap_point_paints_both_sides_of_the_top() {
let (cx, cy) = (24, 24);
let mut t = TestCanvas::new(48, 48);
t.canvas().stroke_arc(
Point::new(cx, cy),
20,
4,
7 * TURN / 8,
TURN / 4,
Color::WHITE,
);
assert!(alpha_of(t.at(cx - 8, cy - 17)) > 0, "left of the top");
assert!(alpha_of(t.at(cx + 8, cy - 17)) > 0, "right of the top");
assert_eq!(alpha_of(t.at(cx, cy + 18)), 0, "nothing at the bottom");
assert_eq!(alpha_of(t.at(cx - 18, cy)), 0, "nothing at nine o'clock");
assert_eq!(alpha_of(t.at(cx + 18, cy)), 0, "nothing at three o'clock");
}
#[test]
fn a_negative_sweep_goes_the_other_way() {
let mut negative = TestCanvas::new(48, 48);
negative
.canvas()
.stroke_arc(Point::new(24, 24), 20, 4, 0, -TURN / 4, Color::WHITE);
let mut positive = TestCanvas::new(48, 48);
positive.canvas().stroke_arc(
Point::new(24, 24),
20,
4,
3 * TURN / 4,
TURN / 4,
Color::WHITE,
);
assert_eq!(negative.pixels(), positive.pixels());
}
#[test]
fn a_wide_arc_and_its_complement_tile_the_ring() {
let mut wide = TestCanvas::new(48, 48);
wide.canvas().stroke_arc(
Point::new(24, 24),
20,
4,
TURN / 4,
3 * TURN / 4,
Color::WHITE,
);
let mut narrow = TestCanvas::new(48, 48);
narrow
.canvas()
.stroke_arc(Point::new(24, 24), 20, 4, 0, TURN / 4, Color::WHITE);
let mut circle = TestCanvas::new(48, 48);
circle
.canvas()
.stroke_circle(Point::new(24, 24), 20, 4, Color::WHITE);
for y in 0..48 {
for x in 0..48 {
let whole = alpha_of(circle.at(x, y));
let sum = alpha_of(wide.at(x, y)) + alpha_of(narrow.at(x, y));
if whole == 0 {
assert_eq!(sum, 0, "painted outside the ring at {x},{y}");
} else if whole == 255 {
assert!(
(255..=510).contains(&sum),
"the two arcs left a hole at {x},{y}: {sum}"
);
}
}
}
}
#[test]
fn thickness_decides_between_a_ring_and_a_pie() {
let mut ring = TestCanvas::new(48, 48);
ring.canvas()
.stroke_arc(Point::new(24, 24), 20, 4, 0, TURN / 2, Color::WHITE);
assert_eq!(alpha_of(ring.at(24, 24)), 0, "ring centre must be empty");
assert_eq!(alpha_of(ring.at(30, 24)), 0, "ring interior must be empty");
let mut pie = TestCanvas::new(48, 48);
pie.canvas()
.stroke_arc(Point::new(24, 24), 20, 99, 0, TURN / 2, Color::WHITE);
assert_eq!(alpha_of(pie.at(30, 24)), 255, "pie interior must be solid");
assert_eq!(alpha_of(pie.at(17, 24)), 0, "outside the pie's half");
}
#[test]
fn translucent_arcs_never_composite_a_pixel_twice() {
for sweep in [TURN / 4, TURN / 2, 3 * TURN / 4, TURN - TURN / 16] {
let mut t = TestCanvas::new(48, 48);
t.canvas().stroke_arc(
Point::new(24, 24),
20,
4,
TURN / 16,
sweep,
Color::rgba(255, 255, 255, 128),
);
let ceiling = 128;
for y in 0..48 {
for x in 0..48 {
assert!(
alpha_of(t.at(x, y)) <= ceiling,
"double-composited at {x},{y} with sweep {sweep}"
);
}
}
}
}
#[test]
fn clipping_an_arc_matches_the_unclipped_result() {
let region = Rect::new(10, 6, 20, 22);
let mut full = TestCanvas::new(48, 48);
full.canvas()
.stroke_arc(Point::new(24, 24), 18, 5, 0, 3 * TURN / 4, Color::WHITE);
let mut clipped = TestCanvas::new(48, 48);
{
let mut c = clipped.canvas();
c.clip_to(region);
c.stroke_arc(Point::new(24, 24), 18, 5, 0, 3 * TURN / 4, Color::WHITE);
}
for y in 0..48 {
for x in 0..48 {
let expected = if region.contains(Point::new(x, y)) {
full.at(x, y)
} else {
0
};
assert_eq!(clipped.at(x, y), expected, "at {x},{y}");
}
}
}
#[test]
fn degenerate_arcs_do_not_panic() {
let mut t = TestCanvas::new(16, 16);
let mut c = t.canvas();
c.stroke_arc(Point::new(8, 8), 0, 4, 0, TURN, Color::WHITE);
c.stroke_arc(Point::new(8, 8), 6, 0, 0, TURN, Color::WHITE);
c.stroke_arc(Point::new(8, 8), -5, 3, 0, TURN, Color::WHITE);
c.stroke_arc(Point::new(8, 8), 6, -2, 0, TURN, Color::WHITE);
c.stroke_arc(Point::new(8, 8), 6, 3, i32::MIN, i32::MIN, Color::WHITE);
c.stroke_arc(Point::new(8, 8), 6, 3, i32::MAX, i32::MAX, Color::WHITE);
c.stroke_arc(
Point::new(i32::MIN, i32::MAX),
i32::MAX,
i32::MAX,
1,
1,
Color::WHITE,
);
c.fill_circle(Point::new(8, 8), 0, Color::WHITE);
c.fill_circle(Point::new(-1000, 8), i32::MAX, Color::WHITE);
c.stroke_circle(Point::new(8, 8), 6, i32::MAX, Color::WHITE);
}
#[test]
fn coverage_agrees_with_a_supersampled_oracle() {
let (cx, cy) = (24, 24);
let (r, t) = (18, 5);
for (start, sweep) in [
(0, TURN / 4),
(TURN / 8, TURN / 2),
(7 * TURN / 8, TURN / 4),
(TURN / 4, 3 * TURN / 4),
(0, TURN / 2),
] {
let mut canvas = TestCanvas::new(48, 48);
canvas
.canvas()
.stroke_arc(Point::new(cx, cy), r, t, start, sweep, Color::WHITE);
let s_dir = direction(start);
let e_dir = direction(start + sweep);
let wide = sweep > TURN / 2;
for py in 0..48 {
for px in 0..48 {
let mut hits = 0u32;
for sy in 0..16 {
for sx in 0..16 {
let dx = (px - cx) * 32 + sx * 2 + 1;
let dy = (py - cy) * 32 + sy * 2 + 1;
let d2 = (dx as i64) * (dx as i64) + (dy as i64) * (dy as i64);
let outer = (r as i64 * 32).pow(2);
let inner = ((r - t) as i64 * 32).pow(2);
if d2 > outer || d2 <= inner {
continue;
}
let cross_s = s_dir.0 as i64 * dy as i64 - s_dir.1 as i64 * dx as i64;
let cross_e = e_dir.0 as i64 * dy as i64 - e_dir.1 as i64 * dx as i64;
let in_sector = if wide {
!(cross_e >= 0 && cross_s <= 0)
} else {
cross_s >= 0 && cross_e <= 0
};
if in_sector {
hits += 1;
}
}
}
let expected = (hits * 255 + 128) / 256;
let actual = alpha_of(canvas.at(px, py));
let error = expected.abs_diff(actual);
assert!(
error <= 72,
"start {start} sweep {sweep} at {px},{py}: \
oracle {expected}, rasteriser {actual}"
);
if expected == 0 {
assert!(
actual <= 16,
"start {start} sweep {sweep}: painted well outside \
the arc at {px},{py}: {actual}"
);
}
if expected == 255 {
assert!(
actual >= 240,
"start {start} sweep {sweep}: hole inside the arc \
at {px},{py}: {actual}"
);
}
}
}
}
}
}