1use blockworx_geom::{Angle, Pos2, Rect};
11use blockworx_paint::text::TextRow;
12use blockworx_paint::{Color, DrawOp, FontChoice, record::Stroke};
13use web_sys::CanvasRenderingContext2d;
14
15use crate::color::Swatches;
16use crate::faces::{self, Faces, Ready};
17use crate::images::Images;
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
22pub enum Repaint {
23 #[default]
24 Settled,
25 Owed,
26}
27
28impl Repaint {
29 #[must_use]
32 pub fn and(self, other: Self) -> Self {
33 if self == Self::Owed || other == Self::Owed {
34 Self::Owed
35 } else {
36 Self::Settled
37 }
38 }
39}
40
41pub fn replay(
43 draw_list: &[DrawOp],
44 ctx: &CanvasRenderingContext2d,
45 images: &Images,
46 faces: &Faces,
47 typeface: FontChoice,
48) -> Repaint {
49 let swatches = Swatches::default();
50 let ready = faces.ready(typeface);
51 ctx.set_line_join("round");
54 ctx.set_line_cap("round");
55 let mut owed = Repaint::Settled;
56 for op in draw_list {
57 match op {
58 DrawOp::Rect {
59 rect,
60 rounding,
61 fill,
62 stroke,
63 } => {
64 ctx.begin_path();
65 outline_rect(ctx, *rect, *rounding);
66 paint(ctx, &swatches, *fill, *stroke);
67 }
68 DrawOp::LineSegment { points, stroke } => {
69 ctx.begin_path();
70 ctx.move_to(points[0].x.into(), points[0].y.into());
71 ctx.line_to(points[1].x.into(), points[1].y.into());
72 paint(ctx, &swatches, Color::TRANSPARENT, *stroke);
73 }
74 DrawOp::Line { points, stroke } => {
75 ctx.begin_path();
76 polyline(ctx, points);
77 paint(ctx, &swatches, Color::TRANSPARENT, *stroke);
78 }
79 DrawOp::Circle {
80 center,
81 radius,
82 fill,
83 stroke,
84 } => {
85 ctx.begin_path();
86 let _ = ctx.arc(
87 center.x.into(),
88 center.y.into(),
89 f64::from(radius.max(0.0)),
90 0.0,
91 std::f64::consts::TAU,
92 );
93 paint(ctx, &swatches, *fill, *stroke);
94 }
95 DrawOp::ConvexPolygon {
96 points,
97 fill,
98 stroke,
99 } => {
100 ctx.begin_path();
101 polyline(ctx, points);
102 ctx.close_path();
103 paint(ctx, &swatches, *fill, *stroke);
104 }
105 DrawOp::Text {
106 rows,
107 font,
108 color,
109 angle,
110 ..
111 } => match ready {
112 Ready::Yes => {
113 if color.a() > 0 {
114 ctx.set_font(&faces::css(typeface, font.size));
115 ctx.set_fill_style_str(&swatches.css(*color));
116 for row in rows {
117 text_row(ctx, row, *angle);
118 }
119 }
120 }
121 Ready::NotYet => owed = Repaint::Owed,
124 Ready::Never => {}
125 },
126 DrawOp::Image { rect, hash } => owed = owed.and(images.draw(ctx, *rect, *hash)),
127 }
128 }
129 owed
130}
131
132fn paint(ctx: &CanvasRenderingContext2d, swatches: &Swatches, fill: Color, stroke: Stroke) {
136 if fill.a() > 0 {
137 ctx.set_fill_style_str(&swatches.css(fill));
138 ctx.fill();
139 }
140 if stroke.width > 0.0 && stroke.color.a() > 0 {
141 ctx.set_line_width(stroke.width.into());
142 ctx.set_stroke_style_str(&swatches.css(stroke.color));
143 ctx.stroke();
144 }
145}
146
147fn outline_rect(ctx: &CanvasRenderingContext2d, rect: Rect, rounding: f32) {
150 let (x, y, w, h) = (
151 f64::from(rect.min.x),
152 f64::from(rect.min.y),
153 f64::from(rect.width()),
154 f64::from(rect.height()),
155 );
156 let radius = f64::from(rounding.max(0.0))
157 .min(w.abs() / 2.0)
158 .min(h.abs() / 2.0);
159 if radius <= 0.0 {
160 ctx.rect(x, y, w, h);
161 } else if ctx.round_rect_with_f64(x, y, w, h, radius).is_err() {
162 ctx.move_to(x + radius, y);
163 let _ = ctx.arc_to(x + w, y, x + w, y + h, radius);
164 let _ = ctx.arc_to(x + w, y + h, x, y + h, radius);
165 let _ = ctx.arc_to(x, y + h, x, y, radius);
166 let _ = ctx.arc_to(x, y, x + w, y, radius);
167 ctx.close_path();
168 }
169}
170
171fn polyline(ctx: &CanvasRenderingContext2d, points: &[Pos2]) {
172 let mut points = points.iter();
173 let Some(first) = points.next() else {
174 return;
175 };
176 ctx.move_to(first.x.into(), first.y.into());
177 for point in points {
178 ctx.line_to(point.x.into(), point.y.into());
179 }
180}
181
182fn text_row(ctx: &CanvasRenderingContext2d, row: &TextRow, angle: Angle) {
186 ctx.save();
187 let _ = ctx.translate(row.pen.x.into(), row.pen.y.into());
190 if angle != Angle::ZERO {
191 let _ = ctx.rotate(f32::from(angle).into());
192 }
193 let _ = ctx.fill_text(&row.text, 0.0, 0.0);
194 ctx.restore();
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn one_mark_that_is_not_ready_owes_the_whole_frame_a_repaint() {
203 assert_eq!(Repaint::Settled.and(Repaint::Settled), Repaint::Settled);
204 assert_eq!(Repaint::Settled.and(Repaint::Owed), Repaint::Owed);
205 assert_eq!(Repaint::Owed.and(Repaint::Settled), Repaint::Owed);
206 assert_eq!(Repaint::default(), Repaint::Settled);
207 }
208}