1use crate::clip::{ClipRect, ClipStack};
9use crate::framebuffer::Framebuffer;
10use oxiui_core::Color;
11
12#[derive(Clone, Copy, Debug)]
14pub struct DashPattern {
15 pub dash_len: f32,
17 pub gap_len: f32,
19}
20
21impl DashPattern {
22 pub fn new(dash_len: f32, gap_len: f32) -> Self {
24 Self {
25 dash_len: dash_len.max(1.0),
26 gap_len: gap_len.max(0.0),
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug)]
35pub struct SrcImage<'a> {
36 pub data: &'a [u8],
38 pub width: u32,
40 pub height: u32,
42}
43
44impl<'a> SrcImage<'a> {
45 pub fn new(data: &'a [u8], width: u32, height: u32) -> Self {
47 Self {
48 data,
49 width,
50 height,
51 }
52 }
53
54 pub fn is_valid(&self) -> bool {
56 self.width > 0
57 && self.height > 0
58 && self.data.len() >= (self.width * self.height * 4) as usize
59 }
60}
61
62pub struct Canvas<'fb> {
64 fb: &'fb mut Framebuffer,
65 clip: ClipStack,
66 pub aa: bool,
69}
70
71impl<'fb> Canvas<'fb> {
72 pub fn new(fb: &'fb mut Framebuffer) -> Self {
75 let clip = ClipStack::new(fb.width(), fb.height());
76 Self { fb, clip, aa: true }
77 }
78
79 pub fn set_aa(&mut self, enabled: bool) {
84 self.aa = enabled;
85 }
86
87 pub fn framebuffer(&self) -> &Framebuffer {
89 self.fb
90 }
91
92 pub fn push_clip(&mut self, x: f32, y: f32, w: f32, h: f32) {
94 let rect = ClipRect::from_rect(
95 x.floor() as i64,
96 y.floor() as i64,
97 w.ceil() as i64,
98 h.ceil() as i64,
99 );
100 self.clip.push(rect);
101 }
102
103 pub fn pop_clip(&mut self) {
105 self.clip.pop();
106 }
107
108 #[inline]
109 fn put(&mut self, x: i64, y: i64, color: &Color, coverage: f32) {
110 if !self.clip.current().contains(x, y) {
111 return;
112 }
113 if x < 0 || y < 0 {
114 return;
115 }
116 self.fb.blend_coverage(x as u32, y as u32, color, coverage);
117 }
118
119 pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
121 if w <= 0.0 || h <= 0.0 {
122 return;
123 }
124 let x0 = x.floor() as i64;
125 let y0 = y.floor() as i64;
126 let x1 = (x + w).ceil() as i64;
127 let y1 = (y + h).ceil() as i64;
128 let clip = self.clip.current();
129 let cx0 = x0.max(clip.x0);
130 let cy0 = y0.max(clip.y0);
131 let cx1 = x1.min(clip.x1);
132 let cy1 = y1.min(clip.y1);
133 for py in cy0..cy1 {
134 for px in cx0..cx1 {
135 if px >= 0 && py >= 0 {
136 self.fb
137 .blend(px as u32, py as u32, crate::framebuffer::pack(&color));
138 }
139 }
140 }
141 }
142
143 pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32, thickness: f32, color: Color) {
145 let t = thickness.max(1.0);
146 self.fill_rect(x, y, w, t, color);
148 self.fill_rect(x, y + h - t, w, t, color);
149 self.fill_rect(x, y, t, h, color);
150 self.fill_rect(x + w - t, y, t, h, color);
151 }
152
153 pub fn draw_line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color) {
156 let mut x0 = x0.round() as i64;
157 let mut y0 = y0.round() as i64;
158 let x1 = x1.round() as i64;
159 let y1 = y1.round() as i64;
160 let dx = (x1 - x0).abs();
161 let dy = -(y1 - y0).abs();
162 let sx = if x0 < x1 { 1 } else { -1 };
163 let sy = if y0 < y1 { 1 } else { -1 };
164 let mut err = dx + dy;
165 loop {
166 self.put(x0, y0, &color, 1.0);
167 if x0 == x1 && y0 == y1 {
168 break;
169 }
170 let e2 = 2 * err;
171 if e2 >= dy {
172 err += dy;
173 x0 += sx;
174 }
175 if e2 <= dx {
176 err += dx;
177 y0 += sy;
178 }
179 }
180 }
181
182 pub fn fill_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color) {
187 if radius <= 0.0 {
188 return;
189 }
190 let x0 = (cx - radius - 1.0).floor() as i64;
191 let y0 = (cy - radius - 1.0).floor() as i64;
192 let x1 = (cx + radius + 1.0).ceil() as i64;
193 let y1 = (cy + radius + 1.0).ceil() as i64;
194 for py in y0..y1 {
195 for px in x0..x1 {
196 let sx = px as f32 + 0.5;
198 let sy = py as f32 + 0.5;
199 let dist = ((sx - cx).powi(2) + (sy - cy).powi(2)).sqrt();
200 let coverage = (radius - dist + 0.5).clamp(0.0, 1.0);
201 if coverage > 0.0 {
202 self.put(px, py, &color, coverage);
203 }
204 }
205 }
206 }
207
208 pub fn fill_rounded_rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, color: Color) {
210 if w <= 0.0 || h <= 0.0 {
211 return;
212 }
213 let r = radius.clamp(0.0, (w.min(h)) * 0.5);
214 if r <= 0.0 {
215 self.fill_rect(x, y, w, h, color);
216 return;
217 }
218 let x0 = x.floor() as i64;
219 let y0 = y.floor() as i64;
220 let x1 = (x + w).ceil() as i64;
221 let y1 = (y + h).ceil() as i64;
222 let left = x + r;
224 let right = x + w - r;
225 let top = y + r;
226 let bottom = y + h - r;
227 for py in y0..y1 {
228 for px in x0..x1 {
229 let sx = px as f32 + 0.5;
230 let sy = py as f32 + 0.5;
231 let dx = if sx < left {
233 left - sx
234 } else if sx > right {
235 sx - right
236 } else {
237 0.0
238 };
239 let dy = if sy < top {
240 top - sy
241 } else if sy > bottom {
242 sy - bottom
243 } else {
244 0.0
245 };
246 let coverage = if dx == 0.0 || dy == 0.0 {
247 if sx >= x && sx <= x + w && sy >= y && sy <= y + h {
249 1.0
250 } else {
251 0.0
252 }
253 } else {
254 let dist = (dx * dx + dy * dy).sqrt();
255 (r - dist + 0.5).clamp(0.0, 1.0)
256 };
257 if coverage > 0.0 {
258 self.put(px, py, &color, coverage);
259 }
260 }
261 }
262 }
263
264 pub fn draw_line_wu(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color) {
271 wu_line(self.fb, &self.clip, x0, y0, x1, y1, &color);
272 }
273
274 pub fn draw_line_thick(
277 &mut self,
278 x0: f32,
279 y0: f32,
280 x1: f32,
281 y1: f32,
282 width: f32,
283 color: Color,
284 ) {
285 if width <= 1.0 {
286 self.draw_line_wu(x0, y0, x1, y1, color);
287 return;
288 }
289 let half = width * 0.5;
290 let dx = x1 - x0;
291 let dy = y1 - y0;
292 let len = (dx * dx + dy * dy).sqrt().max(f32::EPSILON);
293 let nx = -dy / len * half;
294 let ny = dx / len * half;
295 let pts = [
296 (x0 + nx, y0 + ny),
297 (x1 + nx, y1 + ny),
298 (x1 - nx, y1 - ny),
299 (x0 - nx, y0 - ny),
300 ];
301 let clip = self.clip.current();
303 crate::scanline::fill_polygon_clipped(
304 self.fb,
305 &pts,
306 color,
307 crate::scanline::FillRule::NonZero,
308 true,
309 clip,
310 );
311 }
312
313 pub fn draw_line_dashed(
317 &mut self,
318 x0: f32,
319 y0: f32,
320 x1: f32,
321 y1: f32,
322 color: Color,
323 pattern: DashPattern,
324 ) {
325 let dx = x1 - x0;
326 let dy = y1 - y0;
327 let total_len = (dx * dx + dy * dy).sqrt();
328 if total_len < f32::EPSILON {
329 return;
330 }
331 let ux = dx / total_len;
332 let uy = dy / total_len;
333 let mut t = 0.0f32;
334 let mut drawing = true;
335 while t < total_len {
336 let seg_len = if drawing {
337 pattern.dash_len
338 } else {
339 pattern.gap_len
340 };
341 let end_t = (t + seg_len).min(total_len);
342 if drawing && end_t > t {
343 let sx = x0 + ux * t;
344 let sy = y0 + uy * t;
345 let ex = x0 + ux * end_t;
346 let ey = y0 + uy * end_t;
347 wu_line(self.fb, &self.clip, sx, sy, ex, ey, &color);
348 }
349 t += seg_len;
350 if t >= total_len {
351 break;
352 }
353 drawing = !drawing;
354 }
355 }
356
357 pub fn fill_ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, color: Color) {
364 if rx <= 0.0 || ry <= 0.0 {
365 return;
366 }
367 let x0 = (cx - rx - 1.0).floor() as i64;
368 let y0 = (cy - ry - 1.0).floor() as i64;
369 let x1 = (cx + rx + 1.0).ceil() as i64;
370 let y1 = (cy + ry + 1.0).ceil() as i64;
371 let clip = self.clip.current();
372 for py in y0..y1 {
373 for px in x0..x1 {
374 if !clip.contains(px, py) {
375 continue;
376 }
377 let sx = px as f32 + 0.5;
378 let sy = py as f32 + 0.5;
379 let ex = (sx - cx) / rx;
380 let ey = (sy - cy) / ry;
381 let d = (ex * ex + ey * ey).sqrt();
382 let eff_r = 1.0 / (ex.abs() / rx + ey.abs() / ry + f32::EPSILON);
386 let coverage = (1.0 - d + 0.5 / eff_r.max(1.0)).clamp(0.0, 1.0);
387 if coverage > 0.0 && px >= 0 && py >= 0 {
388 self.fb
389 .blend_coverage(px as u32, py as u32, &color, coverage);
390 }
391 }
392 }
393 }
394
395 pub fn fill_rounded_rect_per_corner(
403 &mut self,
404 x: f32,
405 y: f32,
406 w: f32,
407 h: f32,
408 radii: [f32; 4],
409 color: Color,
410 ) {
411 if w <= 0.0 || h <= 0.0 {
412 return;
413 }
414 let half_w = w * 0.5;
416 let half_h = h * 0.5;
417 let r_tl = radii[0].clamp(0.0, half_w.min(half_h));
418 let r_tr = radii[1].clamp(0.0, half_w.min(half_h));
419 let r_br = radii[2].clamp(0.0, half_w.min(half_h));
420 let r_bl = radii[3].clamp(0.0, half_w.min(half_h));
421
422 let x0 = x.floor() as i64;
423 let y0 = y.floor() as i64;
424 let x1 = (x + w).ceil() as i64;
425 let y1 = (y + h).ceil() as i64;
426 let clip = self.clip.current();
427
428 let c_tl = (x + r_tl, y + r_tl);
430 let c_tr = (x + w - r_tr, y + r_tr);
431 let c_br = (x + w - r_br, y + h - r_br);
432 let c_bl = (x + r_bl, y + h - r_bl);
433
434 let corner_params = CornerCoverageParams {
435 left: x,
436 top: y,
437 right: x + w,
438 bottom: y + h,
439 c_tl,
440 c_tr,
441 c_br,
442 c_bl,
443 r_tl,
444 r_tr,
445 r_br,
446 r_bl,
447 };
448 for py in y0..y1 {
449 for px in x0..x1 {
450 if !clip.contains(px, py) {
451 continue;
452 }
453 let sx = px as f32 + 0.5;
454 let sy = py as f32 + 0.5;
455 let coverage = per_corner_coverage(sx, sy, &corner_params);
456 if coverage > 0.0 && px >= 0 && py >= 0 {
457 self.fb
458 .blend_coverage(px as u32, py as u32, &color, coverage);
459 }
460 }
461 }
462 }
463
464 pub fn blit_rgba(&mut self, src: SrcImage<'_>, dx: f32, dy: f32, dst_w: u32, dst_h: u32) {
471 if !src.is_valid() || dst_w == 0 || dst_h == 0 {
472 return;
473 }
474 let src_w = src.width;
475 let src_h = src.height;
476 let ox = dx.round() as i64;
477 let oy = dy.round() as i64;
478 for j in 0..dst_h {
479 for i in 0..dst_w {
480 let su = (i * src_w) / dst_w;
482 let sv = (j * src_h) / dst_h;
483 let si = ((sv * src_w + su) * 4) as usize;
484 let r = src.data[si];
485 let g = src.data[si + 1];
486 let b = src.data[si + 2];
487 let a = src.data[si + 3];
488 if a == 0 {
489 continue;
490 }
491 let px = ox + i as i64;
492 let py = oy + j as i64;
493 if px < 0 || py < 0 || !self.clip.current().contains(px, py) {
494 continue;
495 }
496 self.fb.blend(
497 px as u32,
498 py as u32,
499 crate::framebuffer::pack_rgba(r, g, b, a),
500 );
501 }
502 }
503 }
504 pub fn blit_bilinear(&mut self, src: SrcImage<'_>, dx: f32, dy: f32, dst_w: u32, dst_h: u32) {
507 if !src.is_valid() || dst_w == 0 || dst_h == 0 {
508 return;
509 }
510 let src_w = src.width;
511 let src_h = src.height;
512 let ox = dx.round() as i64;
513 let oy = dy.round() as i64;
514 let clip = self.clip.current();
515
516 for j in 0..dst_h {
517 for i in 0..dst_w {
518 let px = ox + i as i64;
519 let py = oy + j as i64;
520 if !clip.contains(px, py) || px < 0 || py < 0 {
521 continue;
522 }
523 let u = (i as f32 + 0.5) * src_w as f32 / dst_w as f32 - 0.5;
525 let v = (j as f32 + 0.5) * src_h as f32 / dst_h as f32 - 0.5;
526 let x0 = u.floor() as i32;
527 let y0 = v.floor() as i32;
528 let fx = u - x0 as f32;
529 let fy = v - y0 as f32;
530
531 let sample = |sx: i32, sy: i32| -> [f32; 4] {
532 let sx = sx.clamp(0, src_w as i32 - 1) as u32;
533 let sy = sy.clamp(0, src_h as i32 - 1) as u32;
534 let idx = ((sy * src_w + sx) * 4) as usize;
535 [
536 src.data[idx] as f32,
537 src.data[idx + 1] as f32,
538 src.data[idx + 2] as f32,
539 src.data[idx + 3] as f32,
540 ]
541 };
542
543 let s00 = sample(x0, y0);
544 let s10 = sample(x0 + 1, y0);
545 let s01 = sample(x0, y0 + 1);
546 let s11 = sample(x0 + 1, y0 + 1);
547
548 let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
549 let r = lerp(lerp(s00[0], s10[0], fx), lerp(s01[0], s11[0], fx), fy);
550 let g = lerp(lerp(s00[1], s10[1], fx), lerp(s01[1], s11[1], fx), fy);
551 let b = lerp(lerp(s00[2], s10[2], fx), lerp(s01[2], s11[2], fx), fy);
552 let a = lerp(lerp(s00[3], s10[3], fx), lerp(s01[3], s11[3], fx), fy);
553
554 if a <= 0.0 {
555 continue;
556 }
557 self.fb.blend(
558 px as u32,
559 py as u32,
560 crate::framebuffer::pack_rgba(
561 r.round().clamp(0.0, 255.0) as u8,
562 g.round().clamp(0.0, 255.0) as u8,
563 b.round().clamp(0.0, 255.0) as u8,
564 a.round().clamp(0.0, 255.0) as u8,
565 ),
566 );
567 }
568 }
569 }
570
571 pub fn blit_nine_slice(
576 &mut self,
577 src: SrcImage<'_>,
578 dx: f32,
579 dy: f32,
580 dst_w: u32,
581 dst_h: u32,
582 insets: [u32; 4],
583 ) {
584 if !src.is_valid() || dst_w == 0 || dst_h == 0 {
585 return;
586 }
587 let [ins_t, ins_r, ins_b, ins_l] = insets;
588 let sw = src.width;
589 let sh = src.height;
590
591 let ins_l = ins_l.min(sw / 2);
593 let ins_r = ins_r.min(sw / 2);
594 let ins_t = ins_t.min(sh / 2);
595 let ins_b = ins_b.min(sh / 2);
596
597 let dst_l = ins_l.min(dst_w / 2);
599 let dst_r = ins_r.min(dst_w / 2);
600 let dst_t = ins_t.min(dst_h / 2);
601 let dst_b = ins_b.min(dst_h / 2);
602
603 let ox = dx.round() as i64;
604 let oy = dy.round() as i64;
605
606 let sx_segs = [0, ins_l, sw - ins_r, sw];
608 let sy_segs = [0, ins_t, sh - ins_b, sh];
609
610 let dx_segs = [0u32, dst_l, dst_w - dst_r, dst_w];
612 let dy_segs = [0u32, dst_t, dst_h - dst_b, dst_h];
613
614 for row in 0..3usize {
615 for col in 0..3usize {
616 let src_x0 = sx_segs[col];
617 let src_x1 = sx_segs[col + 1];
618 let src_y0 = sy_segs[row];
619 let src_y1 = sy_segs[row + 1];
620 let src_patch_w = src_x1.saturating_sub(src_x0);
621 let src_patch_h = src_y1.saturating_sub(src_y0);
622
623 let dst_x0 = dx_segs[col];
624 let dst_x1 = dx_segs[col + 1];
625 let dst_y0 = dy_segs[row];
626 let dst_y1 = dy_segs[row + 1];
627 let dst_patch_w = dst_x1.saturating_sub(dst_x0);
628 let dst_patch_h = dst_y1.saturating_sub(dst_y0);
629
630 if src_patch_w == 0 || src_patch_h == 0 || dst_patch_w == 0 || dst_patch_h == 0 {
631 continue;
632 }
633
634 let mut patch = Vec::with_capacity((src_patch_w * src_patch_h * 4) as usize);
636 for sy in src_y0..src_y1 {
637 for sxi in src_x0..src_x1 {
638 let idx = ((sy * sw + sxi) * 4) as usize;
639 patch.extend_from_slice(&src.data[idx..idx + 4]);
640 }
641 }
642 let patch_img = SrcImage::new(&patch, src_patch_w, src_patch_h);
643 let dst_abs_x = ox + dst_x0 as i64;
644 let dst_abs_y = oy + dst_y0 as i64;
645 self.blit_bilinear(
646 patch_img,
647 dst_abs_x as f32,
648 dst_abs_y as f32,
649 dst_patch_w,
650 dst_patch_h,
651 );
652 }
653 }
654 }
655
656 pub fn fill_path(&mut self, path: &oxiui_core::paint::PathData, color: Color) {
662 use crate::path::Path;
663 use crate::scanline::FillRule as ScanFillRule;
664 let fill_rule = match path.fill_rule {
665 oxiui_core::paint::FillRule::EvenOdd => ScanFillRule::EvenOdd,
666 oxiui_core::paint::FillRule::NonZero => ScanFillRule::NonZero,
667 };
668 let mut p = Path::new().with_fill_rule(fill_rule);
669 replay_path_verbs(&mut p, path);
670 p.fill_clipped_aa(self.fb, color, self.clip.current(), self.aa);
671 }
672
673 pub fn stroke_path(
675 &mut self,
676 path: &oxiui_core::paint::PathData,
677 style: &oxiui_core::paint::StrokeStyle,
678 color: Color,
679 ) {
680 use crate::path::{Cap, Join, Path, StrokeStyle as PathStroke};
681 let mut p = Path::new();
682 replay_path_verbs(&mut p, path);
683 let ss = PathStroke {
684 width: style.width,
685 join: match style.join {
686 oxiui_core::paint::LineJoin::Miter => Join::Miter,
687 oxiui_core::paint::LineJoin::Bevel => Join::Bevel,
688 oxiui_core::paint::LineJoin::Round => Join::Round,
689 },
690 cap: match style.cap {
691 oxiui_core::paint::LineCap::Butt => Cap::Butt,
692 oxiui_core::paint::LineCap::Round => Cap::Round,
693 oxiui_core::paint::LineCap::Square => Cap::Square,
694 },
695 miter_limit: style.miter_limit,
696 };
697 p.stroke_clipped_aa(self.fb, &ss, color, self.clip.current(), self.aa);
698 }
699
700 pub fn fill_linear_gradient_cmd(
702 &mut self,
703 rect: oxiui_core::geometry::Rect,
704 start: oxiui_core::geometry::Point,
705 end: oxiui_core::geometry::Point,
706 stops: &[oxiui_core::paint::GradientStop],
707 ) {
708 use crate::gradient::{GradientStop as SoftStop, LinearGradient};
709 let soft_stops: Vec<SoftStop> = stops
710 .iter()
711 .map(|s| SoftStop {
712 offset: s.offset,
713 color: s.color,
714 })
715 .collect();
716 let g = LinearGradient::new((start.x, start.y), (end.x, end.y), soft_stops);
717 let clip = self.clip.current();
718 g.fill_rect(
719 self.fb,
720 &clip,
721 rect.left(),
722 rect.top(),
723 rect.width(),
724 rect.height(),
725 );
726 }
727
728 pub fn fill_radial_gradient_cmd(
730 &mut self,
731 rect: oxiui_core::geometry::Rect,
732 center: oxiui_core::geometry::Point,
733 radius: f32,
734 stops: &[oxiui_core::paint::GradientStop],
735 ) {
736 use crate::gradient::{GradientStop as SoftStop, RadialGradient};
737 let soft_stops: Vec<SoftStop> = stops
738 .iter()
739 .map(|s| SoftStop {
740 offset: s.offset,
741 color: s.color,
742 })
743 .collect();
744 let g = RadialGradient::new((center.x, center.y), radius, soft_stops);
745 let clip = self.clip.current();
746 g.fill_rect(
747 self.fb,
748 &clip,
749 rect.left(),
750 rect.top(),
751 rect.width(),
752 rect.height(),
753 );
754 }
755
756 pub fn box_shadow_cmd(
759 &mut self,
760 rect: oxiui_core::geometry::Rect,
761 offset: oxiui_core::geometry::Point,
762 blur_radius: f32,
763 color: Color,
764 cache: &mut crate::shadow::GaussianCache,
765 ) {
766 crate::shadow::box_shadow(
767 self.fb,
768 (rect.left(), rect.top(), rect.width(), rect.height()),
769 offset.x,
770 offset.y,
771 blur_radius,
772 color,
773 cache,
774 );
775 }
776
777 pub fn draw_quad_bezier(
783 &mut self,
784 p0: (f32, f32),
785 ctrl: (f32, f32),
786 p2: (f32, f32),
787 color: Color,
788 ) {
789 let pts = crate::path::flatten_quad_bezier(p0, ctrl, p2, 0.25);
790 for i in 1..pts.len() {
791 wu_line(
792 self.fb,
793 &self.clip,
794 pts[i - 1].0,
795 pts[i - 1].1,
796 pts[i].0,
797 pts[i].1,
798 &color,
799 );
800 }
801 }
802
803 pub fn draw_cubic_bezier(
805 &mut self,
806 p0: (f32, f32),
807 c1: (f32, f32),
808 c2: (f32, f32),
809 p3: (f32, f32),
810 color: Color,
811 ) {
812 let pts = crate::path::flatten_cubic_bezier(p0, c1, c2, p3, 0.25);
813 for i in 1..pts.len() {
814 wu_line(
815 self.fb,
816 &self.clip,
817 pts[i - 1].0,
818 pts[i - 1].1,
819 pts[i].0,
820 pts[i].1,
821 &color,
822 );
823 }
824 }
825}
826
827fn replay_path_verbs(p: &mut crate::path::Path, path: &oxiui_core::paint::PathData) {
833 use oxiui_core::paint::PathVerb;
834 for verb in &path.verbs {
835 match verb {
836 PathVerb::MoveTo(pt) => {
837 p.move_to((pt.x, pt.y));
838 }
839 PathVerb::LineTo(pt) => {
840 p.line_to((pt.x, pt.y));
841 }
842 PathVerb::QuadTo { ctrl, end } => {
843 p.quad_to((ctrl.x, ctrl.y), (end.x, end.y));
844 }
845 PathVerb::CubicTo { c1, c2, end } => {
846 p.cubic_to((c1.x, c1.y), (c2.x, c2.y), (end.x, end.y));
847 }
848 PathVerb::Close => {
849 p.close();
850 }
851 }
852 }
853}
854
855fn wu_line(
861 fb: &mut crate::framebuffer::Framebuffer,
862 clip: &crate::clip::ClipStack,
863 mut x0: f32,
864 mut y0: f32,
865 mut x1: f32,
866 mut y1: f32,
867 color: &Color,
868) {
869 let steep = (y1 - y0).abs() > (x1 - x0).abs();
870 if steep {
871 core::mem::swap(&mut x0, &mut y0);
872 core::mem::swap(&mut x1, &mut y1);
873 }
874 if x0 > x1 {
875 core::mem::swap(&mut x0, &mut x1);
876 core::mem::swap(&mut y0, &mut y1);
877 }
878 let dx = x1 - x0;
879 let dy = y1 - y0;
880 let gradient = if dx.abs() < f32::EPSILON {
881 1.0
882 } else {
883 dy / dx
884 };
885
886 let put_wu = |fb: &mut crate::framebuffer::Framebuffer,
887 clip: &crate::clip::ClipStack,
888 x: i64,
889 y: i64,
890 c: &Color,
891 alpha: f32| {
892 if !clip.current().contains(x, y) || x < 0 || y < 0 {
893 return;
894 }
895 fb.blend_coverage(x as u32, y as u32, c, alpha);
896 };
897
898 let xend = x0.round();
900 let yend = y0 + gradient * (xend - x0);
901 let xgap = 1.0 - (x0 + 0.5).fract();
902 let xpxl1 = xend as i64;
903 let ypxl1 = yend.floor() as i64;
904 if steep {
905 put_wu(fb, clip, ypxl1, xpxl1, color, (1.0 - yend.fract()) * xgap);
906 put_wu(fb, clip, ypxl1 + 1, xpxl1, color, yend.fract() * xgap);
907 } else {
908 put_wu(fb, clip, xpxl1, ypxl1, color, (1.0 - yend.fract()) * xgap);
909 put_wu(fb, clip, xpxl1, ypxl1 + 1, color, yend.fract() * xgap);
910 }
911 let mut intery = yend + gradient;
912
913 let xend = x1.round();
915 let yend = y1 + gradient * (xend - x1);
916 let xgap = (x1 + 0.5).fract();
917 let xpxl2 = xend as i64;
918 let ypxl2 = yend.floor() as i64;
919 if steep {
920 put_wu(fb, clip, ypxl2, xpxl2, color, (1.0 - yend.fract()) * xgap);
921 put_wu(fb, clip, ypxl2 + 1, xpxl2, color, yend.fract() * xgap);
922 } else {
923 put_wu(fb, clip, xpxl2, ypxl2, color, (1.0 - yend.fract()) * xgap);
924 put_wu(fb, clip, xpxl2, ypxl2 + 1, color, yend.fract() * xgap);
925 }
926
927 for x in (xpxl1 + 1)..xpxl2 {
929 let iy = intery.floor() as i64;
930 let frac = intery.fract();
931 if steep {
932 put_wu(fb, clip, iy, x, color, 1.0 - frac);
933 put_wu(fb, clip, iy + 1, x, color, frac);
934 } else {
935 put_wu(fb, clip, x, iy, color, 1.0 - frac);
936 put_wu(fb, clip, x, iy + 1, color, frac);
937 }
938 intery += gradient;
939 }
940}
941
942struct CornerCoverageParams {
944 left: f32,
945 top: f32,
946 right: f32,
947 bottom: f32,
948 c_tl: (f32, f32),
949 c_tr: (f32, f32),
950 c_br: (f32, f32),
951 c_bl: (f32, f32),
952 r_tl: f32,
953 r_tr: f32,
954 r_br: f32,
955 r_bl: f32,
956}
957
958fn per_corner_coverage(sx: f32, sy: f32, p: &CornerCoverageParams) -> f32 {
960 let CornerCoverageParams {
961 left,
962 top,
963 right,
964 bottom,
965 c_tl,
966 c_tr,
967 c_br,
968 c_bl,
969 r_tl,
970 r_tr,
971 r_br,
972 r_bl,
973 } = p;
974 let in_left_half = sx < (left + right) * 0.5;
976 let in_top_half = sy < (top + bottom) * 0.5;
977
978 if in_left_half && in_top_half && *r_tl > 0.0 && sx < c_tl.0 && sy < c_tl.1 {
980 let dx = c_tl.0 - sx;
982 let dy = c_tl.1 - sy;
983 let dist = (dx * dx + dy * dy).sqrt();
984 return (r_tl - dist + 0.5).clamp(0.0, 1.0);
985 }
986 if !in_left_half && in_top_half && *r_tr > 0.0 && sx > c_tr.0 && sy < c_tr.1 {
987 let dx = sx - c_tr.0;
989 let dy = c_tr.1 - sy;
990 let dist = (dx * dx + dy * dy).sqrt();
991 return (r_tr - dist + 0.5).clamp(0.0, 1.0);
992 }
993 if !in_left_half && !in_top_half && *r_br > 0.0 && sx > c_br.0 && sy > c_br.1 {
994 let dx = sx - c_br.0;
996 let dy = sy - c_br.1;
997 let dist = (dx * dx + dy * dy).sqrt();
998 return (r_br - dist + 0.5).clamp(0.0, 1.0);
999 }
1000 if in_left_half && !in_top_half && *r_bl > 0.0 && sx < c_bl.0 && sy > c_bl.1 {
1001 let dx = c_bl.0 - sx;
1003 let dy = sy - c_bl.1;
1004 let dist = (dx * dx + dy * dy).sqrt();
1005 return (r_bl - dist + 0.5).clamp(0.0, 1.0);
1006 }
1007
1008 if sx >= *left && sx <= *right && sy >= *top && sy <= *bottom {
1010 1.0
1011 } else {
1012 0.0
1013 }
1014}
1015
1016#[cfg(test)]
1017mod tests {
1018 use super::*;
1019 use crate::framebuffer::Framebuffer;
1020
1021 fn fresh(w: u32, h: u32) -> Framebuffer {
1022 Framebuffer::with_fill(w, h, Color(0, 0, 0, 255))
1023 }
1024
1025 #[test]
1026 fn fill_rect_paints_inside_only() {
1027 let mut fb = fresh(10, 10);
1028 {
1029 let mut c = Canvas::new(&mut fb);
1030 c.fill_rect(2.0, 2.0, 4.0, 4.0, Color(255, 0, 0, 255));
1031 }
1032 assert_eq!(fb.get_rgba(3, 3), Some((255, 0, 0, 255)));
1033 assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
1034 assert_eq!(fb.get_rgba(6, 6), Some((0, 0, 0, 255)));
1035 }
1036
1037 #[test]
1038 fn clip_prevents_drawing() {
1039 let mut fb = fresh(10, 10);
1040 {
1041 let mut c = Canvas::new(&mut fb);
1042 c.push_clip(0.0, 0.0, 3.0, 3.0);
1043 c.fill_rect(0.0, 0.0, 10.0, 10.0, Color(0, 255, 0, 255));
1045 c.pop_clip();
1046 }
1047 assert_eq!(fb.get_rgba(1, 1), Some((0, 255, 0, 255)));
1048 assert_eq!(fb.get_rgba(5, 5), Some((0, 0, 0, 255)));
1049 }
1050
1051 #[test]
1052 fn draw_line_horizontal_and_diagonal() {
1053 let mut fb = fresh(10, 10);
1054 {
1055 let mut c = Canvas::new(&mut fb);
1056 c.draw_line(0.0, 0.0, 9.0, 0.0, Color(255, 255, 255, 255));
1057 c.draw_line(0.0, 0.0, 9.0, 9.0, Color(255, 0, 0, 255));
1058 }
1059 assert_eq!(fb.get_rgba(5, 0), Some((255, 255, 255, 255)));
1060 assert_eq!(fb.get_rgba(5, 5), Some((255, 0, 0, 255)));
1061 assert_eq!(fb.get_rgba(9, 9), Some((255, 0, 0, 255)));
1062 }
1063
1064 #[test]
1065 fn circle_center_filled_edge_antialiased() {
1066 let mut fb = fresh(20, 20);
1067 {
1068 let mut c = Canvas::new(&mut fb);
1069 c.fill_circle(10.0, 10.0, 6.0, Color(255, 255, 255, 255));
1070 }
1071 assert_eq!(fb.get_rgba(10, 10), Some((255, 255, 255, 255)));
1073 assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
1075 let (r, _, _, _) = fb.get_rgba(10, 4).expect("edge pixel near top of circle");
1077 assert!(r > 0, "edge pixel should have received some coverage");
1078 }
1079
1080 #[test]
1081 fn rounded_rect_corner_softer_than_center() {
1082 let mut fb = fresh(40, 40);
1083 {
1084 let mut c = Canvas::new(&mut fb);
1085 c.fill_rounded_rect(5.0, 5.0, 30.0, 30.0, 8.0, Color(255, 255, 255, 255));
1086 }
1087 assert_eq!(fb.get_rgba(20, 20), Some((255, 255, 255, 255)));
1089 assert_eq!(fb.get_rgba(5, 5), Some((0, 0, 0, 255)));
1091 }
1092
1093 #[test]
1094 fn fill_rect_color_matches() {
1095 let mut fb = fresh(10, 10);
1096 {
1097 let mut c = Canvas::new(&mut fb);
1098 c.fill_rect(0.0, 0.0, 10.0, 10.0, Color(200, 100, 50, 255));
1099 }
1100 let (r, g, b, a) = fb.get_rgba(5, 5).expect("center pixel");
1102 assert_eq!((r, g, b, a), (200, 100, 50, 255));
1103 }
1104
1105 #[test]
1106 fn rounded_rect_corners_aa() {
1107 let mut fb = fresh(30, 30);
1108 {
1109 let mut c = Canvas::new(&mut fb);
1110 c.fill_rounded_rect(2.0, 2.0, 26.0, 26.0, 8.0, Color(255, 255, 255, 255));
1111 }
1112 let (r, _, _, _) = fb.get_rgba(2, 2).expect("corner pixel");
1114 assert!(r < 255, "corner should be anti-aliased (r={r})");
1117 }
1118
1119 #[test]
1120 fn wu_line_edge_alpha() {
1121 let mut fb = fresh(20, 20);
1123 {
1124 let mut c = Canvas::new(&mut fb);
1125 c.draw_line_wu(0.0, 0.3, 10.0, 10.3, Color(255, 255, 255, 255));
1126 }
1127 let mut found_partial = false;
1129 for y in 0..20 {
1130 for x in 0..20 {
1131 let (r, _, _, _) = fb.get_rgba(x, y).unwrap_or((0, 0, 0, 0));
1132 if r > 0 && r < 255 {
1133 found_partial = true;
1134 }
1135 }
1136 }
1137 assert!(
1138 found_partial,
1139 "Wu line should produce partially-alpha pixels"
1140 );
1141 }
1142
1143 #[test]
1144 fn thick_line_covers_area() {
1145 let mut fb = fresh(30, 30);
1146 {
1147 let mut c = Canvas::new(&mut fb);
1148 c.draw_line_thick(0.0, 15.0, 29.0, 15.0, 8.0, Color(255, 0, 0, 255));
1150 }
1151 let (r12, _, _, _) = fb.get_rgba(14, 12).unwrap_or((0, 0, 0, 0));
1153 let (r18, _, _, _) = fb.get_rgba(14, 18).unwrap_or((0, 0, 0, 0));
1154 assert!(
1155 r12 > 0,
1156 "pixel at y=12 should be inside thick line (r={r12})"
1157 );
1158 assert!(
1159 r18 > 0,
1160 "pixel at y=18 should be inside thick line (r={r18})"
1161 );
1162 }
1163
1164 #[test]
1165 fn dashed_line_has_gaps() {
1166 let mut fb = fresh(40, 5);
1167 {
1168 let mut c = Canvas::new(&mut fb);
1169 c.draw_line_dashed(
1170 0.0,
1171 2.0,
1172 39.0,
1173 2.0,
1174 Color(255, 255, 255, 255),
1175 DashPattern::new(5.0, 5.0),
1176 );
1177 }
1178 let mut painted = 0u32;
1180 let mut unpainted = 0u32;
1181 for x in 0..40 {
1182 let (r, _, _, _) = fb.get_rgba(x, 2).unwrap_or((0, 0, 0, 0));
1183 if r > 0 {
1184 painted += 1;
1185 } else {
1186 unpainted += 1;
1187 }
1188 }
1189 assert!(painted > 0, "should have some painted pixels");
1190 assert!(unpainted > 0, "should have some gap pixels");
1191 }
1192
1193 #[test]
1194 fn ellipse_fill() {
1195 let mut fb = fresh(30, 30);
1196 {
1197 let mut c = Canvas::new(&mut fb);
1198 c.fill_ellipse(15.0, 15.0, 10.0, 5.0, Color(0, 255, 0, 255));
1199 }
1200 let (_, g, _, _) = fb.get_rgba(15, 15).expect("centre");
1202 assert!(g > 0, "ellipse centre should be green");
1203 let (_, g2, _, _) = fb.get_rgba(0, 0).expect("corner");
1205 assert_eq!(g2, 0, "corner should not be painted");
1206 }
1207
1208 #[test]
1209 fn per_corner_rounded_rect() {
1210 let mut fb = fresh(30, 30);
1211 {
1212 let mut c = Canvas::new(&mut fb);
1213 c.fill_rounded_rect_per_corner(
1215 2.0,
1216 2.0,
1217 26.0,
1218 26.0,
1219 [10.0, 10.0, 10.0, 10.0],
1220 Color(255, 255, 255, 255),
1221 );
1222 }
1223 let (r, _, _, _) = fb.get_rgba(15, 15).expect("centre");
1225 assert_eq!(r, 255, "centre should be fully painted");
1226 }
1227
1228 #[test]
1229 fn bilinear_2x2_to_4x4() {
1230 let src_data = [
1233 255u8, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, ];
1236 let mut fb = fresh(4, 4);
1237 {
1238 let mut c = Canvas::new(&mut fb);
1239 c.blit_bilinear(SrcImage::new(&src_data, 2, 2), 0.0, 0.0, 4, 4);
1240 }
1241 let (r, g, b, _) = fb.get_rgba(0, 0).expect("(0,0)");
1243 assert!(
1244 r > g && r > b,
1245 "top-left should be reddish (r={r}, g={g}, b={b})"
1246 );
1247 let (r2, g2, b2, _) = fb.get_rgba(3, 0).expect("(3,0)");
1249 assert!(
1250 g2 > r2 && g2 > b2,
1251 "top-right should be greenish (r={r2}, g={g2}, b={b2})"
1252 );
1253 }
1254
1255 #[test]
1256 fn draw_quad_bezier_produces_pixels() {
1257 let mut fb = fresh(30, 30);
1258 {
1259 let mut c = Canvas::new(&mut fb);
1260 c.draw_quad_bezier(
1262 (0.0, 15.0),
1263 (15.0, 0.0),
1264 (29.0, 15.0),
1265 Color(255, 255, 255, 255),
1266 );
1267 }
1268 let mut painted = 0u32;
1270 for y in 0..30 {
1271 for x in 0..30 {
1272 let (r, _, _, _) = fb.get_rgba(x, y).unwrap_or((0, 0, 0, 0));
1273 if r > 0 {
1274 painted += 1;
1275 }
1276 }
1277 }
1278 assert!(
1279 painted >= 5,
1280 "quad bezier should produce >= 5 painted pixels, got {painted}"
1281 );
1282 }
1283
1284 #[test]
1285 fn draw_cubic_bezier_produces_pixels() {
1286 let mut fb = fresh(40, 40);
1287 {
1288 let mut c = Canvas::new(&mut fb);
1289 c.draw_cubic_bezier(
1291 (0.0, 20.0),
1292 (10.0, 0.0),
1293 (30.0, 40.0),
1294 (39.0, 20.0),
1295 Color(0, 255, 0, 255),
1296 );
1297 }
1298 let mut painted = 0u32;
1299 for y in 0..40 {
1300 for x in 0..40 {
1301 let (_, g, _, _) = fb.get_rgba(x, y).unwrap_or((0, 0, 0, 0));
1302 if g > 0 {
1303 painted += 1;
1304 }
1305 }
1306 }
1307 assert!(
1308 painted >= 5,
1309 "cubic bezier should produce >= 5 painted pixels, got {painted}"
1310 );
1311 }
1312
1313 #[test]
1314 fn blit_rgba_nearest_scale() {
1315 let mut fb = fresh(8, 8);
1316 let src = [
1318 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, ];
1321 {
1322 let mut c = Canvas::new(&mut fb);
1323 c.blit_rgba(SrcImage::new(&src, 2, 2), 0.0, 0.0, 4, 4);
1324 }
1325 assert_eq!(fb.get_rgba(0, 0), Some((255, 0, 0, 255)));
1327 assert_eq!(fb.get_rgba(1, 1), Some((255, 0, 0, 255)));
1328 assert_eq!(fb.get_rgba(3, 3), Some((255, 255, 255, 255)));
1330 }
1331}