use denise::{Point, Rect};
use crate::blend::{Paint, blend_span};
use crate::canvas::Canvas;
use crate::rounded::{ONE, SUB_STEP, SUBSAMPLES, ceil_px, floor_px, to_fx};
pub(crate) const MAX_VERTICES: usize = 32;
pub use denise::MAX_ICON_VERTICES;
struct Crossings {
xs: [i32; MAX_VERTICES],
len: usize,
}
impl Crossings {
fn at(points: &[(i32, i32)], sy: i32) -> Self {
let mut xs = [0i32; MAX_VERTICES];
let mut len = 0;
for i in 0..points.len() {
let (x0, y0) = points[i];
let (x1, y1) = points[(i + 1) % points.len()];
if (y0 <= sy) == (y1 <= sy) {
continue;
}
let t = (sy - y0) as i64 * (x1 - x0) as i64 / (y1 - y0) as i64;
let x = x0 as i64 + t;
if len < MAX_VERTICES {
xs[len] = x as i32;
len += 1;
}
}
for i in 1..len {
let v = xs[i];
let mut j = i;
while j > 0 && xs[j - 1] > v {
xs[j] = xs[j - 1];
j -= 1;
}
xs[j] = v;
}
Self { xs, len }
}
fn overlap(&self, px0: i32) -> i32 {
let px1 = px0 + ONE;
let mut covered = 0;
let mut k = 0;
while k + 1 < self.len {
let l = self.xs[k].max(px0);
let r = self.xs[k + 1].min(px1);
covered += (r - l).max(0);
k += 2;
}
covered
}
}
impl Canvas<'_> {
pub(crate) fn fill_polygon_fx(&mut self, points: &[(i32, i32)], paint: Paint) {
if points.len() < 3 || points.len() > MAX_VERTICES || paint.is_invisible() {
return;
}
let (mut top, mut bottom) = (i32::MAX, i32::MIN);
let (mut left, mut right) = (i32::MAX, i32::MIN);
for &(x, y) in points {
top = top.min(y);
bottom = bottom.max(y);
left = left.min(x);
right = right.max(x);
}
let bbox = Rect::from_edges(
floor_px(left),
floor_px(top),
ceil_px(right) + 1,
ceil_px(bottom) + 1,
);
let Some(vis) = self.visible(bbox) else {
return;
};
for y in vis.y..vis.bottom() {
let mut rows = [const {
Crossings {
xs: [0; MAX_VERTICES],
len: 0,
}
}; SUBSAMPLES];
let mut simple = true;
for (k, row) in rows.iter_mut().enumerate() {
let sy = to_fx(y) + k as i32 * SUB_STEP + SUB_STEP / 2;
*row = Crossings::at(points, sy);
simple &= row.len == 2;
}
let (solid0, solid1) = if simple {
let l = rows.iter().map(|r| r.xs[0]).max().unwrap_or(0);
let r = rows.iter().map(|r| r.xs[1]).min().unwrap_or(0);
(ceil_px(l), floor_px(r))
} else {
(vis.right(), vis.right())
};
for x in vis.x..solid0.min(vis.right()) {
self.blend_at(x, y, paint, coverage(&rows, x));
}
let (s0, s1) = (solid0.max(vis.x), solid1.min(vis.right()));
if s0 < s1
&& let Some(span) = self.row_span(y, s0, s1)
{
blend_span(span, paint);
}
for x in s1.max(vis.x)..vis.right() {
self.blend_at(x, y, paint, coverage(&rows, x));
}
}
}
pub fn fill_star(
&mut self,
centre: Point,
outer_radius: i32,
inner_radius: i32,
points: u32,
rotation: i32,
color: impl Into<Paint>,
) {
crate::painter::Painter::fill_star(
self,
centre,
outer_radius,
inner_radius,
points,
rotation,
color.into(),
);
}
}
fn coverage(rows: &[Crossings; SUBSAMPLES], x: i32) -> u32 {
let px0 = to_fx(x);
let mut covered: i32 = 0;
for row in rows {
covered += row.overlap(px0);
}
let total = ONE as u32 * SUBSAMPLES as u32;
((covered.max(0) as u32 * 255 + total / 2) / total).min(255)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TURN;
use crate::testing::TestCanvas;
use denise::Color;
fn alpha_of(px: u32) -> u32 {
px & 0xFF
}
fn star_vertices(cx: f64, cy: f64, outer: f64, inner: f64, points: usize) -> Vec<(f64, f64)> {
let count = points * 2;
(0..count)
.map(|i| {
let a = i as f64 / count as f64 * core::f64::consts::TAU;
let r = if i % 2 == 0 { outer } else { inner };
(cx + a.sin() * r, cy - a.cos() * r)
})
.collect()
}
fn inside(poly: &[(f64, f64)], x: f64, y: f64) -> bool {
let mut hit = false;
for i in 0..poly.len() {
let (x0, y0) = poly[i];
let (x1, y1) = poly[(i + 1) % poly.len()];
if (y0 > y) != (y1 > y) && x < (x1 - x0) * (y - y0) / (y1 - y0) + x0 {
hit = !hit;
}
}
hit
}
#[test]
fn star_coverage_matches_a_supersampled_oracle() {
const N: i32 = 16;
let (cx, cy, outer, inner) = (24, 24, 20, 8);
let mut t = TestCanvas::new(48, 48);
t.canvas()
.fill_star(Point::new(cx, cy), outer, inner, 5, 0, Color::WHITE);
let poly = star_vertices(cx as f64, cy as f64, outer as f64, inner as f64, 5);
let mut worst = 0u32;
for y in 0..48 {
for x in 0..48 {
let mut hits = 0;
for sy in 0..N {
for sx in 0..N {
let px = x as f64 + (sx as f64 + 0.5) / N as f64;
let py = y as f64 + (sy as f64 + 0.5) / N as f64;
if inside(&poly, px, py) {
hits += 1;
}
}
}
let want = (hits * 255 / (N * N)) as u32;
let got = alpha_of(t.at(x, y));
worst = worst.max(got.abs_diff(want));
}
}
assert!(worst <= 48, "worst pixel differs by {worst}");
}
#[test]
fn every_scanline_crosses_a_polygon_an_even_number_of_times() {
let sub_row = |y: i32, k: i32| to_fx(y) + k * SUB_STEP + SUB_STEP / 2;
let apex = sub_row(10, 0);
let shapes: [&[(i32, i32)]; 3] = [
&[
(to_fx(10), apex),
(to_fx(30), to_fx(30)),
(to_fx(2), to_fx(28)),
],
&[
(to_fx(16), apex),
(to_fx(28), sub_row(20, 2)),
(to_fx(16), to_fx(34)),
(to_fx(4), sub_row(20, 2)),
],
&[
(to_fx(4), apex),
(to_fx(28), apex),
(to_fx(28), sub_row(30, 1)),
(to_fx(4), sub_row(30, 1)),
],
];
for (n, shape) in shapes.iter().enumerate() {
for y in 0..48 {
for k in 0..SUBSAMPLES as i32 {
let c = Crossings::at(shape, sub_row(y, k));
assert!(
c.len.is_multiple_of(2),
"shape {n} at y={y} sub-row {k} crossed {} times",
c.len
);
}
}
}
}
#[test]
fn a_horizontal_edge_never_divides_by_zero() {
let mut t = TestCanvas::new(32, 32);
let flat: &[(i32, i32)] = &[
(to_fx(4), to_fx(8)),
(to_fx(28), to_fx(8)),
(to_fx(28), to_fx(20)),
(to_fx(4), to_fx(20)),
];
t.canvas().fill_polygon_fx(flat, Color::WHITE.into());
assert_eq!(alpha_of(t.at(16, 14)), 255, "the interior must be filled");
assert_eq!(alpha_of(t.at(16, 2)), 0, "and nothing above it");
}
#[test]
fn a_star_has_its_tips_and_its_valleys() {
let mut t = TestCanvas::new(64, 64);
t.canvas()
.fill_star(Point::new(32, 32), 28, 11, 5, 0, Color::WHITE);
assert_eq!(alpha_of(t.at(32, 32)), 255, "the middle must be solid");
assert!(alpha_of(t.at(32, 8)) > 0, "no tip at twelve o'clock");
assert_eq!(alpha_of(t.at(32, 2)), 0, "something past the tip");
for (x, y) in [(4, 4), (59, 4), (4, 59), (59, 59)] {
assert_eq!(alpha_of(t.at(x, y)), 0, "spilled at {x},{y}");
}
}
#[test]
fn a_star_stays_inside_its_radius_at_every_size() {
for radius in [3, 8, 20, 60] {
let mut t = TestCanvas::new(160, 160);
t.canvas().fill_star(
Point::new(80, 80),
radius,
radius * 2 / 5,
5,
0,
Color::WHITE,
);
for y in 0..160i32 {
for x in 0..160i32 {
if alpha_of(t.at(x, y)) == 0 {
continue;
}
let (dx, dy) = ((x - 80) as f64 + 0.5, (y - 80) as f64 + 0.5);
let d = (dx * dx + dy * dy).sqrt();
assert!(
d <= radius as f64 + 1.5,
"radius {radius}: ink at {x},{y} is {d} out"
);
}
}
}
}
#[test]
fn rotation_turns_the_star_and_a_full_turn_returns_it() {
let draw = |rotation| {
let mut t = TestCanvas::new(64, 64);
t.canvas()
.fill_star(Point::new(32, 32), 24, 10, 5, rotation, Color::WHITE);
t
};
fn far_apart(a: &TestCanvas, b: &TestCanvas) -> usize {
a.pixels()
.iter()
.zip(b.pixels())
.filter(|&(&p, &q)| alpha_of(p).abs_diff(alpha_of(q)) > 24)
.count()
}
let zero = draw(0);
assert_eq!(
zero.pixels(),
draw(TURN).pixels(),
"a full turn must be exactly identity"
);
let fifth = far_apart(&zero, &draw(TURN / 5));
assert!(fifth < 40, "five-fold symmetry is off by {fifth} pixels");
let tenth = far_apart(&zero, &draw(TURN / 10));
assert!(
tenth > 10 * fifth.max(1),
"half a step differs by only {tenth} against {fifth}"
);
}
#[test]
fn clipping_a_star_matches_the_unclipped_result() {
let region = Rect::new(20, 20, 24, 24);
let mut full = TestCanvas::new(64, 64);
full.canvas()
.fill_star(Point::new(32, 32), 26, 10, 5, 0, Color::WHITE);
let mut clipped = TestCanvas::new(64, 64);
{
let mut c = clipped.canvas();
c.clip_to(region);
c.fill_star(Point::new(32, 32), 26, 10, 5, 0, Color::WHITE);
}
for y in 0..64 {
for x in 0..64 {
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 an_inner_radius_at_the_outer_one_is_a_convex_polygon() {
let mut t = TestCanvas::new(64, 64);
t.canvas()
.fill_star(Point::new(32, 32), 20, 20, 5, 0, Color::WHITE);
assert_eq!(alpha_of(t.at(32, 32)), 255);
assert_eq!(alpha_of(t.at(32, 14)), 255, "a valley became a notch");
}
#[test]
fn degenerate_stars_draw_nothing_and_nobody_panics() {
let mut t = TestCanvas::new(32, 32);
let mut c = t.canvas();
c.fill_star(Point::new(16, 16), 0, 0, 5, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), -10, 4, 5, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), 10, 20, 5, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), 10, 4, 1, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), 10, 4, 99, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), 10, 4, 5, i32::MIN, Color::WHITE);
c.fill_star(Point::new(1_000_000, 0), 10, 4, 5, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), i32::MAX, 4, 5, 0, Color::WHITE);
c.fill_star(Point::new(16, 16), 10, 4, 5, 0, Color::rgba(255, 0, 0, 0));
}
#[test]
fn an_inner_radius_larger_than_the_outer_is_clamped_not_inverted() {
let mut asked = TestCanvas::new(48, 48);
asked
.canvas()
.fill_star(Point::new(24, 24), 16, 999, 5, 0, Color::WHITE);
let mut clamped = TestCanvas::new(48, 48);
clamped
.canvas()
.fill_star(Point::new(24, 24), 16, 16, 5, 0, Color::WHITE);
assert_eq!(asked.pixels(), clamped.pixels());
}
#[test]
fn alpha_never_doubles_up_anywhere() {
let mut t = TestCanvas::new(64, 64);
t.canvas().fill_star(
Point::new(32, 32),
26,
10,
5,
0,
Color::rgba(255, 255, 255, 128),
);
for y in 0..64 {
for x in 0..64 {
assert!(alpha_of(t.at(x, y)) <= 128, "double-composited at {x},{y}");
}
}
}
}