1use crate::font;
7use crate::geometry::{Point, Rect, Shape, Size};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Color {
11 pub r: u8,
12 pub g: u8,
13 pub b: u8,
14}
15
16impl Color {
17 pub const WHITE: Self = Self {
18 r: 255,
19 g: 255,
20 b: 255,
21 };
22
23 pub const fn to_0rgb(self) -> u32 {
24 ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32)
25 }
26}
27
28pub struct Canvas<'a> {
29 px: &'a mut [u32],
30 pub w: i32,
31 pub h: i32,
32}
33
34impl<'a> Canvas<'a> {
35 pub fn new(px: &'a mut [u32], w: i32, h: i32) -> Self {
37 assert!(w > 0 && h > 0, "canvas dimensions must be positive");
38 assert_eq!(
39 px.len(),
40 (w as usize) * (h as usize),
41 "buffer size mismatch"
42 );
43 Self { px, w, h }
44 }
45
46 fn set(&mut self, x: i32, y: i32, color: u32) {
47 if x >= 0 && y >= 0 && x < self.w && y < self.h {
48 self.px[(y as usize) * (self.w as usize) + (x as usize)] = color;
49 }
50 }
51
52 pub fn fill_rect(&mut self, rect: Rect, color: Color) {
53 let c = color.to_0rgb();
54 let x0 = rect.x.max(0);
55 let y0 = rect.y.max(0);
56 let x1 = (rect.x + rect.w).min(self.w);
57 let y1 = (rect.y + rect.h).min(self.h);
58 if x1 <= x0 || y1 <= y0 {
59 return;
60 }
61 for y in y0..y1 {
62 let row = (y as usize) * (self.w as usize);
63 self.px[row + x0 as usize..row + x1 as usize].fill(c);
64 }
65 }
66
67 pub fn dim_rect(&mut self, rect: Rect, strength: u32) {
71 let x0 = rect.x.max(0);
72 let y0 = rect.y.max(0);
73 let x1 = (rect.x + rect.w).min(self.w);
74 let y1 = (rect.y + rect.h).min(self.h);
75 for y in y0..y1 {
76 let row = (y as usize) * (self.w as usize);
77 for px in &mut self.px[row + x0 as usize..row + x1.max(x0) as usize] {
78 let r = (((*px >> 16) & 0xFF) * strength) >> 8;
79 let g = (((*px >> 8) & 0xFF) * strength) >> 8;
80 let b = ((*px & 0xFF) * strength) >> 8;
81 *px = (r << 16) | (g << 8) | b;
82 }
83 }
84 }
85
86 pub fn draw_rect_outline(&mut self, rect: Rect, color: Color, thickness: i32) {
87 if thickness <= 0 {
88 return;
89 }
90 let t = thickness.min(rect.w).min(rect.h);
91 self.fill_rect(Rect::new(rect.x, rect.y, rect.w, t), color);
92 self.fill_rect(Rect::new(rect.x, rect.y + rect.h - t, rect.w, t), color);
93 self.fill_rect(Rect::new(rect.x, rect.y, t, rect.h), color);
94 self.fill_rect(Rect::new(rect.x + rect.w - t, rect.y, t, rect.h), color);
95 }
96
97 pub fn fill_circle(&mut self, cx: i32, cy: i32, r: i32, color: Color) {
98 self.circle_band(cx, cy, 0, r, color);
99 }
100
101 pub fn draw_circle_outline(&mut self, cx: i32, cy: i32, r: i32, color: Color, thickness: i32) {
102 if thickness <= 0 {
103 return;
104 }
105 self.circle_band(cx, cy, (r - thickness).max(0), r, color);
106 }
107
108 fn circle_band(&mut self, cx: i32, cy: i32, inner: i32, outer: i32, color: Color) {
111 if outer <= 0 {
112 return;
113 }
114 let c = color.to_0rgb();
115 let inner2 = i64::from(inner) * i64::from(inner);
116 let outer2 = i64::from(outer) * i64::from(outer);
117 for y in (cy - outer).max(0)..=(cy + outer).min(self.h - 1) {
118 for x in (cx - outer).max(0)..=(cx + outer).min(self.w - 1) {
119 let dx = i64::from(x - cx);
120 let dy = i64::from(y - cy);
121 let d2 = dx * dx + dy * dy;
122 if d2 <= outer2 && (inner == 0 || d2 > inner2) {
123 self.set(x, y, c);
124 }
125 }
126 }
127 }
128
129 pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color, scale: i32) {
133 let advance = font::advance(scale);
134 let baseline = y + font::ascent(scale);
135 let mut pen_x = x;
136 for ch in text.chars() {
137 self.blend_glyph(pen_x, baseline, ch, color, scale);
138 pen_x += advance;
139 }
140 }
141
142 fn blend_glyph(&mut self, pen_x: i32, baseline: i32, ch: char, color: Color, scale: i32) {
144 let (metrics, coverage) = font::rasterize(ch, scale);
145 let x0 = pen_x + metrics.xmin;
146 let y0 = baseline - metrics.height as i32 - metrics.ymin;
147 for row in 0..metrics.height {
148 for col in 0..metrics.width {
149 let alpha = coverage[row * metrics.width + col];
150 if alpha != 0 {
151 self.blend(x0 + col as i32, y0 + row as i32, color, alpha);
152 }
153 }
154 }
155 }
156
157 fn blend(&mut self, col: i32, row: i32, color: Color, alpha: u8) {
159 if col < 0 || row < 0 || col >= self.w || row >= self.h {
160 return;
161 }
162 let index = (row as usize) * (self.w as usize) + (col as usize);
163 let dst = self.px[index];
164 let cover = u32::from(alpha);
165 let keep = 255 - cover;
166 let red = (u32::from(color.r) * cover + ((dst >> 16) & 0xFF) * keep) / 255;
167 let green = (u32::from(color.g) * cover + ((dst >> 8) & 0xFF) * keep) / 255;
168 let blue = (u32::from(color.b) * cover + (dst & 0xFF) * keep) / 255;
169 self.px[index] = (red << 16) | (green << 8) | blue;
170 }
171
172 fn draw_ellipse_rotated(&mut self, shape: &Shape, deg: i32, color: Color, band: Option<i32>) {
175 let Shape::Ellipse { cx, cy, rx, ry } = *shape else {
176 return;
177 };
178 let (thickness, fill) = band.map_or((0, true), |t| (t, false));
179 let bb = shape.rotated_bbox(deg);
180 let c = color.to_0rgb();
181 let (irx, iry) = if fill {
182 (-1, -1)
183 } else {
184 ((rx - thickness).max(0), (ry - thickness).max(0))
185 };
186 for y in bb.y.max(0)..=bb.y.saturating_add(bb.h).min(self.h - 1) {
187 for x in bb.x.max(0)..=bb.x.saturating_add(bb.w).min(self.w - 1) {
188 let local =
189 crate::geometry::rotate_point_about(Point::new(x, y), Point::new(cx, cy), -deg);
190 let (dx, dy) = (local.x - cx, local.y - cy);
191 if ellipse_covers(dx, dy, rx, ry) && !ellipse_covers(dx, dy, irx, iry) {
192 self.set(x, y, c);
193 }
194 }
195 }
196 }
197
198 pub fn draw_shape_rotated(
203 &mut self,
204 shape: &Shape,
205 deg: i32,
206 color: Color,
207 thickness: i32,
208 fill: bool,
209 ) {
210 let deg = crate::geometry::normalize_deg(deg);
211 if deg == 0 || matches!(shape, Shape::Circle { .. }) {
212 return self.draw_shape(shape, color, thickness, fill);
213 }
214 if matches!(shape, Shape::Triangle { .. } | Shape::Poly { .. }) {
215 return self.draw_shape(&shape.with_rotation_baked(deg), color, thickness, fill);
216 }
217 if matches!(shape, Shape::Ellipse { .. }) {
218 let band = (!fill).then_some(thickness);
219 return self.draw_ellipse_rotated(shape, deg, color, band);
220 }
221 let Shape::Rect(rect) = *shape else { return };
222 if !fill && thickness <= 0 {
223 return;
224 }
225 let c = color.to_0rgb();
226 let bb = shape.rotated_bbox(deg);
227 let band = f64::from(thickness);
228 let (x0, y0) = (f64::from(rect.x), f64::from(rect.y));
229 let (x1, y1) = (f64::from(rect.x + rect.w), f64::from(rect.y + rect.h));
230 let pivot_x = f64::from(rect.x) + f64::from(rect.w) / 2.0;
235 let pivot_y = f64::from(rect.y) + f64::from(rect.h) / 2.0;
236 let rad = f64::from(-deg).to_radians();
237 let (sin, cos) = rad.sin_cos();
238 for y in bb.y.max(0)..=bb.y.saturating_add(bb.h).min(self.h - 1) {
239 for x in bb.x.max(0)..=bb.x.saturating_add(bb.w).min(self.w - 1) {
240 let dx = f64::from(x) + 0.5 - pivot_x;
241 let dy = f64::from(y) + 0.5 - pivot_y;
242 let lx = pivot_x + dx * cos - dy * sin;
243 let ly = pivot_y + dx * sin + dy * cos;
244 if lx < x0 || lx >= x1 || ly < y0 || ly >= y1 {
245 continue;
246 }
247 if fill {
248 self.set(x, y, c);
249 continue;
250 }
251 let edge_dist = (lx - x0).min(x1 - lx).min(ly - y0).min(y1 - ly);
252 if edge_dist < band {
253 self.set(x, y, c);
254 }
255 }
256 }
257 }
258
259 pub fn draw_shape(&mut self, shape: &Shape, color: Color, thickness: i32, fill: bool) {
261 match *shape {
262 Shape::Rect(r) if fill => self.fill_rect(r, color),
263 Shape::Rect(r) => self.draw_rect_outline(r, color, thickness),
264 Shape::Circle { cx, cy, r } if fill => self.fill_circle(cx, cy, r, color),
265 Shape::Circle { cx, cy, r } => self.draw_circle_outline(cx, cy, r, color, thickness),
266 Shape::Ellipse { cx, cy, rx, ry } if fill => {
267 self.ellipse_band(cx, cy, rx, ry, 0, color);
268 }
269 Shape::Ellipse { cx, cy, rx, ry } => {
270 self.ellipse_band(cx, cy, rx, ry, thickness, color);
271 }
272 Shape::Triangle { .. } => self.draw_triangle(shape, color, thickness, fill),
273 Shape::Poly { ref points } if fill => self.fill_poly(points, color),
274 Shape::Poly { ref points } => self.draw_poly_outline(points, color, thickness),
275 }
276 }
277
278 fn fill_poly(&mut self, points: &[Point], color: Color) {
281 if points.len() < 3 {
282 return;
283 }
284 let c = color.to_0rgb();
285 let y0 = points.iter().map(|p| p.y).min().unwrap_or(0).max(0);
286 let y1 = points
287 .iter()
288 .map(|p| p.y)
289 .max()
290 .unwrap_or(0)
291 .min(self.h - 1);
292 for y in y0..=y1 {
293 let spans = scanline_spans(points, y);
294 for span in spans.chunks(2) {
295 let [l, r] = span else { continue };
296 let xa = (l.ceil() as i32).max(0);
297 let xb = (r.floor() as i32).min(self.w - 1);
298 for x in xa..=xb {
299 self.set(x, y, c);
300 }
301 }
302 }
303 }
304
305 fn draw_poly_outline(&mut self, points: &[Point], color: Color, thickness: i32) {
308 if points.len() < 2 || thickness <= 0 {
309 return;
310 }
311 let n = points.len();
312 for i in 0..n {
313 self.stamp_segment(points[i], points[(i + 1) % n], color, thickness);
314 }
315 }
316
317 fn stamp_segment(&mut self, a: Point, b: Point, color: Color, thickness: i32) {
319 let steps = (b.x - a.x).abs().max((b.y - a.y).abs()).max(1);
320 let half = thickness / 2;
321 for i in 0..=steps {
322 let x = a.x + ((b.x - a.x) * i) / steps;
323 let y = a.y + ((b.y - a.y) * i) / steps;
324 self.fill_rect(
325 Rect::new(x - half, y - half, thickness.max(1), thickness.max(1)),
326 color,
327 );
328 }
329 }
330
331 fn ellipse_band(&mut self, cx: i32, cy: i32, rx: i32, ry: i32, thickness: i32, color: Color) {
335 if rx <= 0 || ry <= 0 {
336 return;
337 }
338 let c = color.to_0rgb();
339 let (irx, iry) = if thickness <= 0 {
340 (-1, -1)
341 } else {
342 ((rx - thickness).max(0), (ry - thickness).max(0))
343 };
344 for y in (cy - ry).max(0)..=(cy + ry).min(self.h - 1) {
345 for x in (cx - rx).max(0)..=(cx + rx).min(self.w - 1) {
346 if ellipse_covers(x - cx, y - cy, rx, ry)
347 && !ellipse_covers(x - cx, y - cy, irx, iry)
348 {
349 self.set(x, y, c);
350 }
351 }
352 }
353 }
354
355 fn draw_triangle(&mut self, shape: &Shape, color: Color, thickness: i32, fill: bool) {
358 let Shape::Triangle {
359 ax,
360 ay,
361 bx,
362 by,
363 cx,
364 cy,
365 } = *shape
366 else {
367 return;
368 };
369 if !fill && thickness <= 0 {
370 return;
371 }
372 let c = color.to_0rgb();
373 let bb = shape.bbox();
374 let band = f64::from(thickness);
375 for y in bb.y.max(0)..=bb.y.saturating_add(bb.h).min(self.h - 1) {
376 for x in bb.x.max(0)..=bb.x.saturating_add(bb.w).min(self.w - 1) {
377 if !shape.covers(x, y) {
378 continue;
379 }
380 if fill {
381 self.set(x, y, c);
382 continue;
383 }
384 let d = seg_dist(x, y, ax, ay, bx, by)
385 .min(seg_dist(x, y, bx, by, cx, cy))
386 .min(seg_dist(x, y, cx, cy, ax, ay));
387 if d < band {
388 self.set(x, y, c);
389 }
390 }
391 }
392 }
393}
394
395fn seg_dist(px: i32, py: i32, x1: i32, y1: i32, x2: i32, y2: i32) -> f64 {
397 let (px, py) = (f64::from(px), f64::from(py));
398 let (x1, y1) = (f64::from(x1), f64::from(y1));
399 let (x2, y2) = (f64::from(x2), f64::from(y2));
400 let (dx, dy) = (x2 - x1, y2 - y1);
401 let len2 = dx * dx + dy * dy;
402 let t = if len2 == 0.0 {
403 0.0
404 } else {
405 (((px - x1) * dx + (py - y1) * dy) / len2).clamp(0.0, 1.0)
406 };
407 (px - (x1 + t * dx)).hypot(py - (y1 + t * dy))
408}
409
410pub fn coord_text(shape: &Shape) -> String {
413 match *shape {
414 Shape::Rect(ref r) => format!("({}, {}) {}x{}", r.x, r.y, r.w, r.h),
415 Shape::Circle { cx, cy, r } => format!("({cx}, {cy}) r={r}"),
416 Shape::Ellipse { cx, cy, rx, ry } => format!("({cx}, {cy}) {rx}x{ry}"),
417 Shape::Triangle { .. } | Shape::Poly { .. } => {
418 let b = shape.bbox();
419 format!("({}, {}) {}x{}", b.x, b.y, b.w, b.h)
420 }
421 }
422}
423
424pub fn smart_text_position(bbox: Rect, bounds: Size, text_len: usize, scale: i32) -> Point {
429 let scale = scale.max(1);
430 let padding = 4 * scale;
431 let text_w = font::text_width(text_len, scale);
432 let text_h = font::line_height(scale);
433 let mut x = bbox.x;
434 let mut y = bbox.y - text_h - padding;
435 if x + text_w > bounds.w {
436 x = bbox.x - text_w - padding;
437 }
438 if x < 0 {
439 x = bbox.x + bbox.w + padding;
440 }
441 if y < 0 {
442 y = bbox.y + bbox.h + padding;
443 }
444 if y + text_h > bounds.h {
445 y = bbox.y - text_h - padding;
446 if y < 0 {
447 y = bbox.y + padding;
448 }
449 }
450 x = x.max(0);
451 y = y.max(0);
452 if x + text_w > bounds.w {
453 x = bounds.w - text_w;
454 }
455 if y + text_h > bounds.h {
456 y = bounds.h - text_h;
457 }
458 Point::new(x, y)
459}
460
461fn scanline_spans(points: &[Point], row: i32) -> Vec<f64> {
465 let mid = f64::from(row) + 0.5;
466 let count = points.len();
467 let mut crossings = Vec::new();
468 for i in 0..count {
469 let from = points[i];
470 let to = points[(i + 1) % count];
471 let (from_y, to_y) = (f64::from(from.y), f64::from(to.y));
472 if (from_y < mid) == (to_y < mid) {
473 continue;
474 }
475 let t = (mid - from_y) / (to_y - from_y);
476 crossings.push(f64::from(from.x) + t * f64::from(to.x - from.x));
477 }
478 crossings.sort_by(f64::total_cmp);
479 crossings
480}
481
482fn ellipse_covers(dx: i32, dy: i32, rx: i32, ry: i32) -> bool {
485 if rx <= 0 || ry <= 0 {
486 return false;
487 }
488 let (dx, dy) = (i128::from(dx), i128::from(dy));
489 let (rx, ry) = (i128::from(rx), i128::from(ry));
490 dx * dx * ry * ry + dy * dy * rx * rx <= rx * rx * ry * ry
491}
492
493pub fn apply_cutout_mask(rgba: &mut [u8], w: i32, h: i32, shapes: &[(Shape, i32)]) {
499 let covered = coverage(rgba.len(), w, h, shapes);
500 for (i, inside) in covered.iter().enumerate() {
501 if !inside {
502 rgba[i * 4 + 3] = 0;
503 }
504 }
505}
506
507pub fn apply_inverse_cutout_mask(rgba: &mut [u8], w: i32, h: i32, shapes: &[(Shape, i32)]) {
511 let covered = coverage(rgba.len(), w, h, shapes);
512 for (i, inside) in covered.iter().enumerate() {
513 if *inside {
514 rgba[i * 4 + 3] = 0;
515 }
516 }
517}
518
519fn coverage(rgba_len: usize, w: i32, h: i32, shapes: &[(Shape, i32)]) -> Vec<bool> {
523 assert_eq!(
524 rgba_len,
525 (w as usize) * (h as usize) * 4,
526 "RGBA buffer size mismatch"
527 );
528 let mut covered = vec![false; (w as usize) * (h as usize)];
529 for (shape, deg) in shapes {
530 mark_covered(&mut covered, w, h, shape, *deg);
531 }
532 covered
533}
534
535fn mark_covered(covered: &mut [bool], w: i32, h: i32, shape: &Shape, deg: i32) {
537 let bbox = shape.rotated_bbox(deg);
538 let x0 = bbox.x.max(0);
539 let y0 = bbox.y.max(0);
540 let x1 = bbox.x.saturating_add(bbox.w).min(w);
541 let y1 = bbox.y.saturating_add(bbox.h).min(h);
542 for y in y0..y1 {
543 for x in x0..x1 {
544 if shape.hit_test_rotated(deg, crate::geometry::Point::new(x, y)) {
545 covered[(y as usize) * (w as usize) + (x as usize)] = true;
546 }
547 }
548 }
549}
550
551pub fn apply_alpha_mask_outside(rgba: &mut [u8], w: i32, h: i32, shape: &Shape, deg: i32) {
556 assert_eq!(
557 rgba.len(),
558 (w as usize) * (h as usize) * 4,
559 "RGBA buffer size mismatch"
560 );
561 for y in 0..h {
562 for x in 0..w {
563 if !shape.hit_test_rotated(deg, crate::geometry::Point::new(x, y)) {
564 rgba[((y as usize) * (w as usize) + (x as usize)) * 4 + 3] = 0;
565 }
566 }
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 const W: i32 = 100;
575 const H: i32 = 60;
576 const RED: Color = Color { r: 255, g: 0, b: 0 };
577
578 fn canvas_buf() -> Vec<u32> {
579 vec![0u32; (W * H) as usize]
580 }
581
582 fn px(buf: &[u32], x: i32, y: i32) -> u32 {
583 buf[(y * W + x) as usize]
584 }
585
586 #[test]
587 fn rect_outline_sets_border_not_interior() {
588 let mut buf = canvas_buf();
589 let mut c = Canvas::new(&mut buf, W, H);
590 c.draw_rect_outline(Rect::new(10, 10, 20, 20), RED, 2);
591 let red = RED.to_0rgb();
592 assert_eq!(px(&buf, 10, 10), red);
593 assert_eq!(px(&buf, 29, 29), red);
594 assert_eq!(px(&buf, 11, 15), red); assert_eq!(px(&buf, 15, 15), 0); assert_eq!(px(&buf, 9, 10), 0); }
598
599 #[test]
600 fn fill_rect_clips_to_canvas() {
601 let mut buf = canvas_buf();
602 let mut c = Canvas::new(&mut buf, W, H);
603 c.fill_rect(Rect::new(-10, -10, 30, 30), RED);
604 assert_eq!(px(&buf, 0, 0), RED.to_0rgb());
605 assert_eq!(px(&buf, 19, 19), RED.to_0rgb());
606 assert_eq!(px(&buf, 20, 20), 0);
607 }
608
609 #[test]
610 fn fill_rect_fully_off_canvas_is_a_noop() {
611 let mut buf = canvas_buf();
612 let mut c = Canvas::new(&mut buf, W, H);
613 c.fill_rect(Rect::new(W + 10, 10, 20, 20), RED);
614 c.fill_rect(Rect::new(-50, -50, 20, 20), RED);
615 c.fill_rect(Rect::new(10, H + 5, 20, 20), RED);
616 assert!(buf.iter().all(|&p| p == 0));
617 }
618
619 #[test]
620 fn zero_thickness_draws_nothing() {
621 let mut buf = canvas_buf();
622 let mut c = Canvas::new(&mut buf, W, H);
623 c.draw_rect_outline(Rect::new(10, 10, 20, 20), RED, 0);
624 c.draw_circle_outline(50, 30, 10, RED, 0);
625 assert!(buf.iter().all(|&p| p == 0));
626 }
627
628 #[test]
629 fn ellipse_outline_is_a_band_and_fill_covers_the_interior() {
630 let mut buf = canvas_buf();
631 let mut c = Canvas::new(&mut buf, W, H);
632 let e = Shape::Ellipse {
633 cx: 50,
634 cy: 30,
635 rx: 30,
636 ry: 15,
637 };
638 c.draw_shape(&e, RED, 3, false);
639 let red = RED.to_0rgb();
640 assert_eq!(px(&buf, 79, 30), red, "on the rim");
641 assert_eq!(px(&buf, 50, 30), 0, "outline leaves the center empty");
642 assert_eq!(px(&buf, 79, 15), 0, "bbox corner outside");
643
644 let mut buf = canvas_buf();
645 let mut c = Canvas::new(&mut buf, W, H);
646 c.draw_shape(&e, RED, 3, true);
647 assert_eq!(px(&buf, 50, 30), red, "fill covers the center");
648 }
649
650 #[test]
651 fn rotated_ellipse_raster_follows_the_turn() {
652 let mut buf = canvas_buf();
653 let mut c = Canvas::new(&mut buf, W, H);
654 let e = Shape::Ellipse {
655 cx: 50,
656 cy: 30,
657 rx: 25,
658 ry: 6,
659 };
660 c.draw_shape_rotated(&e, 90, RED, 2, true);
661 let red = RED.to_0rgb();
662 assert_eq!(px(&buf, 50, 50), red, "stands tall after the turn");
663 assert_eq!(px(&buf, 70, 30), 0, "no longer lies flat");
664 }
665
666 #[test]
667 fn circle_outline_is_an_annulus() {
668 let mut buf = canvas_buf();
669 let mut c = Canvas::new(&mut buf, W, H);
670 c.draw_circle_outline(50, 30, 10, RED, 2);
671 let red = RED.to_0rgb();
672 assert_eq!(px(&buf, 60, 30), red); assert_eq!(px(&buf, 59, 30), red); assert_eq!(px(&buf, 50, 30), 0); assert_eq!(px(&buf, 62, 30), 0); }
677
678 #[test]
679 fn fill_circle_covers_center_and_clips() {
680 let mut buf = canvas_buf();
681 let mut c = Canvas::new(&mut buf, W, H);
682 c.fill_circle(0, 0, 10, RED); assert_eq!(px(&buf, 0, 0), RED.to_0rgb());
684 assert_eq!(px(&buf, 7, 7), RED.to_0rgb());
685 assert_eq!(px(&buf, 8, 8), 0);
686 }
687
688 #[test]
689 fn triangle_fill_covers_interior_not_bbox_corners() {
690 let mut buf = canvas_buf();
691 let mut c = Canvas::new(&mut buf, W, H);
692 let tri = Shape::Triangle {
693 ax: 50,
694 ay: 10,
695 bx: 10,
696 by: 50,
697 cx: 90,
698 cy: 50,
699 };
700 c.draw_shape(&tri, RED, 2, true);
701 let red = RED.to_0rgb();
702 assert_eq!(px(&buf, 50, 40), red); assert_eq!(px(&buf, 50, 11), red); assert_eq!(px(&buf, 12, 12), 0); assert_eq!(px(&buf, 88, 12), 0); }
707
708 #[test]
709 fn triangle_outline_is_a_band_not_a_fill() {
710 let mut buf = canvas_buf();
711 let mut c = Canvas::new(&mut buf, W, H);
712 let tri = Shape::Triangle {
713 ax: 50,
714 ay: 10,
715 bx: 10,
716 by: 50,
717 cx: 90,
718 cy: 50,
719 };
720 c.draw_shape(&tri, RED, 2, false);
721 let red = RED.to_0rgb();
722 assert_eq!(px(&buf, 50, 49), red); assert_eq!(px(&buf, 50, 35), 0); assert_eq!(px(&buf, 12, 12), 0); }
726
727 #[test]
728 fn text_inks_inside_its_line_box_and_nowhere_else() {
729 let mut buf = canvas_buf();
730 let mut c = Canvas::new(&mut buf, W, H);
731 c.draw_text(10, 10, "I", RED, 1);
732 let box_x = 10 - 1..10 + font::advance(1) + 1;
735 let box_y = 10 - 1..10 + font::line_height(1) + 1;
736 let mut inked = false;
737 for y in 0..H {
738 for x in 0..W {
739 let p = px(&buf, x, y);
740 if box_x.contains(&x) && box_y.contains(&y) {
741 inked |= p != 0;
742 continue;
743 }
744 assert_eq!(p, 0, "stray ink at ({x},{y})");
745 }
746 }
747 assert!(inked, "the glyph drew nothing");
748 }
749
750 #[test]
751 fn text_off_canvas_is_safe() {
752 let mut buf = canvas_buf();
753 let mut c = Canvas::new(&mut buf, W, H);
754 c.draw_text(-5, -5, "EDGE", RED, 1);
755 c.draw_text(W - 3, H - 3, "EDGE", RED, 1);
756 }
757
758 #[test]
759 fn smart_position_defaults_above() {
760 let p = smart_text_position(Rect::new(200, 200, 100, 50), Size::new(1920, 1080), 10, 1);
761 assert_eq!(p, Point::new(200, 200 - font::line_height(1) - 4));
762 }
763
764 #[test]
765 fn smart_position_flips_below_at_top_edge() {
766 let p = smart_text_position(Rect::new(200, 2, 100, 50), Size::new(1920, 1080), 10, 1);
767 assert_eq!(p, Point::new(200, 2 + 50 + 4));
768 }
769
770 #[test]
771 fn smart_position_flips_left_at_right_edge() {
772 let bbox = Rect::new(1900, 200, 15, 50);
773 let p = smart_text_position(bbox, Size::new(1920, 1080), 10, 1);
774 let text_w = font::text_width(10, 1);
775 assert_eq!(p.x, 1900 - text_w - 4);
776 }
777
778 #[test]
779 fn smart_position_stays_on_screen_in_corners() {
780 let bounds = Size::new(1920, 1080);
781 let text_len = 20;
782 for bbox in [
783 Rect::new(0, 0, 50, 50),
784 Rect::new(1870, 0, 50, 50),
785 Rect::new(0, 1030, 50, 50),
786 Rect::new(1870, 1030, 50, 50),
787 ] {
788 let p = smart_text_position(bbox, bounds, text_len, 1);
789 assert!(p.x >= 0 && p.y >= 0, "{bbox:?} gave {p:?}");
790 assert!(
791 p.x + font::text_width(text_len, 1) <= bounds.w
792 && p.y + font::line_height(1) <= bounds.h,
793 "{bbox:?} gave {p:?}"
794 );
795 }
796 }
797
798 #[test]
799 fn scaled_text_is_larger_and_reaches_full_ink() {
800 let mut buf = canvas_buf();
801 let mut c = Canvas::new(&mut buf, W, H);
802 c.draw_text(10, 10, "M", RED, 2);
803 let red = RED.to_0rgb();
806 let box_x = 10 - 1..10 + font::advance(2) + 1;
807 let box_y = 10 - 1..10 + font::line_height(2) + 1;
808 let mut solid = false;
809 for y in 0..H {
810 for x in 0..W {
811 let p = px(&buf, x, y);
812 if box_x.contains(&x) && box_y.contains(&y) {
813 solid |= p == red;
814 continue;
815 }
816 assert_eq!(p, 0, "stray ink at ({x},{y})");
817 }
818 }
819 assert!(solid, "no fully-covered pixel in 'M' at scale 2");
820 }
821
822 #[test]
823 fn scaled_smart_position_scales_offsets() {
824 let p = smart_text_position(Rect::new(200, 200, 100, 50), Size::new(1920, 1080), 10, 2);
825 assert_eq!(p, Point::new(200, 200 - font::line_height(2) - 8));
826 }
827
828 #[test]
829 fn coord_text_formats() {
830 assert_eq!(
831 coord_text(&Shape::Rect(Rect::new(1, 2, 3, 4))),
832 "(1, 2) 3x4"
833 );
834 assert_eq!(
835 coord_text(&Shape::Circle { cx: 9, cy: 8, r: 7 }),
836 "(9, 8) r=7"
837 );
838 }
839
840 #[test]
841 fn rotated_rect_raster_follows_the_turn() {
842 let mut buf = canvas_buf();
843 let mut c = Canvas::new(&mut buf, W, H);
844 let s = Shape::Rect(Rect::new(20, 25, 60, 10));
847 c.draw_shape_rotated(&s, 90, RED, 2, true);
848 let red = RED.to_0rgb();
849 assert_eq!(px(&buf, 50, 5), red); assert_eq!(px(&buf, 50, 55), red); assert_eq!(px(&buf, 75, 30), 0); let mut buf2 = canvas_buf();
854 let mut c2 = Canvas::new(&mut buf2, W, H);
855 c2.draw_shape_rotated(&s, 0, RED, 2, true);
856 let mut buf3 = canvas_buf();
857 let mut c3 = Canvas::new(&mut buf3, W, H);
858 c3.draw_shape(&s, RED, 2, true);
859 assert_eq!(buf2, buf3);
860 }
861
862 #[test]
863 fn rect_covers_same_pixel_count_at_0_and_180_degrees() {
864 let s = Shape::Rect(Rect::new(20, 20, 11, 7)); let mut plain = canvas_buf();
866 Canvas::new(&mut plain, W, H).draw_shape(&s, RED, 2, true);
867 let mut turned = canvas_buf();
868 Canvas::new(&mut turned, W, H).draw_shape_rotated(&s, 180, RED, 2, true);
869 let count = |buf: &[u32]| buf.iter().filter(|&&p| p != 0).count();
870 assert_eq!(count(&plain), 77);
871 assert_eq!(count(&turned), 77, "180-degree raster must match 0-degree");
872 }
873
874 #[test]
875 fn dim_rect_scales_brightness_and_clips_to_the_canvas() {
876 let mut buf = vec![0x00FF_8040u32; (W * H) as usize];
877 let mut c = Canvas::new(&mut buf, W, H);
878 c.dim_rect(Rect::new(-10, -10, 20, 20), 128);
880 assert_eq!(buf[0], 0x007F_4020, "channels each halve");
881 assert_eq!(
882 buf[(10 * W + 10) as usize],
883 0x00FF_8040,
884 "outside the rect untouched"
885 );
886
887 let mut buf = vec![0x00FF_FFFFu32; (W * H) as usize];
888 Canvas::new(&mut buf, W, H).dim_rect(Rect::new(0, 0, 2, 1), 0);
889 assert_eq!(buf[0], 0, "strength 0 blacks out");
890
891 let mut buf = vec![0x0012_3456u32; (W * H) as usize];
892 Canvas::new(&mut buf, W, H).dim_rect(Rect::new(0, 0, 1, 1), 256);
893 assert_eq!(buf[0], 0x0012_3456, "strength 256 leaves pixels alone");
894 Canvas::new(&mut buf, W, H).dim_rect(Rect::new(-50, -50, 10, 10), 64);
896 }
897
898 #[test]
899 fn cutout_keeps_every_shape_in_place_and_clears_the_rest() {
900 let w = 60;
901 let h = 40;
902 let mut rgba = vec![200u8; (w * h * 4) as usize];
903 let shapes = [
904 (Shape::Rect(Rect::new(5, 5, 10, 10)), 0),
905 (
906 Shape::Circle {
907 cx: 40,
908 cy: 20,
909 r: 6,
910 },
911 0,
912 ),
913 ];
914 apply_cutout_mask(&mut rgba, w, h, &shapes);
915 let pixel = |x: i32, y: i32| {
916 let i = ((y * w + x) * 4) as usize;
917 (rgba[i], rgba[i + 3])
918 };
919 assert_eq!(pixel(10, 10), (200, 200));
921 assert_eq!(pixel(40, 20), (200, 200));
922 assert_eq!(pixel(25, 10), (200, 0));
924 assert_eq!(pixel(0, 39), (200, 0));
925 assert_eq!(pixel(35, 15), (200, 0));
927 }
928
929 #[test]
930 fn cutout_of_a_rotated_rect_follows_the_turn() {
931 let w = 30;
932 let h = 30;
933 let mut rgba = vec![255u8; (w * h * 4) as usize];
934 apply_cutout_mask(
936 &mut rgba,
937 w,
938 h,
939 &[(Shape::Rect(Rect::new(3, 12, 24, 6)), 90)],
940 );
941 let alpha = |x: i32, y: i32| rgba[((y * w + x) * 4 + 3) as usize];
942 assert_eq!(alpha(15, 5), 255);
943 assert_eq!(alpha(5, 15), 0);
944 }
945
946 #[test]
947 fn cutout_clips_offscreen_shapes_instead_of_panicking() {
948 let w = 20;
949 let h = 20;
950 let mut rgba = vec![255u8; (w * h * 4) as usize];
951 apply_cutout_mask(
953 &mut rgba,
954 w,
955 h,
956 &[(Shape::Rect(Rect::new(-10, -10, 15, 15)), 0)],
957 );
958 let alpha = |x: i32, y: i32| rgba[((y * w + x) * 4 + 3) as usize];
959 assert_eq!(alpha(2, 2), 255);
960 assert_eq!(alpha(10, 10), 0);
961 }
962
963 #[test]
964 fn cutout_with_no_shapes_clears_everything() {
965 let mut rgba = vec![255u8; 4 * 4];
966 apply_cutout_mask(&mut rgba, 2, 2, &[]);
967 assert!(rgba.chunks(4).all(|p| p[3] == 0));
968 }
969
970 #[test]
971 fn inverse_cutout_is_the_exact_complement() {
972 let w = 60;
973 let h = 40;
974 let shapes = [
975 (Shape::Rect(Rect::new(5, 5, 10, 10)), 0),
976 (
977 Shape::Circle {
978 cx: 40,
979 cy: 20,
980 r: 6,
981 },
982 45,
983 ),
984 ];
985 let mut primary = vec![255u8; (w * h * 4) as usize];
986 let mut inverse = vec![255u8; (w * h * 4) as usize];
987 apply_cutout_mask(&mut primary, w, h, &shapes);
988 apply_inverse_cutout_mask(&mut inverse, w, h, &shapes);
989 for i in 0..(w * h) as usize {
992 let (p, v) = (primary[i * 4 + 3], inverse[i * 4 + 3]);
993 assert_eq!(p ^ v, 255, "pixel {i}: primary {p}, inverse {v}");
994 }
995 let idx = ((10 * w + 10) * 4 + 3) as usize;
998 assert_eq!(primary[idx], 255);
999 assert_eq!(inverse[idx], 0);
1000 }
1001
1002 #[test]
1003 fn rotated_alpha_mask_follows_the_turn() {
1004 let w = 30;
1005 let h = 30;
1006 let mut rgba = vec![255u8; (w * h * 4) as usize];
1007 let s = Shape::Rect(Rect::new(3, 12, 24, 6));
1009 apply_alpha_mask_outside(&mut rgba, w, h, &s, 90);
1010 let alpha = |x: i32, y: i32| rgba[((y * w + x) * 4 + 3) as usize];
1011 assert_eq!(alpha(15, 5), 255); assert_eq!(alpha(5, 15), 0); }
1014
1015 #[test]
1016 fn alpha_mask_zeroes_outside_circle_only() {
1017 let w = 20;
1018 let h = 20;
1019 let mut rgba = vec![255u8; (w * h * 4) as usize];
1020 apply_alpha_mask_outside(
1021 &mut rgba,
1022 w,
1023 h,
1024 &Shape::Circle {
1025 cx: 10,
1026 cy: 10,
1027 r: 8,
1028 },
1029 0,
1030 );
1031 let alpha = |x: i32, y: i32| rgba[((y * w + x) * 4 + 3) as usize];
1032 assert_eq!(alpha(10, 10), 255); assert_eq!(alpha(10, 2), 255); assert_eq!(alpha(0, 0), 0); assert_eq!(rgba[0], 255); }
1037
1038 #[test]
1039 fn a_rotated_rect_fills_its_interior() {
1040 let mut buf = canvas_buf();
1041 let mut c = Canvas::new(&mut buf, W, H);
1042 c.draw_shape_rotated(&Shape::Rect(Rect::new(30, 15, 40, 30)), 30, RED, 2, true);
1043 assert_eq!(px(&buf, 50, 30), RED.to_0rgb());
1045 }
1046
1047 #[test]
1048 fn a_rotated_rect_outline_is_a_band_not_a_fill() {
1049 let mut buf = canvas_buf();
1050 let mut c = Canvas::new(&mut buf, W, H);
1051 c.draw_shape_rotated(&Shape::Rect(Rect::new(30, 15, 40, 30)), 30, RED, 2, false);
1052 assert_eq!(px(&buf, 50, 30), 0, "centre stays empty for an outline");
1053 assert!(
1054 buf.iter().any(|&p| p == RED.to_0rgb()),
1055 "something was drawn"
1056 );
1057 }
1058
1059 #[test]
1060 fn a_rotated_triangle_bakes_its_rotation() {
1061 let mut buf = canvas_buf();
1062 let mut c = Canvas::new(&mut buf, W, H);
1063 let tri = Shape::Triangle {
1064 ax: 50,
1065 ay: 10,
1066 bx: 20,
1067 by: 50,
1068 cx: 80,
1069 cy: 50,
1070 };
1071 c.draw_shape_rotated(&tri, 90, RED, 1, true);
1072 assert!(buf.iter().any(|&p| p == RED.to_0rgb()));
1073 }
1074
1075 #[test]
1076 fn draw_shape_fills_or_outlines_each_kind() {
1077 for fill in [true, false] {
1078 for shape in [
1079 Shape::Rect(Rect::new(10, 10, 20, 20)),
1080 Shape::Circle {
1081 cx: 50,
1082 cy: 30,
1083 r: 12,
1084 },
1085 Shape::Triangle {
1086 ax: 60,
1087 ay: 10,
1088 bx: 45,
1089 by: 40,
1090 cx: 75,
1091 cy: 40,
1092 },
1093 ] {
1094 let mut buf = canvas_buf();
1095 let mut c = Canvas::new(&mut buf, W, H);
1096 c.draw_shape(&shape, RED, 2, fill);
1097 assert!(
1098 buf.iter().any(|&p| p == RED.to_0rgb()),
1099 "{shape:?} fill={fill} drew nothing"
1100 );
1101 }
1102 }
1103 }
1104
1105 #[test]
1106 fn a_caption_flips_inside_the_canvas_at_every_edge() {
1107 let bounds = Size::new(W, H);
1108 let len = 6;
1109 for bbox in [
1112 Rect::new(0, 0, 20, 20), Rect::new(W - 10, 0, 20, 20), Rect::new(0, H - 10, 20, 20), Rect::new(W - 5, H - 5, 20, 20), ] {
1117 let p = smart_text_position(bbox, bounds, len, 1);
1118 assert!(p.x >= 0 && p.y >= 0, "{bbox:?} -> {p:?}");
1119 assert!(p.x + font::text_width(len, 1) <= W, "{bbox:?} -> {p:?}");
1120 assert!(p.y + font::line_height(1) <= H, "{bbox:?} -> {p:?}");
1121 }
1122 }
1123
1124 #[test]
1125 fn a_caption_wider_than_the_canvas_starts_off_the_left_edge() {
1126 let len = 40;
1130 assert!(font::text_width(len, 1) > W);
1131 let p = smart_text_position(Rect::new(0, 0, 20, 20), Size::new(W, H), len, 1);
1132 assert!(p.x < 0, "{p:?}");
1133 }
1134}