piet_web/
lib.rs

1// Copyright 2019 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4// allows e.g. raw_data[dst_off + x * 4 + 2] = buf[src_off + x * 4 + 0];
5#![allow(clippy::identity_op)]
6#![cfg_attr(docsrs, feature(doc_auto_cfg))]
7#![deny(clippy::trivially_copy_pass_by_ref)]
8
9//! The Web Canvas backend for the Piet 2D graphics abstraction.
10
11mod text;
12
13use std::borrow::Cow;
14use std::fmt;
15use std::marker::PhantomData;
16use std::ops::Deref;
17
18use js_sys::{Float64Array, Reflect};
19use wasm_bindgen::{Clamped, JsCast, JsValue};
20use web_sys::{
21    CanvasGradient, CanvasRenderingContext2d, CanvasWindingRule, DomMatrix, HtmlCanvasElement,
22    ImageData, Window,
23};
24
25use piet::kurbo::{Affine, PathEl, Point, Rect, Shape, Size};
26
27use piet::util::unpremul;
28use piet::{
29    Color, Error, FixedGradient, GradientStop, Image, ImageFormat, InterpolationMode, IntoBrush,
30    LineCap, LineJoin, RenderContext, StrokeDash, StrokeStyle,
31};
32
33pub use text::{WebFont, WebTextLayout, WebTextLayoutBuilder};
34
35pub struct WebRenderContext<'a> {
36    ctx: CanvasRenderingContext2d,
37    /// Used for creating image bitmaps and possibly other resources.
38    window: Window,
39    text: WebText,
40    err: Result<(), Error>,
41    canvas_states: Vec<CanvasState>,
42    _phantom: PhantomData<&'a ()>,
43}
44
45impl WebRenderContext<'_> {
46    pub fn new(ctx: CanvasRenderingContext2d, window: Window) -> WebRenderContext<'static> {
47        WebRenderContext {
48            ctx: ctx.clone(),
49            window,
50            text: WebText::new(ctx),
51            err: Ok(()),
52            canvas_states: vec![CanvasState::default()],
53            _phantom: PhantomData,
54        }
55    }
56}
57
58#[derive(Clone)]
59struct CanvasState {
60    line_cap: LineCap,
61    line_dash: StrokeDash,
62    line_dash_offset: f64,
63    line_join: LineJoin,
64    line_width: f64,
65}
66
67impl Default for CanvasState {
68    /// Returns the default canvas state according to the Canvas API.
69    fn default() -> CanvasState {
70        CanvasState {
71            // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap#value
72            line_cap: LineCap::Butt,
73            // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash
74            line_dash: StrokeDash::default(),
75            // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset#value
76            line_dash_offset: 0.,
77            // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin#value
78            // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/miterLimit#value
79            line_join: LineJoin::Miter { limit: 10. },
80            // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineWidth#value
81            line_width: 1.,
82        }
83    }
84}
85
86#[derive(Clone)]
87pub struct WebText {
88    ctx: CanvasRenderingContext2d,
89}
90
91impl WebText {
92    pub fn new(ctx: CanvasRenderingContext2d) -> WebText {
93        WebText { ctx }
94    }
95}
96
97#[derive(Clone)]
98pub enum Brush {
99    Solid(u32),
100    Gradient(CanvasGradient),
101}
102
103#[derive(Clone)]
104pub struct WebImage {
105    /// We use a canvas element for now, but could be ImageData or ImageBitmap,
106    /// so consider an enum.
107    inner: HtmlCanvasElement,
108    width: u32,
109    height: u32,
110}
111
112#[derive(Debug)]
113struct WrappedJs(JsValue);
114
115trait WrapError<T> {
116    fn wrap(self) -> Result<T, Error>;
117}
118
119impl std::error::Error for WrappedJs {}
120
121impl fmt::Display for WrappedJs {
122    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123        write!(f, "Canvas error: {:?}", self.0)
124    }
125}
126
127// Discussion question: a blanket impl here should be pretty doable.
128
129impl<T> WrapError<T> for Result<T, JsValue> {
130    fn wrap(self) -> Result<T, Error> {
131        self.map_err(|e| {
132            let e: Box<dyn std::error::Error> = Box::new(WrappedJs(e));
133            e.into()
134        })
135    }
136}
137
138fn convert_line_cap(line_cap: LineCap) -> &'static str {
139    match line_cap {
140        LineCap::Butt => "butt",
141        LineCap::Round => "round",
142        LineCap::Square => "square",
143    }
144}
145
146fn convert_line_join(line_join: LineJoin) -> &'static str {
147    match line_join {
148        LineJoin::Miter { .. } => "miter",
149        LineJoin::Round => "round",
150        LineJoin::Bevel => "bevel",
151    }
152}
153
154fn convert_dash_pattern(pattern: &[f64]) -> Float64Array {
155    let len = pattern.len() as u32;
156    let array = Float64Array::new_with_length(len);
157    for (i, elem) in pattern.iter().enumerate() {
158        Reflect::set(
159            array.as_ref(),
160            &JsValue::from(i as u32),
161            &JsValue::from(*elem),
162        )
163        .unwrap();
164    }
165    array
166}
167
168impl RenderContext for WebRenderContext<'_> {
169    /// wasm-bindgen doesn't have a native Point type, so use kurbo's.
170    type Brush = Brush;
171
172    type Text = WebText;
173    type TextLayout = WebTextLayout;
174
175    type Image = WebImage;
176
177    fn status(&mut self) -> Result<(), Error> {
178        std::mem::replace(&mut self.err, Ok(()))
179    }
180
181    fn clear(&mut self, region: impl Into<Option<Rect>>, color: Color) {
182        let (width, height) = match self.ctx.canvas() {
183            Some(canvas) => (canvas.offset_width(), canvas.offset_height()),
184            None => return,
185            /* Canvas might be null if the dom node is not in
186             * the document; do nothing. */
187        };
188        let rect = region
189            .into()
190            .unwrap_or_else(|| Rect::new(0.0, 0.0, width as f64, height as f64));
191        let brush = self.solid_brush(color);
192        self.fill(rect, &brush);
193    }
194
195    fn solid_brush(&mut self, color: Color) -> Brush {
196        Brush::Solid(color.as_rgba_u32())
197    }
198
199    fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<Brush, Error> {
200        match gradient.into() {
201            FixedGradient::Linear(linear) => {
202                let (x0, y0) = (linear.start.x, linear.start.y);
203                let (x1, y1) = (linear.end.x, linear.end.y);
204                let mut lg = self.ctx.create_linear_gradient(x0, y0, x1, y1);
205                set_gradient_stops(&mut lg, &linear.stops);
206                Ok(Brush::Gradient(lg))
207            }
208            FixedGradient::Radial(radial) => {
209                let (xc, yc) = (radial.center.x, radial.center.y);
210                let (xo, yo) = (radial.origin_offset.x, radial.origin_offset.y);
211                let r = radial.radius;
212                let mut rg = self
213                    .ctx
214                    .create_radial_gradient(xc + xo, yc + yo, 0.0, xc, yc, r)
215                    .wrap()?;
216                set_gradient_stops(&mut rg, &radial.stops);
217                Ok(Brush::Gradient(rg))
218            }
219        }
220    }
221
222    fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
223        let brush = brush.make_brush(self, || shape.bounding_box());
224        self.set_path(shape);
225        self.set_brush(&brush, true);
226        self.ctx
227            .fill_with_canvas_winding_rule(CanvasWindingRule::Nonzero);
228    }
229
230    fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
231        let brush = brush.make_brush(self, || shape.bounding_box());
232        self.set_path(shape);
233        self.set_brush(&brush, true);
234        self.ctx
235            .fill_with_canvas_winding_rule(CanvasWindingRule::Evenodd);
236    }
237
238    fn clip(&mut self, shape: impl Shape) {
239        self.set_path(shape);
240        self.ctx
241            .clip_with_canvas_winding_rule(CanvasWindingRule::Nonzero);
242    }
243
244    fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
245        let brush = brush.make_brush(self, || shape.bounding_box());
246        self.set_path(shape);
247        self.set_stroke(width, None);
248        self.set_brush(brush.deref(), false);
249        self.ctx.stroke();
250    }
251
252    fn stroke_styled(
253        &mut self,
254        shape: impl Shape,
255        brush: &impl IntoBrush<Self>,
256        width: f64,
257        style: &StrokeStyle,
258    ) {
259        let brush = brush.make_brush(self, || shape.bounding_box());
260        self.set_path(shape);
261        self.set_stroke(width, Some(style));
262        self.set_brush(brush.deref(), false);
263        self.ctx.stroke();
264    }
265
266    fn text(&mut self) -> &mut Self::Text {
267        &mut self.text
268    }
269
270    fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into<Point>) {
271        // TODO: bounding box for text
272        self.ctx.save();
273        self.ctx.set_font(&layout.font.get_font_string());
274        let color = layout.color();
275        let brush = color.make_brush(self, || layout.size().to_rect());
276        self.set_brush(&brush, true);
277        let pos = pos.into();
278        for lm in &layout.line_metrics {
279            let line_text = &layout.text[lm.range()];
280            let line_y = lm.y_offset + lm.baseline + pos.y;
281            let draw_line = self.ctx.fill_text(line_text, pos.x, line_y).wrap();
282
283            if let Err(e) = draw_line {
284                self.err = Err(e);
285            }
286        }
287        self.ctx.restore();
288    }
289
290    fn save(&mut self) -> Result<(), Error> {
291        self.ctx.save();
292        self.canvas_states
293            .push(self.canvas_states.last().unwrap().clone());
294        Ok(())
295    }
296
297    fn restore(&mut self) -> Result<(), Error> {
298        // restore state only if there is a state to restore
299        if self.canvas_states.len() > 1 {
300            self.canvas_states.pop();
301            self.ctx.restore();
302        }
303        Ok(())
304    }
305
306    fn finish(&mut self) -> Result<(), Error> {
307        self.status()
308    }
309
310    fn transform(&mut self, transform: Affine) {
311        let a = transform.as_coeffs();
312        let _ = self.ctx.transform(a[0], a[1], a[2], a[3], a[4], a[5]);
313    }
314
315    fn current_transform(&self) -> Affine {
316        matrix_to_affine(self.ctx.get_transform().unwrap())
317    }
318
319    fn make_image_with_stride(
320        &mut self,
321        width: usize,
322        height: usize,
323        stride: usize,
324        buf: &[u8],
325        format: ImageFormat,
326    ) -> Result<Self::Image, Error> {
327        if buf.len()
328            < piet::util::expected_image_buffer_size(
329                format.bytes_per_pixel() * width,
330                height,
331                stride,
332            )
333        {
334            return Err(Error::InvalidInput);
335        }
336        let document = self.window.document().unwrap();
337        let element = document.create_element("canvas").unwrap();
338        let canvas = element.dyn_into::<HtmlCanvasElement>().unwrap();
339        canvas.set_width(width as u32);
340        canvas.set_height(height as u32);
341        let mut new_buf: Vec<u8>;
342        let buf = match format {
343            ImageFormat::RgbaSeparate => {
344                if stride == width * format.bytes_per_pixel() {
345                    buf
346                } else {
347                    new_buf = piet::util::image_buffer_to_tightly_packed(
348                        buf, width, height, stride, format,
349                    )?;
350                    new_buf.as_slice()
351                }
352            }
353            ImageFormat::RgbaPremul => {
354                new_buf = vec![0; width * height * 4];
355                for y in 0..height {
356                    for x in 0..width {
357                        let src_offset = y * stride + x * 4;
358                        let dst_offset = (y * width + x) * 4;
359                        let a = buf[src_offset + 3];
360                        new_buf[dst_offset + 0] = unpremul(buf[src_offset + 0], a);
361                        new_buf[dst_offset + 1] = unpremul(buf[src_offset + 1], a);
362                        new_buf[dst_offset + 2] = unpremul(buf[src_offset + 2], a);
363                    }
364                }
365                new_buf.as_slice()
366            }
367            ImageFormat::Rgb => {
368                new_buf = vec![0; width * height * 4];
369                for y in 0..height {
370                    for x in 0..width {
371                        let src_offset = y * stride + x * 3;
372                        let dst_offset = (y * width + x) * 4;
373                        new_buf[dst_offset + 0] = buf[src_offset + 0];
374                        new_buf[dst_offset + 1] = buf[src_offset + 1];
375                        new_buf[dst_offset + 2] = buf[src_offset + 2];
376                        new_buf[dst_offset + 3] = 255;
377                    }
378                }
379                new_buf.as_slice()
380            }
381            ImageFormat::Grayscale => {
382                new_buf = vec![0; width * height * 4];
383                for y in 0..height {
384                    for x in 0..width {
385                        let src_offset = y * stride + x;
386                        let dst_offset = (y * width + x) * 4;
387                        new_buf[dst_offset + 0] = buf[src_offset];
388                        new_buf[dst_offset + 1] = buf[src_offset];
389                        new_buf[dst_offset + 2] = buf[src_offset];
390                        new_buf[dst_offset + 3] = 255;
391                    }
392                }
393                new_buf.as_slice()
394            }
395            _ => &[],
396        };
397
398        let image_data = ImageData::new_with_u8_clamped_array(Clamped(buf), width as u32).wrap()?;
399        let context = canvas
400            .get_context("2d")
401            .unwrap()
402            .unwrap()
403            .dyn_into::<web_sys::CanvasRenderingContext2d>()
404            .unwrap();
405        context.put_image_data(&image_data, 0.0, 0.0).wrap()?;
406        Ok(WebImage {
407            inner: canvas,
408            width: width as u32,
409            height: height as u32,
410        })
411    }
412
413    #[inline]
414    fn draw_image(
415        &mut self,
416        image: &Self::Image,
417        dst_rect: impl Into<Rect>,
418        interp: InterpolationMode,
419    ) {
420        draw_image(self, image, None, dst_rect.into(), interp);
421    }
422
423    #[inline]
424    fn draw_image_area(
425        &mut self,
426        image: &Self::Image,
427        src_rect: impl Into<Rect>,
428        dst_rect: impl Into<Rect>,
429        interp: InterpolationMode,
430    ) {
431        draw_image(self, image, Some(src_rect.into()), dst_rect.into(), interp);
432    }
433
434    fn capture_image_area(&mut self, _rect: impl Into<Rect>) -> Result<Self::Image, Error> {
435        Err(Error::Unimplemented)
436    }
437
438    fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush<Self>) {
439        let brush = brush.make_brush(self, || rect);
440        self.ctx.set_shadow_blur(blur_radius);
441        let color = match *brush {
442            Brush::Solid(rgba) => format_color(rgba),
443            // Gradients not yet implemented.
444            Brush::Gradient(_) => "#f0f".into(),
445        };
446        self.ctx.set_shadow_color(&color);
447        self.ctx
448            .fill_rect(rect.x0, rect.y0, rect.width(), rect.height());
449        self.ctx.set_shadow_color("none");
450    }
451}
452
453fn draw_image(
454    ctx: &mut WebRenderContext,
455    image: &<WebRenderContext as RenderContext>::Image,
456    src_rect: Option<Rect>,
457    dst_rect: Rect,
458    _interp: InterpolationMode,
459) {
460    let result = ctx.with_save(|rc| {
461        // TODO: Implement InterpolationMode::NearestNeighbor in software
462        //       See for inspiration http://phrogz.net/tmp/canvas_image_zoom.html
463        let src_rect = match src_rect {
464            Some(src_rect) => src_rect,
465            None => Rect::new(0.0, 0.0, image.width as f64, image.height as f64),
466        };
467        rc.ctx
468            .draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
469                &image.inner,
470                src_rect.x0,
471                src_rect.y0,
472                src_rect.width(),
473                src_rect.height(),
474                dst_rect.x0,
475                dst_rect.y0,
476                dst_rect.width(),
477                dst_rect.height(),
478            )
479            .wrap()
480    });
481    if let Err(e) = result {
482        ctx.err = Err(e);
483    }
484}
485
486impl IntoBrush<WebRenderContext<'_>> for Brush {
487    fn make_brush<'b>(
488        &'b self,
489        _piet: &mut WebRenderContext,
490        _bbox: impl FnOnce() -> Rect,
491    ) -> std::borrow::Cow<'b, Brush> {
492        Cow::Borrowed(self)
493    }
494}
495
496impl Image for WebImage {
497    fn size(&self) -> Size {
498        Size::new(self.width.into(), self.height.into())
499    }
500}
501
502fn format_color(rgba: u32) -> String {
503    let rgb = rgba >> 8;
504    let a = rgba & 0xff;
505    if a == 0xff {
506        format!("#{:06x}", rgba >> 8)
507    } else {
508        format!(
509            "rgba({},{},{},{:.3})",
510            (rgb >> 16) & 0xff,
511            (rgb >> 8) & 0xff,
512            rgb & 0xff,
513            byte_to_frac(a)
514        )
515    }
516}
517
518fn set_gradient_stops(dst: &mut CanvasGradient, src: &[GradientStop]) {
519    for stop in src {
520        // TODO: maybe get error?
521        let rgba = stop.color.as_rgba_u32();
522        let _ = dst.add_color_stop(stop.pos, &format_color(rgba));
523    }
524}
525
526impl WebRenderContext<'_> {
527    /// Set the source pattern to the brush.
528    ///
529    /// Web canvas is super stateful, and we're trying to have more retained stuff.
530    /// This is part of the impedance matching.
531    fn set_brush(&mut self, brush: &Brush, is_fill: bool) {
532        let value = self.brush_value(brush);
533        if is_fill {
534            #[allow(deprecated)]
535            self.ctx.set_fill_style(&value);
536        } else {
537            #[allow(deprecated)]
538            self.ctx.set_stroke_style(&value);
539        }
540    }
541
542    fn brush_value(&self, brush: &Brush) -> JsValue {
543        match *brush {
544            Brush::Solid(rgba) => JsValue::from_str(&format_color(rgba)),
545            Brush::Gradient(ref gradient) => JsValue::from(gradient),
546        }
547    }
548
549    /// Set the stroke parameters.
550    fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) {
551        let default_style = StrokeStyle::default();
552        let style = style.unwrap_or(&default_style);
553        let canvas_state = self.canvas_states.last_mut().unwrap();
554
555        if width != canvas_state.line_width {
556            self.ctx.set_line_width(width);
557            canvas_state.line_width = width;
558        }
559
560        if style.line_join != canvas_state.line_join {
561            self.ctx.set_line_join(convert_line_join(style.line_join));
562            if let Some(limit) = style.miter_limit() {
563                self.ctx.set_miter_limit(limit);
564            }
565            canvas_state.line_join = style.line_join;
566        }
567
568        if style.line_cap != canvas_state.line_cap {
569            self.ctx.set_line_cap(convert_line_cap(style.line_cap));
570            canvas_state.line_cap = style.line_cap;
571        }
572
573        if style.dash_pattern != canvas_state.line_dash {
574            let dash_segs = convert_dash_pattern(&style.dash_pattern);
575            self.ctx.set_line_dash(dash_segs.as_ref()).unwrap();
576            canvas_state.line_dash = style.dash_pattern.clone();
577        }
578
579        if style.dash_offset != canvas_state.line_dash_offset {
580            self.ctx.set_line_dash_offset(style.dash_offset);
581            canvas_state.line_dash_offset = style.dash_offset;
582        }
583    }
584
585    fn set_path(&mut self, shape: impl Shape) {
586        // This shouldn't be necessary, we always leave the context in no-path
587        // state. But just in case, and it should be harmless.
588        self.ctx.begin_path();
589        for el in shape.path_elements(1e-3) {
590            match el {
591                PathEl::MoveTo(p) => self.ctx.move_to(p.x, p.y),
592                PathEl::LineTo(p) => self.ctx.line_to(p.x, p.y),
593                PathEl::QuadTo(p1, p2) => self.ctx.quadratic_curve_to(p1.x, p1.y, p2.x, p2.y),
594                PathEl::CurveTo(p1, p2, p3) => {
595                    self.ctx.bezier_curve_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
596                }
597                PathEl::ClosePath => self.ctx.close_path(),
598            }
599        }
600    }
601}
602
603fn byte_to_frac(byte: u32) -> f64 {
604    ((byte & 255) as f64) * (1.0 / 255.0)
605}
606
607fn matrix_to_affine(matrix: DomMatrix) -> Affine {
608    Affine::new([
609        matrix.a(),
610        matrix.b(),
611        matrix.c(),
612        matrix.d(),
613        matrix.e(),
614        matrix.f(),
615    ])
616}