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