1 use js_sys::JSON;
2 use wasm_bindgen::{JsCast, JsValue};
3 use web_sys::{window, CanvasRenderingContext2d, HtmlCanvasElement};
4 
5 use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
6 use crate::style::text_anchor::{HPos, VPos};
7 use crate::style::{Color, FontTransform, RGBAColor, TextStyle};
8 
9 /// The backend that is drawing on the HTML canvas
10 /// TODO: Support double buffering
11 pub struct CanvasBackend {
12     canvas: HtmlCanvasElement,
13     context: CanvasRenderingContext2d,
14 }
15 
16 pub struct CanvasError(String);
17 
18 impl std::fmt::Display for CanvasError {
fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>19     fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
20         return write!(fmt, "Canvas Error: {}", self.0);
21     }
22 }
23 
24 impl std::fmt::Debug for CanvasError {
fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>25     fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
26         return write!(fmt, "CanvasError({})", self.0);
27     }
28 }
29 
30 impl From<JsValue> for DrawingErrorKind<CanvasError> {
from(e: JsValue) -> DrawingErrorKind<CanvasError>31     fn from(e: JsValue) -> DrawingErrorKind<CanvasError> {
32         DrawingErrorKind::DrawingError(CanvasError(
33             JSON::stringify(&e)
34                 .map(|s| Into::<String>::into(&s))
35                 .unwrap_or_else(|_| "Unknown".to_string()),
36         ))
37     }
38 }
39 
40 impl std::error::Error for CanvasError {}
41 
42 impl CanvasBackend {
init_backend(canvas: HtmlCanvasElement) -> Option<Self>43     fn init_backend(canvas: HtmlCanvasElement) -> Option<Self> {
44         let context: CanvasRenderingContext2d = canvas.get_context("2d").ok()??.dyn_into().ok()?;
45         Some(CanvasBackend { canvas, context })
46     }
47 
48     /// Create a new drawing backend backed with an HTML5 canvas object with given Id
49     /// - `elem_id` The element id for the canvas
50     /// - Return either some drawing backend has been created, or none in error case
new(elem_id: &str) -> Option<Self>51     pub fn new(elem_id: &str) -> Option<Self> {
52         let document = window()?.document()?;
53         let canvas = document.get_element_by_id(elem_id)?;
54         let canvas: HtmlCanvasElement = canvas.dyn_into().ok()?;
55         Self::init_backend(canvas)
56     }
57 
58     /// Create a new drawing backend backend with a HTML5 canvas object passed in
59     /// - `canvas` The object we want to use as backend
60     /// - Return either the drawing backend or None for error
with_canvas_object(canvas: HtmlCanvasElement) -> Option<Self>61     pub fn with_canvas_object(canvas: HtmlCanvasElement) -> Option<Self> {
62         Self::init_backend(canvas)
63     }
64 }
65 
make_canvas_color(color: RGBAColor) -> JsValue66 fn make_canvas_color(color: RGBAColor) -> JsValue {
67     let (r, g, b) = color.rgb();
68     let a = color.alpha();
69     format!("rgba({},{},{},{})", r, g, b, a).into()
70 }
71 
72 impl DrawingBackend for CanvasBackend {
73     type ErrorType = CanvasError;
74 
get_size(&self) -> (u32, u32)75     fn get_size(&self) -> (u32, u32) {
76         // Getting just canvas.width gives poor results on HighDPI screens.
77         let rect = self.canvas.get_bounding_client_rect();
78         (rect.width() as u32, rect.height() as u32)
79     }
80 
ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<CanvasError>>81     fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
82         Ok(())
83     }
84 
present(&mut self) -> Result<(), DrawingErrorKind<CanvasError>>85     fn present(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
86         Ok(())
87     }
88 
draw_pixel( &mut self, point: BackendCoord, style: &RGBAColor, ) -> Result<(), DrawingErrorKind<CanvasError>>89     fn draw_pixel(
90         &mut self,
91         point: BackendCoord,
92         style: &RGBAColor,
93     ) -> Result<(), DrawingErrorKind<CanvasError>> {
94         if style.alpha() == 0.0 {
95             return Ok(());
96         }
97 
98         self.context
99             .set_fill_style(&make_canvas_color(style.as_color()));
100         self.context
101             .fill_rect(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
102         Ok(())
103     }
104 
draw_line<S: BackendStyle>( &mut self, from: BackendCoord, to: BackendCoord, style: &S, ) -> Result<(), DrawingErrorKind<Self::ErrorType>>105     fn draw_line<S: BackendStyle>(
106         &mut self,
107         from: BackendCoord,
108         to: BackendCoord,
109         style: &S,
110     ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
111         if style.as_color().alpha() == 0.0 {
112             return Ok(());
113         }
114 
115         self.context
116             .set_stroke_style(&make_canvas_color(style.as_color()));
117         self.context.begin_path();
118         self.context.move_to(f64::from(from.0), f64::from(from.1));
119         self.context.line_to(f64::from(to.0), f64::from(to.1));
120         self.context.stroke();
121         Ok(())
122     }
123 
draw_rect<S: BackendStyle>( &mut self, upper_left: BackendCoord, bottom_right: BackendCoord, style: &S, fill: bool, ) -> Result<(), DrawingErrorKind<Self::ErrorType>>124     fn draw_rect<S: BackendStyle>(
125         &mut self,
126         upper_left: BackendCoord,
127         bottom_right: BackendCoord,
128         style: &S,
129         fill: bool,
130     ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
131         if style.as_color().alpha() == 0.0 {
132             return Ok(());
133         }
134         if fill {
135             self.context
136                 .set_fill_style(&make_canvas_color(style.as_color()));
137             self.context.fill_rect(
138                 f64::from(upper_left.0),
139                 f64::from(upper_left.1),
140                 f64::from(bottom_right.0 - upper_left.0),
141                 f64::from(bottom_right.1 - upper_left.1),
142             );
143         } else {
144             self.context
145                 .set_stroke_style(&make_canvas_color(style.as_color()));
146             self.context.stroke_rect(
147                 f64::from(upper_left.0),
148                 f64::from(upper_left.1),
149                 f64::from(bottom_right.0 - upper_left.0),
150                 f64::from(bottom_right.1 - upper_left.1),
151             );
152         }
153         Ok(())
154     }
155 
draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>( &mut self, path: I, style: &S, ) -> Result<(), DrawingErrorKind<Self::ErrorType>>156     fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
157         &mut self,
158         path: I,
159         style: &S,
160     ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
161         if style.as_color().alpha() == 0.0 {
162             return Ok(());
163         }
164         let mut path = path.into_iter();
165         self.context.begin_path();
166         if let Some(start) = path.next() {
167             self.context
168                 .set_stroke_style(&make_canvas_color(style.as_color()));
169             self.context.move_to(f64::from(start.0), f64::from(start.1));
170             for next in path {
171                 self.context.line_to(f64::from(next.0), f64::from(next.1));
172             }
173         }
174         self.context.stroke();
175         Ok(())
176     }
177 
fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>( &mut self, path: I, style: &S, ) -> Result<(), DrawingErrorKind<Self::ErrorType>>178     fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
179         &mut self,
180         path: I,
181         style: &S,
182     ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
183         if style.as_color().alpha() == 0.0 {
184             return Ok(());
185         }
186         let mut path = path.into_iter();
187         self.context.begin_path();
188         if let Some(start) = path.next() {
189             self.context
190                 .set_fill_style(&make_canvas_color(style.as_color()));
191             self.context.move_to(f64::from(start.0), f64::from(start.1));
192             for next in path {
193                 self.context.line_to(f64::from(next.0), f64::from(next.1));
194             }
195             self.context.close_path();
196         }
197         self.context.fill();
198         Ok(())
199     }
200 
draw_circle<S: BackendStyle>( &mut self, center: BackendCoord, radius: u32, style: &S, fill: bool, ) -> Result<(), DrawingErrorKind<Self::ErrorType>>201     fn draw_circle<S: BackendStyle>(
202         &mut self,
203         center: BackendCoord,
204         radius: u32,
205         style: &S,
206         fill: bool,
207     ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
208         if style.as_color().alpha() == 0.0 {
209             return Ok(());
210         }
211         if fill {
212             self.context
213                 .set_fill_style(&make_canvas_color(style.as_color()));
214         } else {
215             self.context
216                 .set_stroke_style(&make_canvas_color(style.as_color()));
217         }
218         self.context.begin_path();
219         self.context.arc(
220             f64::from(center.0),
221             f64::from(center.1),
222             f64::from(radius),
223             0.0,
224             std::f64::consts::PI * 2.0,
225         )?;
226         if fill {
227             self.context.fill();
228         } else {
229             self.context.stroke();
230         }
231         Ok(())
232     }
233 
draw_text( &mut self, text: &str, style: &TextStyle, pos: BackendCoord, ) -> Result<(), DrawingErrorKind<Self::ErrorType>>234     fn draw_text(
235         &mut self,
236         text: &str,
237         style: &TextStyle,
238         pos: BackendCoord,
239     ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
240         let font = &style.font;
241         let color = &style.color;
242         if color.alpha() == 0.0 {
243             return Ok(());
244         }
245 
246         let (mut x, mut y) = (pos.0, pos.1);
247 
248         let degree = match font.get_transform() {
249             FontTransform::None => 0.0,
250             FontTransform::Rotate90 => 90.0,
251             FontTransform::Rotate180 => 180.0,
252             FontTransform::Rotate270 => 270.0,
253         } / 180.0
254             * std::f64::consts::PI;
255 
256         if degree != 0.0 {
257             self.context.save();
258             self.context.translate(f64::from(x), f64::from(y))?;
259             self.context.rotate(degree)?;
260             x = 0;
261             y = 0;
262         }
263 
264         let text_baseline = match style.pos.v_pos {
265             VPos::Top => "top",
266             VPos::Center => "middle",
267             VPos::Bottom => "bottom",
268         };
269         self.context.set_text_baseline(text_baseline);
270 
271         let text_align = match style.pos.h_pos {
272             HPos::Left => "start",
273             HPos::Right => "end",
274             HPos::Center => "center",
275         };
276         self.context.set_text_align(text_align);
277 
278         self.context
279             .set_fill_style(&make_canvas_color(color.clone()));
280         self.context.set_font(&format!(
281             "{} {}px {}",
282             font.get_style().as_str(),
283             font.get_size(),
284             font.get_name()
285         ));
286         self.context.fill_text(text, f64::from(x), f64::from(y))?;
287 
288         if degree != 0.0 {
289             self.context.restore();
290         }
291 
292         Ok(())
293     }
294 }
295 
296 #[cfg(test)]
297 mod test {
298     use super::*;
299     use crate::element::Circle;
300     use crate::prelude::*;
301     use crate::style::text_anchor::Pos;
302     use wasm_bindgen_test::wasm_bindgen_test_configure;
303     use wasm_bindgen_test::*;
304     use web_sys::Document;
305 
306     wasm_bindgen_test_configure!(run_in_browser);
307 
create_canvas(document: &Document, id: &str, width: u32, height: u32) -> HtmlCanvasElement308     fn create_canvas(document: &Document, id: &str, width: u32, height: u32) -> HtmlCanvasElement {
309         let canvas = document
310             .create_element("canvas")
311             .unwrap()
312             .dyn_into::<HtmlCanvasElement>()
313             .unwrap();
314         let div = document.create_element("div").unwrap();
315         div.append_child(&canvas).unwrap();
316         document.body().unwrap().append_child(&div).unwrap();
317         canvas.set_attribute("id", id).unwrap();
318         canvas.set_width(width);
319         canvas.set_height(height);
320         canvas
321     }
322 
check_content(document: &Document, id: &str)323     fn check_content(document: &Document, id: &str) {
324         let canvas = document
325             .get_element_by_id(id)
326             .unwrap()
327             .dyn_into::<HtmlCanvasElement>()
328             .unwrap();
329         let data_uri = canvas.to_data_url().unwrap();
330         let prefix = "data:image/png;base64,";
331         assert!(&data_uri.starts_with(prefix));
332     }
333 
draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str)334     fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
335         let document = window().unwrap().document().unwrap();
336         let canvas = create_canvas(&document, test_name, 500, 500);
337         let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
338         let root = backend.into_drawing_area();
339 
340         let mut chart = ChartBuilder::on(&root)
341             .caption("This is a test", ("sans-serif", 20))
342             .set_all_label_area_size(40)
343             .build_ranged(0..10, 0..10)
344             .unwrap();
345 
346         chart
347             .configure_mesh()
348             .set_all_tick_mark_size(tick_size)
349             .draw()
350             .unwrap();
351 
352         check_content(&document, test_name);
353     }
354 
355     #[wasm_bindgen_test]
test_draw_mesh_no_ticks()356     fn test_draw_mesh_no_ticks() {
357         draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
358     }
359 
360     #[wasm_bindgen_test]
test_draw_mesh_negative_ticks()361     fn test_draw_mesh_negative_ticks() {
362         draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
363     }
364 
365     #[wasm_bindgen_test]
test_text_draw()366     fn test_text_draw() {
367         let document = window().unwrap().document().unwrap();
368         let canvas = create_canvas(&document, "test_text_draw", 1500, 800);
369         let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
370         let root = backend.into_drawing_area();
371         let root = root
372             .titled("Image Title", ("sans-serif", 60).into_font())
373             .unwrap();
374 
375         let mut chart = ChartBuilder::on(&root)
376             .caption("All anchor point positions", ("sans-serif", 20))
377             .set_all_label_area_size(40)
378             .build_ranged(0..100, 0..50)
379             .unwrap();
380 
381         chart
382             .configure_mesh()
383             .disable_x_mesh()
384             .disable_y_mesh()
385             .x_desc("X Axis")
386             .y_desc("Y Axis")
387             .draw()
388             .unwrap();
389 
390         let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
391 
392         for (dy, trans) in [
393             FontTransform::None,
394             FontTransform::Rotate90,
395             FontTransform::Rotate180,
396             FontTransform::Rotate270,
397         ]
398         .iter()
399         .enumerate()
400         {
401             for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
402                 for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
403                     let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
404                     let y = 120 + dy as i32 * 150;
405                     let draw = |x, y, text| {
406                         root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
407                         let style = TextStyle::from(("sans-serif", 20).into_font())
408                             .pos(Pos::new(*h_pos, *v_pos))
409                             .transform(trans.clone());
410                         root.draw_text(text, &style, (x, y)).unwrap();
411                     };
412                     draw(x + x1, y + y1, "dood");
413                     draw(x + x2, y + y2, "dog");
414                     draw(x + x3, y + y3, "goog");
415                 }
416             }
417         }
418         check_content(&document, "test_text_draw");
419     }
420 
421     #[wasm_bindgen_test]
test_text_clipping()422     fn test_text_clipping() {
423         let (width, height) = (500_i32, 500_i32);
424         let document = window().unwrap().document().unwrap();
425         let canvas = create_canvas(&document, "test_text_clipping", width as u32, height as u32);
426         let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
427         let root = backend.into_drawing_area();
428 
429         let style = TextStyle::from(("sans-serif", 20).into_font())
430             .pos(Pos::new(HPos::Center, VPos::Center));
431         root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
432         root.draw_text("TOP CENTER", &style, (width / 2, 0))
433             .unwrap();
434         root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
435 
436         root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
437             .unwrap();
438         root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
439             .unwrap();
440 
441         root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
442         root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
443             .unwrap();
444         root.draw_text("BOTTOM RIGHT", &style, (width, height))
445             .unwrap();
446 
447         check_content(&document, "test_text_clipping");
448     }
449 
450     #[wasm_bindgen_test]
test_series_labels()451     fn test_series_labels() {
452         let (width, height) = (500, 500);
453         let document = window().unwrap().document().unwrap();
454         let canvas = create_canvas(&document, "test_series_labels", width, height);
455         let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
456         let root = backend.into_drawing_area();
457 
458         let mut chart = ChartBuilder::on(&root)
459             .caption("All series label positions", ("sans-serif", 20))
460             .set_all_label_area_size(40)
461             .build_ranged(0..50, 0..50)
462             .unwrap();
463 
464         chart
465             .configure_mesh()
466             .disable_x_mesh()
467             .disable_y_mesh()
468             .draw()
469             .unwrap();
470 
471         chart
472             .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
473             .expect("Drawing error")
474             .label("Series 1")
475             .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
476 
477         chart
478             .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
479             .expect("Drawing error")
480             .label("Series 2")
481             .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
482 
483         for pos in vec![
484             SeriesLabelPosition::UpperLeft,
485             SeriesLabelPosition::MiddleLeft,
486             SeriesLabelPosition::LowerLeft,
487             SeriesLabelPosition::UpperMiddle,
488             SeriesLabelPosition::MiddleMiddle,
489             SeriesLabelPosition::LowerMiddle,
490             SeriesLabelPosition::UpperRight,
491             SeriesLabelPosition::MiddleRight,
492             SeriesLabelPosition::LowerRight,
493             SeriesLabelPosition::Coordinate(70, 70),
494         ]
495         .into_iter()
496         {
497             chart
498                 .configure_series_labels()
499                 .border_style(&BLACK.mix(0.5))
500                 .position(pos)
501                 .draw()
502                 .expect("Drawing error");
503         }
504 
505         check_content(&document, "test_series_labels");
506     }
507 
508     #[wasm_bindgen_test]
test_draw_pixel_alphas()509     fn test_draw_pixel_alphas() {
510         let (width, height) = (100_i32, 100_i32);
511         let document = window().unwrap().document().unwrap();
512         let canvas = create_canvas(
513             &document,
514             "test_draw_pixel_alphas",
515             width as u32,
516             height as u32,
517         );
518         let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
519         let root = backend.into_drawing_area();
520 
521         for i in -20..20 {
522             let alpha = i as f64 * 0.1;
523             root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
524                 .unwrap();
525         }
526 
527         check_content(&document, "test_draw_pixel_alphas");
528     }
529 }
530