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