1use denise::{Point, Rect};
25
26use crate::arc::{TURN, direction};
27use crate::blend::{Paint, blend_span};
28use crate::canvas::Canvas;
29use crate::rounded::{COORD_LIMIT, ONE, SUB_STEP, SUBSAMPLES, ceil_px, floor_px, to_fx};
30
31pub(crate) const MAX_VERTICES: usize = 32;
35
36struct Crossings {
38 xs: [i32; MAX_VERTICES],
39 len: usize,
40}
41
42impl Crossings {
43 fn at(points: &[(i32, i32)], sy: i32) -> Self {
49 let mut xs = [0i32; MAX_VERTICES];
50 let mut len = 0;
51 for i in 0..points.len() {
52 let (x0, y0) = points[i];
53 let (x1, y1) = points[(i + 1) % points.len()];
54 if (y0 <= sy) == (y1 <= sy) {
55 continue;
56 }
57 let t = (sy - y0) as i64 * (x1 - x0) as i64 / (y1 - y0) as i64;
59 let x = x0 as i64 + t;
60 if len < MAX_VERTICES {
61 xs[len] = x as i32;
62 len += 1;
63 }
64 }
65 for i in 1..len {
68 let v = xs[i];
69 let mut j = i;
70 while j > 0 && xs[j - 1] > v {
71 xs[j] = xs[j - 1];
72 j -= 1;
73 }
74 xs[j] = v;
75 }
76 Self { xs, len }
77 }
78
79 fn overlap(&self, px0: i32) -> i32 {
82 let px1 = px0 + ONE;
83 let mut covered = 0;
84 let mut k = 0;
85 while k + 1 < self.len {
86 let l = self.xs[k].max(px0);
87 let r = self.xs[k + 1].min(px1);
88 covered += (r - l).max(0);
89 k += 2;
90 }
91 covered
92 }
93}
94
95impl Canvas<'_> {
96 pub(crate) fn fill_polygon_fx(&mut self, points: &[(i32, i32)], paint: Paint) {
100 if points.len() < 3 || points.len() > MAX_VERTICES || paint.is_invisible() {
101 return;
102 }
103 let (mut top, mut bottom) = (i32::MAX, i32::MIN);
104 let (mut left, mut right) = (i32::MAX, i32::MIN);
105 for &(x, y) in points {
106 top = top.min(y);
107 bottom = bottom.max(y);
108 left = left.min(x);
109 right = right.max(x);
110 }
111 let bbox = Rect::from_edges(
112 floor_px(left),
113 floor_px(top),
114 ceil_px(right) + 1,
115 ceil_px(bottom) + 1,
116 );
117 let Some(vis) = self.visible(bbox) else {
118 return;
119 };
120
121 for y in vis.y..vis.bottom() {
122 let mut rows = [const {
123 Crossings {
124 xs: [0; MAX_VERTICES],
125 len: 0,
126 }
127 }; SUBSAMPLES];
128 let mut simple = true;
129 for (k, row) in rows.iter_mut().enumerate() {
130 let sy = to_fx(y) + k as i32 * SUB_STEP + SUB_STEP / 2;
131 *row = Crossings::at(points, sy);
132 simple &= row.len == 2;
133 }
134
135 let (solid0, solid1) = if simple {
140 let l = rows.iter().map(|r| r.xs[0]).max().unwrap_or(0);
141 let r = rows.iter().map(|r| r.xs[1]).min().unwrap_or(0);
142 (ceil_px(l), floor_px(r))
143 } else {
144 (vis.right(), vis.right())
145 };
146
147 for x in vis.x..solid0.min(vis.right()) {
148 self.blend_at(x, y, paint, coverage(&rows, x));
149 }
150 let (s0, s1) = (solid0.max(vis.x), solid1.min(vis.right()));
151 if s0 < s1
152 && let Some(span) = self.row_span(y, s0, s1)
153 {
154 blend_span(span, paint);
155 }
156 for x in s1.max(vis.x)..vis.right() {
157 self.blend_at(x, y, paint, coverage(&rows, x));
158 }
159 }
160 }
161
162 pub fn fill_star(
186 &mut self,
187 centre: Point,
188 outer_radius: i32,
189 inner_radius: i32,
190 points: u32,
191 rotation: i32,
192 color: impl Into<Paint>,
193 ) {
194 let paint = color.into();
195 if points < 2 || outer_radius <= 0 || paint.is_invisible() {
196 return;
197 }
198 let count = (points as usize) * 2;
199 if count > MAX_VERTICES {
200 return;
201 }
202 let outer = outer_radius.clamp(0, COORD_LIMIT) as i64;
203 let inner = inner_radius.clamp(0, outer_radius) as i64;
204
205 let mut vertices = [(0i32, 0i32); MAX_VERTICES];
206 let (cx, cy) = (to_fx(centre.x), to_fx(centre.y));
207 for (i, vertex) in vertices.iter_mut().enumerate().take(count) {
208 let step = (i as i64 * TURN as i64 + count as i64 / 2) / count as i64;
211 let angle = rotation.wrapping_add(step as i32);
212 let (dx, dy) = direction(angle);
213 let r = if i % 2 == 0 { outer } else { inner };
214 *vertex = (
217 cx + ((dx as i64 * r) >> 8) as i32,
218 cy + ((dy as i64 * r) >> 8) as i32,
219 );
220 }
221 self.fill_polygon_fx(&vertices[..count], paint);
222 }
223}
224
225fn coverage(rows: &[Crossings; SUBSAMPLES], x: i32) -> u32 {
227 let px0 = to_fx(x);
228 let mut covered: i32 = 0;
229 for row in rows {
230 covered += row.overlap(px0);
231 }
232 let total = ONE as u32 * SUBSAMPLES as u32;
235 ((covered.max(0) as u32 * 255 + total / 2) / total).min(255)
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::testing::TestCanvas;
242 use denise::Color;
243
244 fn alpha_of(px: u32) -> u32 {
245 px & 0xFF
246 }
247
248 fn star_vertices(cx: f64, cy: f64, outer: f64, inner: f64, points: usize) -> Vec<(f64, f64)> {
251 let count = points * 2;
252 (0..count)
253 .map(|i| {
254 let a = i as f64 / count as f64 * core::f64::consts::TAU;
255 let r = if i % 2 == 0 { outer } else { inner };
256 (cx + a.sin() * r, cy - a.cos() * r)
257 })
258 .collect()
259 }
260
261 fn inside(poly: &[(f64, f64)], x: f64, y: f64) -> bool {
263 let mut hit = false;
264 for i in 0..poly.len() {
265 let (x0, y0) = poly[i];
266 let (x1, y1) = poly[(i + 1) % poly.len()];
267 if (y0 > y) != (y1 > y) && x < (x1 - x0) * (y - y0) / (y1 - y0) + x0 {
268 hit = !hit;
269 }
270 }
271 hit
272 }
273
274 #[test]
278 fn star_coverage_matches_a_supersampled_oracle() {
279 const N: i32 = 16;
280 let (cx, cy, outer, inner) = (24, 24, 20, 8);
281 let mut t = TestCanvas::new(48, 48);
282 t.canvas()
283 .fill_star(Point::new(cx, cy), outer, inner, 5, 0, Color::WHITE);
284
285 let poly = star_vertices(cx as f64, cy as f64, outer as f64, inner as f64, 5);
286 let mut worst = 0u32;
287 for y in 0..48 {
288 for x in 0..48 {
289 let mut hits = 0;
290 for sy in 0..N {
291 for sx in 0..N {
292 let px = x as f64 + (sx as f64 + 0.5) / N as f64;
293 let py = y as f64 + (sy as f64 + 0.5) / N as f64;
294 if inside(&poly, px, py) {
295 hits += 1;
296 }
297 }
298 }
299 let want = (hits * 255 / (N * N)) as u32;
300 let got = alpha_of(t.at(x, y));
301 worst = worst.max(got.abs_diff(want));
302 }
303 }
304 assert!(worst <= 48, "worst pixel differs by {worst}");
307 }
308
309 #[test]
319 fn every_scanline_crosses_a_polygon_an_even_number_of_times() {
320 let sub_row = |y: i32, k: i32| to_fx(y) + k * SUB_STEP + SUB_STEP / 2;
321
322 let apex = sub_row(10, 0);
326 let shapes: [&[(i32, i32)]; 3] = [
327 &[
328 (to_fx(10), apex),
329 (to_fx(30), to_fx(30)),
330 (to_fx(2), to_fx(28)),
331 ],
332 &[
333 (to_fx(16), apex),
334 (to_fx(28), sub_row(20, 2)),
335 (to_fx(16), to_fx(34)),
336 (to_fx(4), sub_row(20, 2)),
337 ],
338 &[
339 (to_fx(4), apex),
340 (to_fx(28), apex),
341 (to_fx(28), sub_row(30, 1)),
342 (to_fx(4), sub_row(30, 1)),
343 ],
344 ];
345
346 for (n, shape) in shapes.iter().enumerate() {
347 for y in 0..48 {
348 for k in 0..SUBSAMPLES as i32 {
349 let c = Crossings::at(shape, sub_row(y, k));
350 assert!(
351 c.len.is_multiple_of(2),
352 "shape {n} at y={y} sub-row {k} crossed {} times",
353 c.len
354 );
355 }
356 }
357 }
358 }
359
360 #[test]
363 fn a_horizontal_edge_never_divides_by_zero() {
364 let mut t = TestCanvas::new(32, 32);
365 let flat: &[(i32, i32)] = &[
366 (to_fx(4), to_fx(8)),
367 (to_fx(28), to_fx(8)),
368 (to_fx(28), to_fx(20)),
369 (to_fx(4), to_fx(20)),
370 ];
371 t.canvas().fill_polygon_fx(flat, Color::WHITE.into());
372 assert_eq!(alpha_of(t.at(16, 14)), 255, "the interior must be filled");
373 assert_eq!(alpha_of(t.at(16, 2)), 0, "and nothing above it");
374 }
375
376 #[test]
377 fn a_star_has_its_tips_and_its_valleys() {
378 let mut t = TestCanvas::new(64, 64);
379 t.canvas()
380 .fill_star(Point::new(32, 32), 28, 11, 5, 0, Color::WHITE);
381 assert_eq!(alpha_of(t.at(32, 32)), 255, "the middle must be solid");
382 assert!(alpha_of(t.at(32, 8)) > 0, "no tip at twelve o'clock");
384 assert_eq!(alpha_of(t.at(32, 2)), 0, "something past the tip");
385 for (x, y) in [(4, 4), (59, 4), (4, 59), (59, 59)] {
387 assert_eq!(alpha_of(t.at(x, y)), 0, "spilled at {x},{y}");
388 }
389 }
390
391 #[test]
392 fn a_star_stays_inside_its_radius_at_every_size() {
393 for radius in [3, 8, 20, 60] {
394 let mut t = TestCanvas::new(160, 160);
395 t.canvas().fill_star(
396 Point::new(80, 80),
397 radius,
398 radius * 2 / 5,
399 5,
400 0,
401 Color::WHITE,
402 );
403 for y in 0..160i32 {
404 for x in 0..160i32 {
405 if alpha_of(t.at(x, y)) == 0 {
406 continue;
407 }
408 let (dx, dy) = ((x - 80) as f64 + 0.5, (y - 80) as f64 + 0.5);
409 let d = (dx * dx + dy * dy).sqrt();
410 assert!(
411 d <= radius as f64 + 1.5,
412 "radius {radius}: ink at {x},{y} is {d} out"
413 );
414 }
415 }
416 }
417 }
418
419 #[test]
420 fn rotation_turns_the_star_and_a_full_turn_returns_it() {
421 let draw = |rotation| {
422 let mut t = TestCanvas::new(64, 64);
423 t.canvas()
424 .fill_star(Point::new(32, 32), 24, 10, 5, rotation, Color::WHITE);
425 t
426 };
427 fn far_apart(a: &TestCanvas, b: &TestCanvas) -> usize {
429 a.pixels()
430 .iter()
431 .zip(b.pixels())
432 .filter(|&(&p, &q)| alpha_of(p).abs_diff(alpha_of(q)) > 24)
433 .count()
434 }
435
436 let zero = draw(0);
437 assert_eq!(
438 zero.pixels(),
439 draw(TURN).pixels(),
440 "a full turn must be exactly identity"
441 );
442
443 let fifth = far_apart(&zero, &draw(TURN / 5));
447 assert!(fifth < 40, "five-fold symmetry is off by {fifth} pixels");
448
449 let tenth = far_apart(&zero, &draw(TURN / 10));
452 assert!(
453 tenth > 10 * fifth.max(1),
454 "half a step differs by only {tenth} against {fifth}"
455 );
456 }
457
458 #[test]
459 fn clipping_a_star_matches_the_unclipped_result() {
460 let region = Rect::new(20, 20, 24, 24);
461 let mut full = TestCanvas::new(64, 64);
462 full.canvas()
463 .fill_star(Point::new(32, 32), 26, 10, 5, 0, Color::WHITE);
464
465 let mut clipped = TestCanvas::new(64, 64);
466 {
467 let mut c = clipped.canvas();
468 c.clip_to(region);
469 c.fill_star(Point::new(32, 32), 26, 10, 5, 0, Color::WHITE);
470 }
471 for y in 0..64 {
472 for x in 0..64 {
473 let expected = if region.contains(Point::new(x, y)) {
474 full.at(x, y)
475 } else {
476 0
477 };
478 assert_eq!(clipped.at(x, y), expected, "at {x},{y}");
479 }
480 }
481 }
482
483 #[test]
484 fn an_inner_radius_at_the_outer_one_is_a_convex_polygon() {
485 let mut t = TestCanvas::new(64, 64);
488 t.canvas()
489 .fill_star(Point::new(32, 32), 20, 20, 5, 0, Color::WHITE);
490 assert_eq!(alpha_of(t.at(32, 32)), 255);
491 assert_eq!(alpha_of(t.at(32, 14)), 255, "a valley became a notch");
492 }
493
494 #[test]
495 fn degenerate_stars_draw_nothing_and_nobody_panics() {
496 let mut t = TestCanvas::new(32, 32);
497 let mut c = t.canvas();
498 c.fill_star(Point::new(16, 16), 0, 0, 5, 0, Color::WHITE);
499 c.fill_star(Point::new(16, 16), -10, 4, 5, 0, Color::WHITE);
500 c.fill_star(Point::new(16, 16), 10, 20, 5, 0, Color::WHITE);
501 c.fill_star(Point::new(16, 16), 10, 4, 1, 0, Color::WHITE);
502 c.fill_star(Point::new(16, 16), 10, 4, 99, 0, Color::WHITE);
503 c.fill_star(Point::new(16, 16), 10, 4, 5, i32::MIN, Color::WHITE);
504 c.fill_star(Point::new(1_000_000, 0), 10, 4, 5, 0, Color::WHITE);
505 c.fill_star(Point::new(16, 16), i32::MAX, 4, 5, 0, Color::WHITE);
506 c.fill_star(Point::new(16, 16), 10, 4, 5, 0, Color::rgba(255, 0, 0, 0));
507 }
508
509 #[test]
510 fn an_inner_radius_larger_than_the_outer_is_clamped_not_inverted() {
511 let mut asked = TestCanvas::new(48, 48);
514 asked
515 .canvas()
516 .fill_star(Point::new(24, 24), 16, 999, 5, 0, Color::WHITE);
517 let mut clamped = TestCanvas::new(48, 48);
518 clamped
519 .canvas()
520 .fill_star(Point::new(24, 24), 16, 16, 5, 0, Color::WHITE);
521 assert_eq!(asked.pixels(), clamped.pixels());
522 }
523
524 #[test]
525 fn alpha_never_doubles_up_anywhere() {
526 let mut t = TestCanvas::new(64, 64);
529 t.canvas().fill_star(
530 Point::new(32, 32),
531 26,
532 10,
533 5,
534 0,
535 Color::rgba(255, 255, 255, 128),
536 );
537 for y in 0..64 {
538 for x in 0..64 {
539 assert!(alpha_of(t.at(x, y)) <= 128, "double-composited at {x},{y}");
540 }
541 }
542 }
543}