Skip to main content

euv_chart/backend/canvas2d/
impl.rs

1use super::*;
2
3/// Inherent implementation of [`Canvas2dChartBackend`].
4impl Canvas2dChartBackend {
5    /// Creates a backend around the given canvas.
6    ///
7    /// Returns `None` when the canvas cannot give out a 2D context — in
8    /// practice, when the canvas already has a WebGPU or WebGL context.
9    ///
10    /// # Arguments
11    ///
12    /// - `&HtmlCanvasElement` - Shared reference to a `HtmlCanvasElement`.
13    ///
14    /// # Returns
15    ///
16    /// - `Option<Self>` - `Some(...)` on success, `None` otherwise.
17    pub fn create(canvas: &HtmlCanvasElement) -> Option<Self> {
18        let context: CanvasRenderingContext2d = canvas
19            .get_context("2d")
20            .ok()
21            .flatten()?
22            .dyn_into::<CanvasRenderingContext2d>()
23            .ok()?;
24        Some(Self {
25            canvas: canvas.clone(),
26            context,
27            dpr: 1.0,
28            width: 0.0,
29            height: 0.0,
30        })
31    }
32
33    /// Returns the canvas the backend draws into.
34    ///
35    /// # Returns
36    ///
37    /// - `&HtmlCanvasElement` - A `HtmlCanvasElement` value.
38    pub fn canvas(&self) -> &HtmlCanvasElement {
39        self.get_canvas()
40    }
41
42    /// Returns the device pixel ratio the backend renders at.
43    ///
44    /// # Returns
45    ///
46    /// - `f64` - A 64-bit float.
47    pub fn dpr(&self) -> f64 {
48        self.get_dpr()
49    }
50
51    /// Traces a path into the 2D context.
52    ///
53    /// # Arguments
54    ///
55    /// - `&Path` - Shared reference to a `Path`.
56    pub(crate) fn trace_path(&self, path: &Path) {
57        self.get_context().begin_path();
58        for command in path.commands() {
59            match *command {
60                PathCommand::MoveTo(x, y) => self.get_context().move_to(x as f64, y as f64),
61                PathCommand::LineTo(x, y) => self.get_context().line_to(x as f64, y as f64),
62                PathCommand::QuadTo(cx, cy, x, y) => self
63                    .get_context()
64                    .quadratic_curve_to(cx as f64, cy as f64, x as f64, y as f64),
65                PathCommand::CubicTo(c1x, c1y, c2x, c2y, x, y) => {
66                    self.get_context().bezier_curve_to(
67                        c1x as f64, c1y as f64, c2x as f64, c2y as f64, x as f64, y as f64,
68                    )
69                }
70                PathCommand::Close => self.get_context().close_path(),
71            }
72        }
73    }
74
75    /// Traces a triangle strip into the 2D context.
76    ///
77    /// # Arguments
78    ///
79    /// - `&[Point]` - Shared reference to a `[Point]`.
80    /// - `f32` - A 32-bit float (`f32`).
81    pub(crate) fn trace_strip(&self, points: &[Point], baseline: f32) {
82        self.get_context().begin_path();
83        if let Some(first) = points.first() {
84            self.get_context().move_to(first.x as f64, first.y as f64);
85            for point in &points[1..] {
86                self.get_context().line_to(point.x as f64, point.y as f64);
87            }
88            self.get_context()
89                .line_to(points[points.len() - 1].x as f64, baseline as f64);
90            self.get_context()
91                .line_to(points[0].x as f64, baseline as f64);
92            self.get_context().close_path();
93        }
94    }
95
96    /// Applies a fill paint to the context.
97    ///
98    /// # Arguments
99    ///
100    /// - `&Fill` - Shared reference to a `Fill`.
101    pub(crate) fn apply_fill(&self, paint: &Fill) {
102        match paint {
103            Fill::Solid(color) => {
104                self.get_context().set_fill_style_str(&color.to_css());
105            }
106            Fill::Linear(gradient) => {
107                if let Some(canvas) = self.create_gradient(gradient) {
108                    self.get_context().set_fill_style_canvas_gradient(&canvas);
109                }
110            }
111            Fill::Radial(gradient) => {
112                if let Some(canvas) = self.create_radial_gradient(gradient) {
113                    self.get_context().set_fill_style_canvas_gradient(&canvas);
114                }
115            }
116            Fill::None => {}
117        }
118    }
119
120    /// Applies a stroke paint to the context.
121    ///
122    /// # Arguments
123    ///
124    /// - `&Stroke` - Shared reference to a `Stroke`.
125    pub(crate) fn apply_stroke(&self, paint: &Stroke) {
126        match paint {
127            Stroke::Solid(color) => {
128                self.get_context().set_stroke_style_str(&color.to_css());
129            }
130            Stroke::Linear(gradient) => {
131                if let Some(canvas) = self.create_gradient(gradient) {
132                    self.get_context().set_stroke_style_canvas_gradient(&canvas);
133                }
134            }
135            Stroke::Radial(gradient) => {
136                if let Some(canvas) = self.create_radial_gradient(gradient) {
137                    self.get_context().set_stroke_style_canvas_gradient(&canvas);
138                }
139            }
140            Stroke::None => {}
141        }
142    }
143
144    /// Builds a Canvas2D linear gradient.
145    ///
146    /// # Arguments
147    ///
148    /// - `&LinearGradient` - Shared reference to a `LinearGradient`.
149    ///
150    /// # Returns
151    ///
152    /// - `Option<CanvasGradient>` - `Some(...)` on success, `None` otherwise.
153    fn create_gradient(&self, gradient: &LinearGradient) -> Option<CanvasGradient> {
154        let canvas: CanvasGradient = self.get_context().create_linear_gradient(
155            gradient.start.0,
156            gradient.start.1,
157            gradient.end.0,
158            gradient.end.1,
159        );
160        for stop in &gradient.stops {
161            let _: Result<(), JsValue> =
162                canvas.add_color_stop(stop.offset as f32, &stop.color.to_css());
163        }
164        Some(canvas)
165    }
166
167    /// Builds a Canvas2D radial gradient.
168    ///
169    /// # Arguments
170    ///
171    /// - `&RadialGradient` - Shared reference to a `RadialGradient`.
172    ///
173    /// # Returns
174    ///
175    /// - `Option<CanvasGradient>` - `Some(...)` on success, `None` otherwise.
176    fn create_radial_gradient(&self, gradient: &RadialGradient) -> Option<CanvasGradient> {
177        let canvas: CanvasGradient = self
178            .get_context()
179            .create_radial_gradient(
180                gradient.center.0,
181                gradient.center.1,
182                0.0,
183                gradient.center.0,
184                gradient.center.1,
185                gradient.radius,
186            )
187            .ok()?;
188        for stop in &gradient.stops {
189            let _: Result<(), JsValue> =
190                canvas.add_color_stop(stop.offset as f32, &stop.color.to_css());
191        }
192        Some(canvas)
193    }
194}
195
196/// Implements [`ChartBackend`] for [`Canvas2dChartBackend`].
197impl ChartBackend for Canvas2dChartBackend {
198    /// Implements the `kind` operation on [`Canvas2dChartBackend`].
199    ///
200    /// # Returns
201    ///
202    /// - `BackendKind` - A `BackendKind` value.
203    fn kind(&self) -> BackendKind {
204        BackendKind::Canvas2D
205    }
206
207    /// Implements the `resize` operation on [`Canvas2dChartBackend`].
208    ///
209    /// # Arguments
210    ///
211    /// - `f64` - A 64-bit float (`f64`).
212    fn resize(&mut self, width: f64, height: f64, dpr: f64) {
213        self.set_dpr(dpr);
214        self.set_width(width);
215        self.set_height(height);
216        let physical_width: u32 = (width * dpr).round().max(1.0) as u32;
217        let physical_height: u32 = (height * dpr).round().max(1.0) as u32;
218        if self.get_canvas().width() != physical_width {
219            self.get_canvas().set_width(physical_width);
220        }
221        if self.get_canvas().height() != physical_height {
222            self.get_canvas().set_height(physical_height);
223        }
224        // Setting the width or the height of a canvas resets its context
225        // state, so the transform has to be re-applied afterwards.
226        let _: Result<(), JsValue> = self
227            .get_context()
228            .set_transform(dpr, 0.0, 0.0, dpr, 0.0, 0.0);
229        self.get_context().set_line_join("miter");
230    }
231
232    /// Implements the `render` operation on [`Canvas2dChartBackend`].
233    ///
234    /// # Arguments
235    ///
236    /// - `&DisplayList` - Shared reference to a `DisplayList`.
237    /// - `&mut TextShaper` - Mutable reference to a `TextShaper` (mutated in place).
238    fn render(&mut self, list: &DisplayList, shaper: &mut TextShaper) {
239        let _: Result<(), JsValue> =
240            self.get_context()
241                .set_transform(self.get_dpr(), 0.0, 0.0, self.get_dpr(), 0.0, 0.0);
242        self.get_context()
243            .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
244        for item in list.items() {
245            match &item.cmd {
246                DrawCmd::Fill {
247                    path,
248                    paint,
249                    opacity,
250                } => {
251                    if matches!(paint, Fill::None) {
252                        continue;
253                    }
254                    self.get_context().save();
255                    self.get_context().set_global_alpha(*opacity);
256                    self.apply_fill(paint);
257                    self.trace_path(path);
258                    self.get_context().fill();
259                    self.get_context().restore();
260                }
261                DrawCmd::Stroke {
262                    path,
263                    paint,
264                    style,
265                    opacity,
266                } => {
267                    if matches!(paint, Stroke::None) || style.width <= 0.0 {
268                        continue;
269                    }
270                    self.get_context().save();
271                    self.get_context()
272                        .set_global_alpha(*opacity * style.opacity);
273                    self.apply_stroke(paint);
274                    self.get_context().set_line_width(style.width);
275                    self.get_context().set_line_cap(line_cap_name(style.cap));
276                    self.get_context().set_line_join(line_join_name(style.join));
277                    if !style.dash.is_solid() {
278                        let dash: Array = Array::new();
279                        for length in &style.dash.pattern {
280                            dash.push(&JsValue::from_f64(*length as f64));
281                        }
282                        let _: Result<(), JsValue> =
283                            self.get_context().set_line_dash(dash.as_ref());
284                        self.get_context()
285                            .set_line_dash_offset(style.dash.offset as f64);
286                    } else {
287                        let _: Result<(), JsValue> =
288                            self.get_context().set_line_dash(Array::new().as_ref());
289                    }
290                    self.trace_path(path);
291                    self.get_context().stroke();
292                    self.get_context().restore();
293                }
294                DrawCmd::Strip {
295                    points,
296                    baseline,
297                    paint,
298                    opacity,
299                } => {
300                    if matches!(paint, Fill::None) || points.len() < 2 {
301                        continue;
302                    }
303                    self.get_context().save();
304                    self.get_context().set_global_alpha(*opacity);
305                    self.apply_fill(paint);
306                    self.trace_strip(points, *baseline);
307                    self.get_context().fill();
308                    self.get_context().restore();
309                }
310                DrawCmd::Text { text, x, y, style } => {
311                    // The run is shaped with the same shaper the GPU
312                    // backends use, so the baseline lands on the same
313                    // pixel whichever backend is in use.
314                    let run: TextRun = shaper.shape(text, style, *x, *y);
315                    self.get_context().save();
316                    self.get_context().set_font(&style.font());
317                    self.get_context().set_text_align("left");
318                    self.get_context().set_text_baseline("alphabetic");
319                    for line in &run.lines {
320                        if let Some(background) = style.background {
321                            self.get_context().set_fill_style_str(&background.to_css());
322                            self.get_context().fill_rect(
323                                line.left - style.padding.3,
324                                line.baseline - run.ascent - style.padding.0,
325                                line.width + style.padding.1 + style.padding.3,
326                                run.ascent + run.descent + style.padding.0 + style.padding.2,
327                            );
328                        }
329                        self.get_context().set_fill_style_str(&style.color.to_css());
330                        let _: Result<(), JsValue> =
331                            self.get_context()
332                                .fill_text(&line.text, line.left, line.baseline);
333                    }
334                    self.get_context().restore();
335                }
336                DrawCmd::Image {
337                    image,
338                    x,
339                    y,
340                    width,
341                    height,
342                } => {
343                    let _: Result<(), JsValue> = self
344                        .get_context()
345                        .draw_image_with_html_image_element_and_dw_and_dh(
346                            image, *x, *y, *width, *height,
347                        );
348                }
349                DrawCmd::Clip { path } => {
350                    self.get_context().save();
351                    self.trace_path(path);
352                    self.get_context().clip();
353                }
354                DrawCmd::PopClip => {
355                    self.get_context().restore();
356                }
357            }
358        }
359    }
360
361    /// Implements the `dispose` operation on [`Canvas2dChartBackend`].
362    fn dispose(&mut self) {
363        let _: Result<(), JsValue> = self
364            .get_context()
365            .set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
366        self.get_context()
367            .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
368    }
369}