1#![deny(missing_docs)]
31
32pub use plotkit_core::primitives::*;
34pub use plotkit_core::renderer::Renderer;
35
36pub fn color_to_css(c: &Color) -> String {
54 if c.a == 255 {
55 format!("rgba({},{},{},1)", c.r, c.g, c.b)
56 } else {
57 format!("rgba({},{},{},{:.4})", c.r, c.g, c.b, c.a as f64 / 255.0)
58 }
59}
60
61pub fn build_font_string(style: &TextStyle) -> String {
81 let weight = match style.weight {
82 FontWeight::Normal => "",
83 FontWeight::Bold => "bold ",
84 };
85 let family = style.family.as_deref().unwrap_or("sans-serif");
86 format!("{}{:.0}px {}", weight, style.size, family)
87}
88
89pub fn halign_to_canvas(align: HAlign) -> &'static str {
94 match align {
95 HAlign::Left => "left",
96 HAlign::Center => "center",
97 HAlign::Right => "right",
98 }
99}
100
101pub fn valign_to_canvas(align: VAlign) -> &'static str {
106 match align {
107 VAlign::Top => "top",
108 VAlign::Middle => "middle",
109 VAlign::Bottom => "bottom",
110 VAlign::Baseline => "alphabetic",
111 }
112}
113
114pub fn stroke_cap_to_canvas(cap: StrokeCap) -> &'static str {
116 match cap {
117 StrokeCap::Butt => "butt",
118 StrokeCap::Round => "round",
119 StrokeCap::Square => "square",
120 }
121}
122
123pub fn stroke_join_to_canvas(join: StrokeJoin) -> &'static str {
125 match join {
126 StrokeJoin::Miter => "miter",
127 StrokeJoin::Round => "round",
128 StrokeJoin::Bevel => "bevel",
129 }
130}
131
132pub fn count_path_elements(path: &Path) -> (usize, usize, usize, usize, usize) {
137 let mut m = 0;
138 let mut l = 0;
139 let mut q = 0;
140 let mut c = 0;
141 let mut z = 0;
142 for el in &path.elements {
143 match el {
144 PathEl::MoveTo(_) => m += 1,
145 PathEl::LineTo(_) => l += 1,
146 PathEl::QuadTo(_, _) => q += 1,
147 PathEl::CurveTo(_, _, _) => c += 1,
148 PathEl::ClosePath => z += 1,
149 }
150 }
151 (m, l, q, c, z)
152}
153
154pub fn affine_to_canvas_params(affine: Affine) -> [f64; 6] {
168 affine.as_coeffs()
169}
170
171pub fn estimate_text_width(text: &str, style: &TextStyle) -> f64 {
179 let factor = match style.weight {
180 FontWeight::Normal => 0.6,
181 FontWeight::Bold => 0.65,
182 };
183 text.len() as f64 * style.size * factor
184}
185
186pub fn dash_pattern_values(stroke: &Stroke) -> Vec<f64> {
192 match &stroke.dash {
193 Some(pattern) => pattern.dashes.clone(),
194 None => Vec::new(),
195 }
196}
197
198#[cfg(target_arch = "wasm32")]
203mod wasm_impl {
204 use super::*;
207 use js_sys::Array;
208 use wasm_bindgen::prelude::*;
209 use web_sys::CanvasRenderingContext2d;
210
211 pub struct WasmRenderer {
225 ctx: CanvasRenderingContext2d,
226 width: u32,
227 height: u32,
228 }
229
230 impl WasmRenderer {
231 pub fn new(ctx: CanvasRenderingContext2d, width: u32, height: u32) -> Self {
237 Self { ctx, width, height }
238 }
239
240 pub fn context(&self) -> &CanvasRenderingContext2d {
242 &self.ctx
243 }
244
245 fn trace_path(&self, path: &Path) {
251 self.ctx.begin_path();
252 for el in &path.elements {
253 match *el {
254 PathEl::MoveTo(p) => {
255 self.ctx.move_to(p.x, p.y);
256 }
257 PathEl::LineTo(p) => {
258 self.ctx.line_to(p.x, p.y);
259 }
260 PathEl::QuadTo(cp, end) => {
261 self.ctx.quadratic_curve_to(cp.x, cp.y, end.x, end.y);
262 }
263 PathEl::CurveTo(cp1, cp2, end) => {
264 self.ctx
265 .bezier_curve_to(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
266 }
267 PathEl::ClosePath => {
268 self.ctx.close_path();
269 }
270 }
271 }
272 }
273
274 fn apply_transform(&self, transform: Affine) {
276 let [a, b, c, d, e, f] = affine_to_canvas_params(transform);
277 let _ = self.ctx.set_transform(a, b, c, d, e, f);
278 }
279
280 fn reset_transform(&self) {
282 let _ = self.ctx.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
283 }
284
285 fn configure_stroke(&self, paint: &Paint, stroke: &Stroke) {
287 let color = color_to_css(&paint.color);
288 self.ctx.set_stroke_style_str(&color);
289 self.ctx.set_line_width(stroke.width);
290 self.ctx.set_line_cap(stroke_cap_to_canvas(stroke.cap));
291 self.ctx.set_line_join(stroke_join_to_canvas(stroke.join));
292
293 let dash_values = dash_pattern_values(stroke);
294 let js_array = Array::new();
295 for &v in &dash_values {
296 js_array.push(&JsValue::from_f64(v));
297 }
298 let _ = self.ctx.set_line_dash(&js_array);
299
300 if let Some(ref pattern) = stroke.dash {
301 self.ctx.set_line_dash_offset(pattern.offset);
302 } else {
303 self.ctx.set_line_dash_offset(0.0);
304 }
305 }
306 }
307
308 impl Renderer for WasmRenderer {
309 fn size(&self) -> (u32, u32) {
310 (self.width, self.height)
311 }
312
313 fn fill_path(&mut self, path: &Path, paint: &Paint, transform: Affine) {
314 self.ctx.save();
315 self.apply_transform(transform);
316
317 let color = color_to_css(&paint.color);
318 self.ctx.set_fill_style_str(&color);
319
320 self.trace_path(path);
321 self.ctx.fill();
322
323 self.ctx.restore();
324 }
325
326 fn stroke_path(&mut self, path: &Path, paint: &Paint, stroke: &Stroke, transform: Affine) {
327 self.ctx.save();
328 self.apply_transform(transform);
329 self.configure_stroke(paint, stroke);
330
331 self.trace_path(path);
332 self.ctx.stroke();
333
334 self.ctx.restore();
335 }
336
337 fn draw_text(&mut self, text: &str, pos: Point, style: &TextStyle, transform: Affine) {
338 self.ctx.save();
339 self.apply_transform(transform);
340
341 let font = build_font_string(style);
342 self.ctx.set_font(&font);
343
344 let color = color_to_css(&style.color);
345 self.ctx.set_fill_style_str(&color);
346
347 self.ctx.set_text_align(halign_to_canvas(style.halign));
348 self.ctx.set_text_baseline(valign_to_canvas(style.valign));
349
350 let _ = self.ctx.fill_text(text, pos.x, pos.y);
351
352 self.ctx.restore();
353 }
354
355 fn draw_image(&mut self, img: &Image, dst: Rect, transform: Affine) {
356 self.ctx.save();
357 self.apply_transform(transform);
358
359 #[allow(irrefutable_let_patterns)]
364 if let Ok(clamped) = wasm_bindgen::Clamped(img.data.as_slice()).try_into() {
365 if let Ok(image_data) = web_sys::ImageData::new_with_u8_clamped_array_and_sh(
366 clamped, img.width, img.height,
367 ) {
368 if let Some(window) = web_sys::window() {
373 if let Some(document) = window.document() {
374 if let Ok(temp_canvas) = document.create_element("canvas") {
375 let temp_canvas: web_sys::HtmlCanvasElement =
376 temp_canvas.unchecked_into();
377 temp_canvas.set_width(img.width);
378 temp_canvas.set_height(img.height);
379 if let Ok(Some(temp_ctx)) = temp_canvas.get_context("2d") {
380 let temp_ctx: CanvasRenderingContext2d =
381 temp_ctx.unchecked_into();
382 let _ = temp_ctx.put_image_data(&image_data, 0.0, 0.0);
383 let _ =
384 self.ctx.draw_image_with_html_canvas_element_and_dw_and_dh(
385 &temp_canvas,
386 dst.x,
387 dst.y,
388 dst.width,
389 dst.height,
390 );
391 }
392 }
393 }
394 }
395 }
396 }
397
398 self.ctx.restore();
399 }
400
401 fn push_clip(&mut self, path: &Path, transform: Affine) {
402 self.ctx.save();
403 self.apply_transform(transform);
404 self.trace_path(path);
405 self.ctx.clip();
406 self.reset_transform();
409 }
410
411 fn pop_clip(&mut self) {
412 self.ctx.restore();
413 }
414
415 fn measure_text(&self, text: &str, style: &TextStyle) -> (f64, f64) {
416 let font = build_font_string(style);
417 self.ctx.set_font(&font);
418
419 if let Ok(metrics) = self.ctx.measure_text(text) {
420 let width = metrics.width();
421 let height = style.size;
425 (width, height)
426 } else {
427 (estimate_text_width(text, style), style.size)
429 }
430 }
431
432 fn finalize(self) -> Vec<u8> {
433 Vec::new()
437 }
438 }
439
440 #[wasm_bindgen]
444 pub fn render_demo(canvas: web_sys::HtmlCanvasElement, kind: &str) -> Result<(), JsValue> {
445 use plotkit_core::figure::Figure;
446 use wasm_bindgen::JsCast;
447
448 let width = canvas.width();
449 let height = canvas.height();
450 let ctx = canvas
451 .get_context("2d")?
452 .ok_or_else(|| JsValue::from_str("canvas has no 2d context"))?
453 .dyn_into::<web_sys::CanvasRenderingContext2d>()?;
454
455 let mut fig = Figure::with_size(width, height);
456 let ax = fig.add_subplot(1, 1, 1);
457 let to_js = |e: plotkit_core::error::PlotError| JsValue::from_str(&e.to_string());
458 let xs: Vec<f64> = (0..200).map(|i| i as f64 * 0.05).collect();
459 match kind {
460 "scatter" => {
461 let ys: Vec<f64> = xs.iter().map(|v| v.sin()).collect();
462 ax.scatter(xs.clone(), ys).map_err(to_js)?;
463 ax.set_title("scatter demo");
464 }
465 "bar" => {
466 let cats = vec!["A".to_string(), "B".to_string(), "C".to_string()];
467 ax.bar(cats, vec![3.0, 7.0, 5.0]).map_err(to_js)?;
468 ax.set_title("bar demo");
469 }
470 "hist" => {
471 let data: Vec<f64> = (0..500)
472 .map(|i| (i as f64 * 0.1).sin() + (i as f64 * 0.031).cos())
473 .collect();
474 ax.hist(data, 30).map_err(to_js)?;
475 ax.set_title("histogram demo");
476 }
477 _ => {
478 let ys: Vec<f64> = xs.iter().map(|v| v.sin()).collect();
479 ax.plot(xs.clone(), ys).map_err(to_js)?.label("sin(x)");
480 ax.set_title("line demo");
481 ax.legend();
482 }
483 }
484 ax.set_xlabel("x");
485 ax.set_ylabel("y");
486 ax.grid(true);
487
488 let renderer = WasmRenderer::new(ctx, width, height);
489 let _ = fig.render_to(renderer);
490 Ok(())
491 }
492}
493
494#[cfg(target_arch = "wasm32")]
495pub use wasm_impl::WasmRenderer;
496
497#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
508 fn color_opaque_to_css() {
509 let c = Color::rgb(255, 0, 0);
510 assert_eq!(color_to_css(&c), "rgba(255,0,0,1)");
511 }
512
513 #[test]
514 fn color_transparent_to_css() {
515 let c = Color::TRANSPARENT;
516 assert_eq!(color_to_css(&c), "rgba(0,0,0,0.0000)");
517 }
518
519 #[test]
520 fn color_semi_transparent_to_css() {
521 let c = Color::new(0, 128, 255, 128);
522 let css = color_to_css(&c);
523 assert_eq!(css, "rgba(0,128,255,0.5020)");
524 }
525
526 #[test]
527 fn color_white_to_css() {
528 let c = Color::WHITE;
529 assert_eq!(color_to_css(&c), "rgba(255,255,255,1)");
530 }
531
532 #[test]
533 fn color_black_to_css() {
534 let c = Color::BLACK;
535 assert_eq!(color_to_css(&c), "rgba(0,0,0,1)");
536 }
537
538 #[test]
539 fn color_with_alpha_one_to_css() {
540 let c = Color::new(100, 200, 50, 1);
542 let css = color_to_css(&c);
543 assert!(css.contains("0.0039"), "expected '0.0039' in {}", css);
544 }
545
546 #[test]
547 fn color_tableau_blue_to_css() {
548 let c = Color::TAB_BLUE; assert_eq!(color_to_css(&c), "rgba(78,121,167,1)");
550 }
551
552 #[test]
555 fn font_string_default() {
556 let style = TextStyle::new(14.0);
557 assert_eq!(build_font_string(&style), "14px sans-serif");
558 }
559
560 #[test]
561 fn font_string_bold() {
562 let mut style = TextStyle::new(20.0);
563 style.weight = FontWeight::Bold;
564 assert_eq!(build_font_string(&style), "bold 20px sans-serif");
565 }
566
567 #[test]
568 fn font_string_custom_family() {
569 let mut style = TextStyle::new(12.0);
570 style.family = Some("Helvetica Neue".to_string());
571 assert_eq!(build_font_string(&style), "12px Helvetica Neue");
572 }
573
574 #[test]
575 fn font_string_bold_custom_family() {
576 let mut style = TextStyle::new(16.0);
577 style.weight = FontWeight::Bold;
578 style.family = Some("Georgia".to_string());
579 assert_eq!(build_font_string(&style), "bold 16px Georgia");
580 }
581
582 #[test]
583 fn font_string_fractional_size() {
584 let style = TextStyle::new(10.5);
585 assert_eq!(build_font_string(&style), "10px sans-serif");
587 }
588
589 #[test]
592 fn halign_mapping() {
593 assert_eq!(halign_to_canvas(HAlign::Left), "left");
594 assert_eq!(halign_to_canvas(HAlign::Center), "center");
595 assert_eq!(halign_to_canvas(HAlign::Right), "right");
596 }
597
598 #[test]
599 fn valign_mapping() {
600 assert_eq!(valign_to_canvas(VAlign::Top), "top");
601 assert_eq!(valign_to_canvas(VAlign::Middle), "middle");
602 assert_eq!(valign_to_canvas(VAlign::Bottom), "bottom");
603 assert_eq!(valign_to_canvas(VAlign::Baseline), "alphabetic");
604 }
605
606 #[test]
609 fn stroke_cap_mapping() {
610 assert_eq!(stroke_cap_to_canvas(StrokeCap::Butt), "butt");
611 assert_eq!(stroke_cap_to_canvas(StrokeCap::Round), "round");
612 assert_eq!(stroke_cap_to_canvas(StrokeCap::Square), "square");
613 }
614
615 #[test]
616 fn stroke_join_mapping() {
617 assert_eq!(stroke_join_to_canvas(StrokeJoin::Miter), "miter");
618 assert_eq!(stroke_join_to_canvas(StrokeJoin::Round), "round");
619 assert_eq!(stroke_join_to_canvas(StrokeJoin::Bevel), "bevel");
620 }
621
622 #[test]
625 fn count_empty_path() {
626 let path = Path::new();
627 assert_eq!(count_path_elements(&path), (0, 0, 0, 0, 0));
628 }
629
630 #[test]
631 fn count_rect_path() {
632 let path = Path::rect(Rect::new(0.0, 0.0, 100.0, 50.0));
633 let (m, l, q, c, z) = count_path_elements(&path);
634 assert_eq!(m, 1, "rect should have 1 MoveTo");
635 assert_eq!(l, 3, "rect should have 3 LineTo");
636 assert_eq!(q, 0, "rect should have 0 QuadTo");
637 assert_eq!(c, 0, "rect should have 0 CurveTo");
638 assert_eq!(z, 1, "rect should have 1 ClosePath");
639 }
640
641 #[test]
642 fn count_circle_path() {
643 let path = Path::circle(Point::new(50.0, 50.0), 25.0);
644 let (m, l, q, c, z) = count_path_elements(&path);
645 assert_eq!(m, 1, "circle should have 1 MoveTo");
646 assert_eq!(l, 0, "circle should have 0 LineTo");
647 assert_eq!(q, 0, "circle should have 0 QuadTo");
648 assert_eq!(c, 4, "circle should have 4 CurveTo");
649 assert_eq!(z, 1, "circle should have 1 ClosePath");
650 }
651
652 #[test]
653 fn count_mixed_path() {
654 let mut path = Path::new();
655 path.move_to(0.0, 0.0)
656 .line_to(10.0, 0.0)
657 .quad_to(15.0, 5.0, 10.0, 10.0)
658 .curve_to(5.0, 15.0, -5.0, 15.0, -10.0, 10.0)
659 .close();
660 let (m, l, q, c, z) = count_path_elements(&path);
661 assert_eq!(m, 1);
662 assert_eq!(l, 1);
663 assert_eq!(q, 1);
664 assert_eq!(c, 1);
665 assert_eq!(z, 1);
666 }
667
668 #[test]
671 fn identity_affine_params() {
672 let params = affine_to_canvas_params(Affine::IDENTITY);
673 assert_eq!(params, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
674 }
675
676 #[test]
677 fn translate_affine_params() {
678 let t = Affine::translate((100.0, 200.0));
679 let [a, b, c, d, e, f] = affine_to_canvas_params(t);
680 assert_eq!(a, 1.0);
681 assert_eq!(b, 0.0);
682 assert_eq!(c, 0.0);
683 assert_eq!(d, 1.0);
684 assert_eq!(e, 100.0);
685 assert_eq!(f, 200.0);
686 }
687
688 #[test]
689 fn scale_affine_params() {
690 let s = Affine::scale_non_uniform(2.0, 3.0);
691 let [a, b, c, d, e, f] = affine_to_canvas_params(s);
692 assert_eq!(a, 2.0);
693 assert_eq!(d, 3.0);
694 assert_eq!(e, 0.0);
695 assert_eq!(f, 0.0);
696 assert_eq!(b, 0.0);
697 assert_eq!(c, 0.0);
698 }
699
700 #[test]
703 fn estimate_text_width_normal() {
704 let style = TextStyle::new(10.0);
705 let width = estimate_text_width("hello", &style);
706 assert!((width - 30.0).abs() < 1e-10);
708 }
709
710 #[test]
711 fn estimate_text_width_bold() {
712 let mut style = TextStyle::new(10.0);
713 style.weight = FontWeight::Bold;
714 let width = estimate_text_width("hello", &style);
715 assert!((width - 32.5).abs() < 1e-10);
717 }
718
719 #[test]
720 fn estimate_text_width_empty() {
721 let style = TextStyle::new(16.0);
722 let width = estimate_text_width("", &style);
723 assert_eq!(width, 0.0);
724 }
725
726 #[test]
729 fn dash_pattern_solid_stroke() {
730 let stroke = Stroke::new(2.0);
731 let dashes = dash_pattern_values(&stroke);
732 assert!(dashes.is_empty());
733 }
734
735 #[test]
736 fn dash_pattern_dashed_stroke() {
737 let stroke = Stroke::new(1.5).with_dash(DashPattern {
738 dashes: vec![5.0, 3.0, 1.0],
739 offset: 2.0,
740 });
741 let dashes = dash_pattern_values(&stroke);
742 assert_eq!(dashes, vec![5.0, 3.0, 1.0]);
743 }
744
745 #[test]
748 fn font_string_large_size() {
749 let style = TextStyle::new(72.0);
750 assert_eq!(build_font_string(&style), "72px sans-serif");
751 }
752
753 #[test]
754 fn font_string_small_size() {
755 let style = TextStyle::new(6.0);
756 assert_eq!(build_font_string(&style), "6px sans-serif");
757 }
758}