1//! Submission through the browser's 2D canvas, where it has neither WebGPU nor WebGL2. Each
2//! quad of the frame becomes the canvas's own calls: shapes as paths, glyphs, icons and paths
3//! from an atlas canvas, pictures and groups by `drawImage`. The canvas blends sRGB-encoded,
4//! so a translucent colour's opacity is corrected toward what linear light shows.
5//!
6//! A group leaning back in perspective, which the canvas's affine transforms can't draw,
7//! paints into a canvas of its own laid over the target and leaned by a CSS 3D transform
8//! with the same projection; what paints after it goes into a canvas above that.
9use super::*;
10use std::{
11 cell::{Cell, RefCell},
12 f64::consts::{FRAC_PI_2, PI},
13};
14use wasm_bindgen::{Clamped, JsCast};
15use web_sys::{
16 CanvasRenderingContext2d as Context, CanvasWindingRule, HtmlCanvasElement as Canvas, ImageData,
17};
18
19/// The widest canvas every browser keeps whole.
20const MAX_SIDE: u32 = 8192;
21/// Most colours glyphs are kept coloured in at once.
22const MAX_TINTS: usize = 16;
23/// Most shadows kept drawn at once.
24const MAX_SHADOWS: usize = 8;
25
26/// A canvas and its 2D context, which a frame is drawn into.
27#[derive(Clone)]
28pub struct Target {
29 canvas: Canvas,
30 context: Context,
31}
32
33impl Target {
34 pub fn new(canvas: Canvas) -> Result<Self, String> {
35 let context = canvas
36 .get_context("2d")
37 .map_err(|error| format!("{error:?}"))?
38 .ok_or("The canvas has no 2D context")?
39 .dyn_into()
40 .map_err(|_| "The canvas's context is not 2D")?;
41 Ok(Self { canvas, context })
42 }
43
44 /// A canvas of its own, in no page, `size` pixels large.
45 fn detached(size: [u32; 2]) -> Result<Self, String> {
46 let canvas: Canvas = web_sys::window()
47 .and_then(|window| window.document())
48 .ok_or("No document")?
49 .create_element("canvas")
50 .map_err(|error| format!("{error:?}"))?
51 .unchecked_into();
52 let target = Self::new(canvas)?;
53 target.resize(size);
54 Ok(target)
55 }
56
57 pub fn size(&self) -> [u32; 2] {
58 [self.canvas.width(), self.canvas.height()]
59 }
60
61 /// Resizes the canvas, which clears it, if it is another size.
62 fn resize(&self, size: [u32; 2]) {
63 if self.size() != size {
64 self.canvas.set_width(size[0]);
65 self.canvas.set_height(size[1]);
66 }
67 }
68}
69
70/// A picture as straight-alpha pixels in a canvas of its own, unless the browser had no
71/// canvas to spare.
72pub(super) struct Image(Option<Canvas>);
73
74impl AsRef<Image> for Image {
75 fn as_ref(&self) -> &Image {
76 self
77 }
78}
79
80/// Glyphs of one colour, coloured once in a sheet of their own that frames copy them from:
81/// the browser copies a whole canvas drawn into after it was last drawn from, so colouring each
82/// run of glyphs afresh, or the whole atlas, would copy a large canvas a frame.
83struct Tint {
84 color: [u32; 4],
85 sheet: Target,
86 /// Where each atlas rectangle `[left, top, right, bottom]` lies coloured in the sheet.
87 glyphs: HashMap<[u32; 4], [u32; 2]>,
88 /// Where the sheet's next glyph goes, and the height of the row it goes in.
89 pen: [u32; 2],
90 row: u32,
91 /// The last frame drawing with it.
92 used: u64,
93}
94
95impl Tint {
96 /// Colours atlas rectangle `glyph` into the sheet, growing it or else emptying it when
97 /// full; where even an empty sheet can't hold it, the glyph takes its colour as it draws.
98 fn add(&mut self, atlas: &Canvas, glyph: [u32; 4]) {
99 let color = css(self.color.map(f32::from_bits));
100 let [width, height] = [glyph[2] - glyph[0], glyph[3] - glyph[1]];
101 loop {
102 let side = self.sheet.size()[0];
103 if self.pen[0] + width > side {
104 self.pen = [0, self.pen[1] + self.row + 1];
105 self.row = 0;
106 }
107 if self.pen[1] + height <= side {
108 break;
109 }
110 if side >= MAX_SIDE / 2 {
111 if self.glyphs.is_empty() {
112 return;
113 }
114 self.glyphs.clear();
115 self.sheet
116 .context
117 .clear_rect(0.0, 0.0, side.into(), side.into());
118 (self.pen, self.row) = ([0; 2], 0);
119 continue;
120 }
121 let Ok(grown) = Target::detached([2 * side; 2]) else {
122 return;
123 };
124 let _ = grown
125 .context
126 .draw_image_with_html_canvas_element(&self.sheet.canvas, 0.0, 0.0);
127 self.sheet = grown;
128 }
129 let [x, y] = self.pen.map(f64::from);
130 let [w, h] = [width, height].map(f64::from);
131 let context = &self.sheet.context;
132 let [u, v] = [glyph[0], glyph[1]].map(f64::from);
133 let _ = context
134 .draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
135 atlas, u, v, w, h, x, y, w, h,
136 );
137 context.set_fill_style_str(&color);
138 color_in(context, [x, y, w, h]);
139 self.glyphs.insert(glyph, self.pen);
140 self.pen[0] += width + 1;
141 self.row = self.row.max(height);
142 }
143}
144
145/// A shadow drawn once and copied while it stays the same, as a popup's does while it opens:
146/// the browser may blur in software, slowly.
147struct Shadow {
148 /// The rectangle's half size, corner radius and blur, then the colour, as bits.
149 key: Vec<u32>,
150 sheet: Target,
151 /// The last frame drawing it.
152 used: u64,
153}
154
155pub(super) struct Gpu {
156 atlas: Target,
157 /// Whether the atlas was emptied since the last frame, as writing its white texel at the
158 /// origin shows (`Renderer::clear_glyph_cache`): the tints' glyphs are gone.
159 emptied: Cell<bool>,
160 tints: Vec<Tint>,
161 shadows: RefCell<Vec<Shadow>>,
162 /// Frames submitted.
163 frames: u64,
164 /// Where glyphs of a colour no tint holds take it, together, before reaching the target.
165 scratch: Target,
166 /// Offscreen pictures for groups, the target's size.
167 groups: Vec<Target>,
168 /// Canvases over the target: in turn a group leaning back, then what paints above it.
169 overlays: Vec<Target>,
170}
171
172impl Drop for Gpu {
173 fn drop(&mut self) {
174 for overlay in &self.overlays {
175 overlay.canvas.remove();
176 }
177 }
178}
179
180impl Renderer {
181 /// Draws with the browser's 2D canvas, into `Target`s.
182 pub fn canvas2d() -> Result<Self, String> {
183 Ok(Self::with_gpu(Gpu {
184 atlas: Target::detached([ATLAS_SIZE; 2])?,
185 emptied: Cell::new(false),
186 tints: Vec::new(),
187 shadows: RefCell::new(Vec::new()),
188 frames: 0,
189 scratch: Target::detached([1; 2])?,
190 groups: Vec::new(),
191 overlays: Vec::new(),
192 }))
193 }
194}
195
196impl Gpu {
197 /// Blends sRGB-encoded, as the canvas does.
198 pub(super) fn blends_linear(&self) -> bool {
199 false
200 }
201
202 pub(super) fn flipped(&self) -> bool {
203 false
204 }
205
206 pub(super) fn max_texture_dimension(&self) -> u32 {
207 MAX_SIDE
208 }
209
210 pub(super) fn atlas_side(&self) -> u32 {
211 self.atlas.canvas.width()
212 }
213
214 pub(super) fn new_atlas(&mut self, side: u32) {
215 self.atlas.resize([side; 2]);
216 self.tints.clear();
217 }
218
219 pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) {
220 put(&self.atlas.context, origin, size, rgba);
221 if origin == [0; 2] {
222 self.emptied.set(true);
223 }
224 }
225
226 /// Colours the glyphs `frame` draws in a colour into that colour's tint, keeping tints
227 /// for as many colours as `MAX_TINTS` allows.
228 fn tint(&mut self, frame: &Frame<'_>) {
229 self.frames += 1;
230 if self.emptied.take() {
231 self.tints.clear();
232 }
233 let side = self.atlas_side() as f32;
234 let batches = (frame.batches.iter())
235 .chain(frame.groups.iter().flat_map(|group| &group.batches))
236 .filter(|batch| batch.blend == Blend::Over);
237 for batch in batches {
238 let range = batch.vertices.start as usize..batch.vertices.end as usize;
239 for quad in frame.vertices[range].chunks_exact(6) {
240 let Some(color) = tinted(quad) else {
241 continue;
242 };
243 let key = color.map(f32::to_bits);
244 let at = match self.tints.iter().position(|tint| tint.color == key) {
245 Some(at) => at,
246 None => {
247 let sheet = if self.tints.len() < MAX_TINTS {
248 match Target::detached([256; 2]) {
249 Ok(sheet) => sheet,
250 Err(error) => return report(&error),
251 }
252 } else if let Some(at) = (0..self.tints.len())
253 .filter(|at| self.tints[*at].used != self.frames)
254 .min_by_key(|at| self.tints[*at].used)
255 {
256 let sheet = self.tints.swap_remove(at).sheet;
257 let [width, height] = sheet.size().map(f64::from);
258 sheet.context.clear_rect(0.0, 0.0, width, height);
259 sheet
260 } else {
261 continue;
262 };
263 self.tints.push(Tint {
264 color: key,
265 sheet,
266 glyphs: HashMap::new(),
267 pen: [0; 2],
268 row: 0,
269 used: 0,
270 });
271 self.tints.len() - 1
272 }
273 };
274 let tint = &mut self.tints[at];
275 tint.used = self.frames;
276 let glyph = source(quad, side).map(|value| value as u32);
277 if !tint.glyphs.contains_key(&glyph) {
278 tint.add(&self.atlas.canvas, glyph);
279 }
280 }
281 }
282 }
283
284 /// The canvas holding shadow `key`, `size` pixels with the shadow in its middle, which
285 /// `cast` draws where it isn't kept; none where `MAX_SHADOWS` are kept for this frame.
286 fn shadow(
287 &self,
288 key: Vec<u32>,
289 size: [u32; 2],
290 cast: impl FnOnce(&Context, [f64; 2]),
291 ) -> Option<Canvas> {
292 let mut shadows = self.shadows.borrow_mut();
293 if let Some(shadow) = shadows.iter_mut().find(|shadow| shadow.key == key) {
294 shadow.used = self.frames;
295 return Some(shadow.sheet.canvas.clone());
296 }
297 let sheet = if shadows.len() < MAX_SHADOWS {
298 Target::detached(size).ok()?
299 } else {
300 let at = (0..shadows.len())
301 .filter(|at| shadows[*at].used != self.frames)
302 .min_by_key(|at| shadows[*at].used)?;
303 let sheet = shadows.swap_remove(at).sheet;
304 sheet.resize(size);
305 let [width, height] = size.map(f64::from);
306 sheet.context.clear_rect(0.0, 0.0, width, height);
307 sheet
308 };
309 cast(&sheet.context, size.map(|side| f64::from(side) / 2.0));
310 let canvas = sheet.canvas.clone();
311 shadows.push(Shadow {
312 key,
313 sheet,
314 used: self.frames,
315 });
316 Some(canvas)
317 }
318
319 pub(super) fn upload_image(&self, image: &RasterImage) -> Image {
320 // The canvas takes straight alpha; the picture's colour was premultiplied in linear
321 // light.
322 let mut pixels = image.pixels().to_vec();
323 for pixel in pixels.chunks_exact_mut(4) {
324 let alpha = f32::from(pixel[3]) / 255.0;
325 if alpha > 0.0 && alpha < 1.0 {
326 for byte in &mut pixel[..3] {
327 *byte = srgb_byte(crate::linear(f32::from(*byte) / 255.0) / alpha);
328 }
329 }
330 }
331 match Target::detached(image.size) {
332 Ok(picture) => {
333 put(&picture.context, [0; 2], image.size, &pixels);
334 Image(Some(picture.canvas))
335 }
336 Err(error) => {
337 report(&error);
338 Image(None)
339 }
340 }
341 }
342
343 /// The target's sRGB RGBA rows, top first.
344 pub(super) fn read_pixels(&self, target: &Target) -> Result<Vec<u8>, String> {
345 let [width, height] = target.size();
346 let data = target
347 .context
348 .get_image_data(0.0, 0.0, width.into(), height.into())
349 .map_err(|error| format!("{error:?}"))?;
350 Ok(data.data().0)
351 }
352
353 /// Clears `target`, `size` device pixels, to linear `clear` and draws the frame.
354 pub(super) fn submit(
355 &mut self,
356 frame: &Frame<'_>,
357 target: &Target,
358 size: [u32; 2],
359 clear: [f32; 4],
360 ) {
361 target.resize(size);
362 self.tint(frame);
363 // Laid over the target only where it is in the page.
364 let onscreen = target.canvas.parent_node().is_some();
365 let leaning = |batch: &&Batch| matches!(batch.blend, Blend::Group(index) if frame.groups[index].motion.tilt != 0.0);
366 let overlays = if onscreen {
367 2 * frame.batches.iter().filter(leaning).count()
368 } else {
369 0
370 };
371 while self.groups.len() < frame.groups.len() || self.overlays.len() < overlays {
372 let made = if self.groups.len() < frame.groups.len() {
373 Target::detached(size).map(|picture| self.groups.push(picture))
374 } else {
375 overlay(self.overlays.last().unwrap_or(target)).map(|o| self.overlays.push(o))
376 };
377 if let Err(error) = made {
378 report(&error);
379 return;
380 }
381 }
382 let gpu = &*self;
383 let light = clear[3] == 0.0 || crate::encode(luminance(clear)) >= 0.5;
384 let mut painter = Painter::new(gpu, frame, target.context.clone(), size, light);
385 if clear[3] < 1.0 {
386 painter
387 .context
388 .clear_rect(0.0, 0.0, size[0].into(), size[1].into());
389 }
390 if clear[3] > 0.0 {
391 painter.context.set_fill_style_str(&css(clear));
392 painter
393 .context
394 .fill_rect(0.0, 0.0, size[0].into(), size[1].into());
395 }
396 let mut used = 0;
397 for batch in frame.batches {
398 match batch.blend {
399 Blend::Group(index) if onscreen && leaning(&batch) => {
400 painter.finish();
401 let [leaned, above] = [used, used + 1].map(|at| &gpu.overlays[at]);
402 leaned.resize(size);
403 let mut group = Painter::new(gpu, frame, leaned.context.clone(), size, light);
404 group
405 .context
406 .clear_rect(0.0, 0.0, size[0].into(), size[1].into());
407 group.batches(&frame.groups[index].batches);
408 group.finish();
409 lean(leaned, target, Some(frame.groups[index].motion), size);
410 above.resize(size);
411 lean(above, target, None, size);
412 painter = Painter::new(gpu, frame, above.context.clone(), size, light);
413 painter
414 .context
415 .clear_rect(0.0, 0.0, size[0].into(), size[1].into());
416 used += 2;
417 }
418 _ => painter.batch(batch),
419 }
420 }
421 painter.finish();
422 for overlay in &gpu.overlays[used..] {
423 let style = overlay.canvas.style();
424 if style.get_property_value("display").as_deref() != Ok("none") {
425 let _ = style.set_property("display", "none");
426 let _ = style.remove_property("transform");
427 let _ = style.remove_property("opacity");
428 }
429 }
430 }
431}
432
433/// The atlas rectangle `[left, top, right, bottom]` a quad copies, the atlas `side` texels
434/// square.
435fn source(quad: &[Vertex], side: f32) -> [f32; 4] {
436 let [u0, v0] = quad[0].uv.map(|value| (value * side).round());
437 let [u1, v1] = quad[2].uv.map(|value| (value * side).round());
438 [u0, v0, u1, v1]
439}
440
441/// The one colour a quad copying a mask from the atlas paints it in, other than white, which
442/// copies as it is.
443fn tinted(quad: &[Vertex]) -> Option<[f32; 4]> {
444 let vertex = &quad[0];
445 let shape = vertex.blur.abs() > 1e-6 || (vertex.shape[0] > 0.0 && vertex.shape[1] > 0.0);
446 let solid = quad[0].uv == quad[2].uv;
447 let color = quad[0].color;
448 (!shape && !solid && quad[1].color == color && color[..3] != [1.0; 3]).then_some(color)
449}
450
451/// Colours what `context` holds within `[x, y, width, height]` in its fill. Clipped there, as
452/// colouring "source-in" clears the whole canvas outside what it fills.
453fn color_in(context: &Context, [x, y, width, height]: [f64; 4]) {
454 context.save();
455 context.begin_path();
456 context.rect(x, y, width, height);
457 context.clip();
458 context.set_global_composite_operation("source-in").ok();
459 context.fill_rect(x, y, width, height);
460 context.restore();
461}
462
463/// Writes straight-alpha `rgba`, `size` pixels, into `context` at `origin`.
464fn put(context: &Context, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) {
465 let written = ImageData::new_with_u8_clamped_array_and_sh(Clamped(rgba), size[0], size[1])
466 .and_then(|data| context.put_image_data(&data, origin[0].into(), origin[1].into()));
467 if let Err(error) = written {
468 report(&format!("{error:?}"));
469 }
470}
471
472fn report(error: &str) {
473 web_sys::console::error_1(&format!("Canvas 2D: {error}").into());
474}
475
476/// A canvas laid just after `previous` in the page, over it, taking no input.
477fn overlay(previous: &Target) -> Result<Target, String> {
478 let overlay = Target::detached([1; 2])?;
479 let canvas = &overlay.canvas;
480 canvas
481 .set_attribute("aria-hidden", "true")
482 .map_err(|error| format!("{error:?}"))?;
483 let style = canvas.style();
484 for (name, value) in [
485 ("position", "fixed"),
486 ("pointer-events", "none"),
487 ("transform-origin", "0 0"),
488 ("display", "none"),
489 ] {
490 style
491 .set_property(name, value)
492 .map_err(|error| format!("{error:?}"))?;
493 }
494 previous
495 .canvas
496 .after_with_node_1(canvas)
497 .map_err(|error| format!("{error:?}"))?;
498 Ok(overlay)
499}
500
501/// Shows `overlay` exactly over `target`, leaned back by `motion` as `Motion::project` leans a
502/// group, or flat without one.
503fn lean(overlay: &Target, target: &Target, motion: Option<Motion>, size: [u32; 2]) {
504 let rect = target.canvas.get_bounding_client_rect();
505 let [width, height] = [rect.width(), rect.height()];
506 let style = overlay.canvas.style();
507 let mut properties = vec![
508 ("display", "block".to_owned()),
509 ("left", format!("{}px", rect.left())),
510 ("top", format!("{}px", rect.top())),
511 ("width", format!("{width}px")),
512 ("height", format!("{height}px")),
513 ];
514 if let Some(motion) = motion.filter(|_| width > 0.0 && height > 0.0) {
515 // CSS pixels per device pixel, on each axis.
516 let ratio = [width / f64::from(size[0]), height / f64::from(size[1])];
517 let [x, y] = [0, 1].map(|axis| f64::from(motion.pivot[axis]) * ratio[axis]);
518 let distance = f64::from(Motion::DISTANCE) * height;
519 properties.push((
520 "transform",
521 format!(
522 "translate({x}px, {y}px) perspective({distance}px) rotateX({}rad) \
523 translate({}px, {}px)",
524 -motion.tilt, -x, -y
525 ),
526 ));
527 properties.push(("opacity", motion.opacity.to_string()));
528 } else {
529 properties.push(("transform", "none".to_owned()));
530 properties.push(("opacity", "1".to_owned()));
531 }
532 for (name, value) in properties {
533 let _ = style.set_property(name, &value);
534 }
535}
536
537/// Where a quad paints: within a scissor rectangle and, where it has one, a rounded rectangle's
538/// middle, half size and corner radius.
539#[derive(Clone, Copy, PartialEq)]
540struct Clip {
541 scissor: [u32; 4],
542 round: Option<[f32; 5]>,
543}
544
545/// Glyph quads of one colour waiting to take it together: their device bounds, and each one's
546/// atlas and device rectangles.
547struct Run {
548 colors: [[f32; 4]; 2],
549 bounds: [f32; 4],
550 quads: Vec<([f32; 4], [f32; 4])>,
551}
552
553/// Draws batches into one context, setting only the state that changes.
554struct Painter<'a> {
555 gpu: &'a Gpu,
556 frame: &'a Frame<'a>,
557 context: Context,
558 size: [u32; 2],
559 /// The clip in force; none paints everywhere.
560 clip: Option<Clip>,
561 composite: &'static str,
562 alpha: f32,
563 run: Option<Run>,
564 /// Whether the frame clears to a light colour, which faded pictures most likely lie on.
565 light: bool,
566}
567
568impl<'a> Painter<'a> {
569 fn new(
570 gpu: &'a Gpu,
571 frame: &'a Frame<'a>,
572 context: Context,
573 size: [u32; 2],
574 light: bool,
575 ) -> Self {
576 context.save();
577 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
578 context.set_global_composite_operation("source-over").ok();
579 context.set_global_alpha(1.0);
580 context.set_image_smoothing_enabled(true);
581 // Kept between clips: `restore` returns to this state.
582 context.save();
583 Self {
584 gpu,
585 frame,
586 context,
587 size,
588 clip: None,
589 composite: "source-over",
590 alpha: 1.0,
591 run: None,
592 light,
593 }
594 }
595
596 fn finish(&mut self) {
597 self.flush();
598 self.context.restore();
599 self.context.restore();
600 }
601
602 fn batches(&mut self, batches: &[Batch]) {
603 for batch in batches {
604 self.batch(batch);
605 }
606 }
607
608 fn batch(&mut self, batch: &Batch) {
609 let range = batch.vertices.start as usize..batch.vertices.end as usize;
610 let vertices = &self.frame.vertices[range];
611 if let Blend::Group(index) = batch.blend {
612 self.flush();
613 self.set_clip(None);
614 return self.group(index, vertices);
615 }
616 for quad in vertices.chunks_exact(6) {
617 let clip = self.clip_of(batch.scissor, quad);
618 self.set_clip(clip);
619 match batch.blend {
620 Blend::Image(id) => self.image(id, quad),
621 Blend::Over => self.quad(quad, "source-over"),
622 Blend::Erase => self.quad(quad, "destination-out"),
623 Blend::Multiply => self.quad(quad, "multiply"),
624 Blend::Group(_) => unreachable!("Groups paint above"),
625 }
626 }
627 }
628
629 /// What `quad` paints within: none where its scissor and rounded rectangle hold it all,
630 /// as changing the canvas's clip costs.
631 fn clip_of(&self, scissor: [u32; 4], quad: &[Vertex]) -> Option<Clip> {
632 let corners = [&quad[0], &quad[1], &quad[2], &quad[5]].map(|vertex| self.device(vertex));
633 let [mut left, mut top, mut right, mut bottom] = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
634 for [x, y] in corners {
635 [left, top] = [left.min(x), top.min(y)];
636 [right, bottom] = [right.max(x), bottom.max(y)];
637 }
638 let within =
639 |[x0, y0, x1, y1]: [f32; 4]| left >= x0 && top >= y0 && right <= x1 && bottom <= y1;
640 let full = [0, 0, self.size[0], self.size[1]];
641 let [x, y, width, height] = scissor.map(|value| value as f32);
642 let scissor = if within([x, y, x + width, y + height]) {
643 full
644 } else {
645 scissor
646 };
647 let vertex = &quad[0];
648 let round = (vertex.clip[0] > 0.0)
649 .then(|| {
650 let [x, y] = self.device(vertex);
651 [
652 x - vertex.clip_local[0],
653 y - vertex.clip_local[1],
654 vertex.clip[0],
655 vertex.clip[1],
656 vertex.clip[2],
657 ]
658 .map(snap)
659 })
660 .filter(|[x, y, half_x, half_y, radius]| {
661 !within([
662 x - half_x,
663 y - half_y + radius,
664 x + half_x,
665 y + half_y - radius,
666 ]) && !within([
667 x - half_x + radius,
668 y - half_y,
669 x + half_x - radius,
670 y + half_y,
671 ])
672 });
673 (scissor != full || round.is_some()).then_some(Clip { scissor, round })
674 }
675
676 fn set_clip(&mut self, clip: Option<Clip>) {
677 if self.clip == clip {
678 return;
679 }
680 self.flush();
681 self.context.restore();
682 self.context.save();
683 self.composite = "source-over";
684 self.alpha = 1.0;
685 self.clip = clip;
686 let Some(Clip { scissor, round }) = clip else {
687 return;
688 };
689 let context = &self.context;
690 if scissor != [0, 0, self.size[0], self.size[1]] {
691 context.begin_path();
692 let [x, y, width, height] = scissor.map(f64::from);
693 context.rect(x, y, width, height);
694 context.clip();
695 }
696 if let Some([x, y, half_x, half_y, radius]) = round {
697 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, x.into(), y.into());
698 context.begin_path();
699 rounded(context, [half_x, half_y], [radius; 2]);
700 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
701 context.clip();
702 }
703 }
704
705 fn set_blend(&mut self, composite: &'static str, alpha: f32) {
706 if self.composite != composite {
707 self.context.set_global_composite_operation(composite).ok();
708 self.composite = composite;
709 }
710 if self.alpha != alpha {
711 self.context.set_global_alpha(alpha.into());
712 self.alpha = alpha;
713 }
714 }
715
716 /// Device position of `vertex`, whole pixels kept whole.
717 fn device(&self, vertex: &Vertex) -> [f32; 2] {
718 let [width, height] = self.size.map(|side| side as f32);
719 [
720 snap((vertex.position[0] + 1.0) * width / 2.0),
721 snap((1.0 - vertex.position[1]) * height / 2.0),
722 ]
723 }
724
725 /// The device rectangle an axis-aligned quad covers.
726 fn rect(&self, quad: &[Vertex]) -> [f32; 4] {
727 let [left, top] = self.device(&quad[0]);
728 let [right, bottom] = self.device(&quad[2]);
729 [left, top, right, bottom]
730 }
731
732 fn image(&mut self, id: u64, quad: &[Vertex]) {
733 self.flush();
734 self.set_blend("source-over", 1.0);
735 let Image(Some(picture)) = self.frame.image::<Image>(id) else {
736 return;
737 };
738 let [left, top, right, bottom] = self.rect(quad).map(f64::from);
739 let [width, height] = [picture.width(), picture.height()].map(f64::from);
740 let _ = self
741 .context
742 .draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
743 picture,
744 0.0,
745 0.0,
746 width,
747 height,
748 left,
749 top,
750 right - left,
751 bottom - top,
752 );
753 }
754
755 fn quad(&mut self, quad: &[Vertex], composite: &'static str) {
756 let vertex = &quad[0];
757 let colors = [quad[0].color, quad[1].color];
758 let solid = quad[0].uv == quad[2].uv;
759 let shape = vertex.blur.abs() > 1e-6 || (vertex.shape[0] > 0.0 && vertex.shape[1] > 0.0);
760 if !shape && !solid {
761 return self.glyph(quad, colors, composite);
762 }
763 self.flush();
764 if shape {
765 return self.shape(quad, colors, composite);
766 }
767 let [left, top, right, bottom] = self.rect(quad).map(f64::from);
768 self.set_blend(composite, 1.0);
769 self.fill(colors, [0.0, top, 0.0, bottom]);
770 self.context
771 .fill_rect(left, top, right - left, bottom - top);
772 }
773
774 /// Sets the fill and stroke to `colors`, shading from `colors[0]` at `span`'s top to
775 /// `colors[1]` at its bottom.
776 fn fill(&self, colors: [[f32; 4]; 2], span: [f64; 4]) {
777 if colors[0] == colors[1] {
778 let color = css(colors[0]);
779 self.context.set_fill_style_str(&color);
780 self.context.set_stroke_style_str(&color);
781 } else {
782 let gradient = self
783 .context
784 .create_linear_gradient(0.0, span[1], 0.0, span[3]);
785 for (stop, color) in [0.0, 1.0].into_iter().zip(colors) {
786 let _ = gradient.add_color_stop(stop, &css(color));
787 }
788 self.context.set_fill_style_canvas_gradient(&gradient);
789 self.context.set_stroke_style_canvas_gradient(&gradient);
790 }
791 }
792
793 /// A rounded rectangle, filled, stroked or dashed, a tapered capsule or a shadow, drawn in
794 /// the quad's own frame.
795 fn shape(&mut self, quad: &[Vertex], colors: [[f32; 4]; 2], composite: &'static str) {
796 let [a, b, d] = [&quad[0], &quad[1], &quad[5]];
797 let [corner, below, across] = [a, b, d].map(|vertex| self.device(vertex));
798 let [span_x, span_y] = [d.local[0] - a.local[0], b.local[1] - a.local[1]];
799 if span_x == 0.0 || span_y == 0.0 {
800 return;
801 }
802 let x_axis = [0, 1].map(|axis| f64::from((across[axis] - corner[axis]) / span_x));
803 let y_axis = [0, 1].map(|axis| f64::from((below[axis] - corner[axis]) / span_y));
804 let origin = [0, 1].map(|axis| {
805 f64::from(corner[axis])
806 - x_axis[axis] * f64::from(a.local[0])
807 - y_axis[axis] * f64::from(a.local[1])
808 });
809 self.set_blend(composite, 1.0);
810 let context = &self.context;
811 let _ = context.set_transform(
812 x_axis[0], x_axis[1], y_axis[0], y_axis[1], origin[0], origin[1],
813 );
814 let span = [0.0, a.local[1], 0.0, b.local[1]].map(f64::from);
815 let [half_x, half_y, radius_x, radius_y] = a.shape;
816 context.begin_path();
817 if a.blur > 1e-6 {
818 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
819 let color = colors[0];
820 // A wide blur loses nothing drawn smaller and enlarged, which a browser blurring
821 // in software does many times faster.
822 let scale = (a.blur / 3.0).floor().clamp(1.0, 4.0);
823 let [half, radius, sigma] = [[half_x, half_y], [radius_x; 2], [a.blur; 2]]
824 .map(|values| values.map(|value| value / scale));
825 let reach = (3.0 * sigma[0]).ceil() + 2.0;
826 let size = half.map(|half| (2.0 * (half + reach)).ceil() as u32);
827 let key = (half.into_iter().chain([radius[0], sigma[0]]).chain(color))
828 .map(f32::to_bits)
829 .collect::<Vec<_>>();
830 let small = |context: &Context, middle: [f64; 2]| {
831 cast(context, middle, half, radius[0], sigma[0], color);
832 };
833 match self.gpu.shadow(key, size, small) {
834 Some(sheet) => {
835 let [width, height] = size.map(|side| f64::from(side as f32 * scale));
836 let _ = context.draw_image_with_html_canvas_element_and_dw_and_dh(
837 &sheet,
838 origin[0] - width / 2.0,
839 origin[1] - height / 2.0,
840 width,
841 height,
842 );
843 }
844 None => cast(context, origin, [half_x, half_y], radius_x, a.blur, color),
845 }
846 } else if a.blur < -0.5 {
847 self.fill(colors, span);
848 taper(context, half_x, half_y, radius_x);
849 context.fill();
850 } else {
851 self.fill(colors, span);
852 let width = a.stroke.abs();
853 let radii = [radius_x, radius_y];
854 if a.stroke < -1e-6 {
855 let middle = radii.map(|radius| (radius - width / 2.0).max(0.0));
856 let middle = if middle.contains(&0.0) {
857 [0.0; 2]
858 } else {
859 middle
860 };
861 rounded(
862 context,
863 [half_x - width / 2.0, half_y - width / 2.0],
864 middle,
865 );
866 let dash = js_sys::Array::of2(&(2.0 * width).into(), &(2.0 * width).into());
867 let _ = context.set_line_dash(&dash);
868 context.set_line_dash_offset(-f64::from(width));
869 context.set_line_width(width.into());
870 context.stroke();
871 let _ = context.set_line_dash(&js_sys::Array::new());
872 } else {
873 rounded(context, [half_x, half_y], radii);
874 if width > 1e-6 && width < half_x.min(half_y) {
875 let inner = radii.map(|radius| (radius - width).max(0.0));
876 rounded(context, [half_x - width, half_y - width], inner);
877 context.fill_with_canvas_winding_rule(CanvasWindingRule::Evenodd);
878 } else {
879 context.fill();
880 }
881 }
882 }
883 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
884 }
885
886 /// A glyph, icon or path from the atlas: drawn as it is where white, else gathered with
887 /// glyphs of its colour.
888 fn glyph(&mut self, quad: &[Vertex], colors: [[f32; 4]; 2], composite: &'static str) {
889 let source = source(quad, self.gpu.atlas_side() as f32);
890 let rect = self.rect(quad);
891 let white = colors[0] == colors[1] && colors[0][..3] == [1.0; 3];
892 if composite != "source-over" || white {
893 self.flush();
894 // A faded icon's colours are its own; it fades as a mid grey would over the
895 // frame's light or dark colour.
896 let grey = if self.light { 0.18 } else { 0.5 };
897 let alpha = if white {
898 opacity([grey, grey, grey, colors[0][3]])
899 } else {
900 colors[0][3]
901 };
902 self.set_blend(composite, alpha);
903 return self.copy(&self.gpu.atlas.canvas, &self.context, source, rect);
904 }
905 if let Some(color) = tinted(quad)
906 && let Some(tint) =
907 (self.gpu.tints.iter()).find(|tint| tint.color == color.map(f32::to_bits))
908 && let Some([x, y]) = tint.glyphs.get(&source.map(|value| value as u32))
909 {
910 self.flush();
911 self.set_blend(composite, 1.0);
912 let [x, y] = [*x, *y].map(|value| value as f32);
913 let colored = [x, y, x + source[2] - source[0], y + source[3] - source[1]];
914 return self.copy(&tint.sheet.canvas, &self.context, colored, rect);
915 }
916 if let Some(run) = &mut self.run
917 && run.colors == colors
918 && colors[0] == colors[1]
919 {
920 run.bounds = [
921 run.bounds[0].min(rect[0]),
922 run.bounds[1].min(rect[1]),
923 run.bounds[2].max(rect[2]),
924 run.bounds[3].max(rect[3]),
925 ];
926 run.quads.push((source, rect));
927 return;
928 }
929 self.flush();
930 self.run = Some(Run {
931 colors,
932 bounds: rect,
933 quads: vec![(source, rect)],
934 });
935 }
936
937 /// Copies `source` of `sheet`, the atlas or a tint of it, to `rect` of `context`.
938 fn copy(&self, sheet: &Canvas, context: &Context, source: [f32; 4], rect: [f32; 4]) {
939 let [u0, v0, u1, v1] = source.map(f64::from);
940 let [left, top, right, bottom] = rect.map(f64::from);
941 let _ = context
942 .draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
943 sheet,
944 u0,
945 v0,
946 u1 - u0,
947 v1 - v0,
948 left,
949 top,
950 right - left,
951 bottom - top,
952 );
953 }
954
955 /// Paints the glyphs waiting for their colour: their coverage gathered in the scratch
956 /// canvas, coloured there, then laid over the target.
957 fn flush(&mut self) {
958 let Some(run) = self.run.take() else {
959 return;
960 };
961 let [width, height] = self.size.map(|side| side as f32);
962 let [left, top] = [
963 run.bounds[0].max(0.0).floor(),
964 run.bounds[1].max(0.0).floor(),
965 ];
966 let [right, bottom] = [
967 run.bounds[2].min(width).ceil(),
968 run.bounds[3].min(height).ceil(),
969 ];
970 if right <= left || bottom <= top {
971 return;
972 }
973 let area = [left, top, right - left, bottom - top].map(f64::from);
974 // As small as the run, as the browser copies the whole scratch canvas each time it is
975 // drawn from after being drawn into.
976 let [wide, tall] = [right - left, bottom - top].map(|side| side as u32);
977 let scratch = &self.gpu.scratch;
978 let [have_wide, have_tall] = scratch.size();
979 if have_wide < wide || have_tall < tall {
980 scratch.resize(
981 [wide.max(have_wide), tall.max(have_tall)].map(|side| side.next_multiple_of(64)),
982 );
983 }
984 let scratch = &scratch.context;
985 scratch.clear_rect(0.0, 0.0, area[2], area[3]);
986 let _ = scratch.set_transform(1.0, 0.0, 0.0, 1.0, -area[0], -area[1]);
987 for (source, rect) in &run.quads {
988 self.copy(&self.gpu.atlas.canvas, scratch, *source, *rect);
989 }
990 let [top_color, bottom_color] = run.colors;
991 if top_color == bottom_color {
992 scratch.set_fill_style_str(&css(top_color));
993 } else {
994 let gradient = scratch.create_linear_gradient(
995 0.0,
996 run.bounds[1].into(),
997 0.0,
998 run.bounds[3].into(),
999 );
1000 let _ = gradient.add_color_stop(0.0, &css(top_color));
1001 let _ = gradient.add_color_stop(1.0, &css(bottom_color));
1002 scratch.set_fill_style_canvas_gradient(&gradient);
1003 }
1004 color_in(scratch, area);
1005 let _ = scratch.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
1006 self.set_blend("source-over", 1.0);
1007 let _ = self
1008 .context
1009 .draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
1010 &self.gpu.scratch.canvas,
1011 0.0,
1012 0.0,
1013 area[2],
1014 area[3],
1015 area[0],
1016 area[1],
1017 area[2],
1018 area[3],
1019 );
1020 }
1021
1022 /// Paints `group` offscreen, then its picture over the target, faded. One leaning back
1023 /// leans in a canvas of its own (`lean`) where the target is in the page, and lies flat on
1024 /// one that isn't, as the canvas has no perspective.
1025 fn group(&mut self, index: usize, vertices: &[Vertex]) {
1026 let group = &self.frame.groups[index];
1027 let picture = &self.gpu.groups[index];
1028 picture.resize(self.size);
1029 let [width, height] = self.size.map(|side| side as f32);
1030 // The picture's painted bounds, as the group's batch samples it.
1031 let [mut left, mut top, mut right, mut bottom] = [width, height, 0.0, 0.0];
1032 for vertex in vertices {
1033 let [x, y] = [vertex.uv[0] * width, vertex.uv[1] * height];
1034 [left, top] = [left.min(x), top.min(y)];
1035 [right, bottom] = [right.max(x), bottom.max(y)];
1036 }
1037 if right <= left || bottom <= top {
1038 return;
1039 }
1040 let mut painter = Painter::new(
1041 self.gpu,
1042 self.frame,
1043 picture.context.clone(),
1044 self.size,
1045 self.light,
1046 );
1047 painter
1048 .context
1049 .clear_rect(0.0, 0.0, width.into(), height.into());
1050 painter.batches(&group.batches);
1051 painter.finish();
1052 self.set_blend("source-over", group.motion.opacity);
1053 let [x, y, w, h] = [left, top, right - left, bottom - top].map(f64::from);
1054 let _ = self
1055 .context
1056 .draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
1057 &picture.canvas,
1058 x,
1059 y,
1060 w,
1061 h,
1062 x,
1063 y,
1064 w,
1065 h,
1066 );
1067 }
1068}
1069
1070/// `value`, made whole within a thousandth of a whole number, as quads placed on whole pixels
1071/// are, so pictures on them copy without resampling.
1072fn snap(value: f32) -> f32 {
1073 if (value - value.round()).abs() < 1e-3 {
1074 value.round()
1075 } else {
1076 value
1077 }
1078}
1079
1080/// Draws the shadow a rounded rectangle about `middle`, `half` its half size, casts: blurred
1081/// by `sigma` device pixels, in `color`.
1082fn cast(
1083 context: &Context,
1084 middle: [f64; 2],
1085 half: [f32; 2],
1086 radius: f32,
1087 sigma: f32,
1088 color: [f32; 4],
1089) {
1090 // The canvas's shadow is a gaussian of half its blur as deviation, as `sigma` is here; its
1091 // offset and blur ignore the transform. The rectangle itself lies far off the canvas.
1092 let side = context
1093 .canvas()
1094 .map_or(0, |canvas| canvas.width().max(canvas.height()));
1095 let away = 4.0 * f64::from(side) + 8.0 * f64::from(sigma);
1096 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, middle[0] - away, middle[1]);
1097 context.begin_path();
1098 rounded(context, half, [radius; 2]);
1099 context.set_shadow_color(&css(color));
1100 context.set_shadow_blur(2.0 * f64::from(sigma));
1101 context.set_shadow_offset_x(away);
1102 context.set_fill_style_str("#000");
1103 context.fill();
1104 context.set_shadow_color("transparent");
1105 context.set_shadow_blur(0.0);
1106 context.set_shadow_offset_x(0.0);
1107 let _ = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
1108}
1109
1110/// Adds a rounded rectangle about the origin, `half` its half size, to the path: clockwise
1111/// from the middle of its right edge, which is where a dashed outline's pattern starts.
1112fn rounded(context: &Context, half: [f32; 2], radius: [f32; 2]) {
1113 let [x, y] = half.map(f64::from);
1114 if radius.iter().any(|radius| *radius <= 0.0) {
1115 context.rect(-x, -y, 2.0 * x, 2.0 * y);
1116 return;
1117 }
1118 let [rx, ry] = [0, 1].map(|axis| f64::from(radius[axis].min(half[axis])));
1119 context.move_to(x, 0.0);
1120 for (corner, from) in [
1121 ([x - rx, y - ry], 0.0),
1122 ([-x + rx, y - ry], FRAC_PI_2),
1123 ([-x + rx, -y + ry], PI),
1124 ([x - rx, -y + ry], 3.0 * FRAC_PI_2),
1125 ] {
1126 let _ = context.ellipse(corner[0], corner[1], rx, ry, 0.0, from, from + FRAC_PI_2);
1127 }
1128 context.close_path();
1129}
1130
1131/// Adds the tapered capsule from `(-half, 0)` to `(half, 0)`, round ends `radii` across, to
1132/// the path: the two circles and the lines touching both.
1133fn taper(context: &Context, half: f32, start: f32, end: f32) {
1134 let [half, start, end] = [half, start, end].map(f64::from);
1135 let slope = (start - end) / (2.0 * half).max(1e-6);
1136 if slope.abs() >= 1.0 {
1137 let (x, radius) = if start >= end {
1138 (-half, start)
1139 } else {
1140 (half, end)
1141 };
1142 let _ = context.arc(x, 0.0, radius, 0.0, 2.0 * PI);
1143 return;
1144 }
1145 // Where the touching lines meet each circle, by their outward normal's angle.
1146 let angle = (1.0 - slope * slope).sqrt().atan2(slope);
1147 context.move_to(-half + start * angle.cos(), start * angle.sin());
1148 let _ = context.arc_with_anticlockwise(half, 0.0, end, angle, -angle, true);
1149 let _ = context.arc_with_anticlockwise(-half, 0.0, start, -angle, angle, true);
1150 context.close_path();
1151}
1152
1153/// A CSS colour for linear `color`, its opacity corrected for the canvas's sRGB blending.
1154fn css(color: [f32; 4]) -> String {
1155 let [red, green, blue] = srgb_bytes(color);
1156 format!("rgba({red},{green},{blue},{:.4})", opacity(color))
1157}
1158
1159/// The opacity that, blended sRGB-encoded as the canvas blends, shows `color` as linear light
1160/// blends it over what it stands out against most: white under a dark colour, black under a
1161/// light one.
1162fn opacity([red, green, blue, alpha]: [f32; 4]) -> f32 {
1163 if alpha <= 0.0 || alpha >= 1.0 {
1164 return alpha.clamp(0.0, 1.0);
1165 }
1166 let ink = luminance([red, green, blue, alpha]);
1167 let shown = crate::encode(ink);
1168 let corrected = if shown < 0.5 {
1169 (1.0 - crate::encode(1.0 - alpha * (1.0 - ink))) / (1.0 - shown)
1170 } else {
1171 crate::encode(alpha * ink) / shown
1172 };
1173 corrected.clamp(0.0, 1.0)
1174}
1175
1176fn luminance([red, green, blue, _]: [f32; 4]) -> f32 {
1177 0.2126 * red + 0.7152 * green + 0.0722 * blue
1178}