1#![allow(clippy::identity_op)]
6#![cfg_attr(docsrs, feature(doc_auto_cfg))]
7#![deny(clippy::trivially_copy_pass_by_ref)]
8
9mod 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 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 fn default() -> CanvasState {
70 CanvasState {
71 line_cap: LineCap::Butt,
73 line_dash: StrokeDash::default(),
75 line_dash_offset: 0.,
77 line_join: LineJoin::Miter { limit: 10. },
80 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 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
127impl<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 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 };
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 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 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 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 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 let rgba = stop.color.as_rgba_u32();
522 let _ = dst.add_color_stop(stop.pos, &format_color(rgba));
523 }
524}
525
526impl WebRenderContext<'_> {
527 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 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 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}