xref: /aosp_15_r20/external/skia/demos.skia.org/demos/mesh2d/index.html (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1<!DOCTYPE html>
2<title>Mesh2D Demo</title>
3<meta charset="utf-8" />
4<meta name="viewport" content="width=device-width, initial-scale=1.0">
5
6<!-- Mesh2D origin trial (https://developer.chrome.com/origintrials/#/view_trial/2797298318550499329) -->
7<meta http-equiv="origin-trial" content="AtLoDlklU0E4Hvr2CcMAmFtHYbi+esffS5I/qCK8i5bG9hhtiqpiJgM9qdK+7sbunIPtgSntEYNWExeHzn1tTAQAAABUeyJvcmlnaW4iOiJodHRwczovL2RlbW9zLnNraWEub3JnOjQ0MyIsImZlYXR1cmUiOiJDYW52YXMyZE1lc2giLCJleHBpcnkiOjE3NDk1OTk5OTl9">
8
9<style>
10  canvas {
11    width: 1024px;
12    height: 1024px;
13    background-color: #ccc;
14    display: none;
15  }
16
17  .root {
18    display: flex;
19  }
20
21  .controls {
22    display: flex;
23  }
24  .controls-left  { width: 50%; }
25  .controls-right { width: 50%; }
26  .controls-right select { width: 100%; }
27
28  #loader {
29    width: 1024px;
30    height: 1024px;
31    display: flex;
32    flex-direction: column;
33    justify-content: center;
34    align-items: center;
35    background-color: #f1f2f3;
36    font: bold 2em monospace;
37    color: #85a2b6;
38  }
39</style>
40
41<div class="root">
42  <div id="loader">
43    <img src="BeanEater-1s-200px.gif">
44    <div>Fetching <a href="https://skia.org/docs/user/modules/canvaskit/">CanvasKit</a>...</div>
45  </div>
46
47  <div id="canvas_wrapper">
48    <canvas id="canvas2d" width="1024" height="1024"></canvas>
49    <canvas id="canvas3d" width="1024" height="1024"></canvas>
50  </div>
51
52  <div class="controls">
53    <div class="controls-left">
54      <div>Show mesh</div>
55      <div>Level of detail</div>
56      <div>Animator</div>
57      <div>Renderer</div>
58    </div>
59    <div class="controls-right">
60      <div>
61        <input type="checkbox" id="show_mesh"/>
62      </div>
63      <div>
64        <select id="lod">
65          <option value="4">4x4</option>
66          <option value="8" selected>8x8</option>
67          <option value="16">16x16</option>
68          <option value="32">32x32</option>
69          <option value="64">64x64</option>
70          <option value="128">128x128</option>
71          <option value="255">255x255</option>
72        </select>
73      </div>
74      <div>
75        <select id="animator">
76          <option value="">Manual</option>
77          <option value="squircleAnimator">Squircle</option>
78          <option value="twirlAnimator">Twirl</option>
79          <option value="wiggleAnimator">Wiggle</option>
80          <option value="cylinderAnimator" selected>Cylinder</option>
81        </select>
82      </div>
83      <div>
84        <select id="renderer" disabled>
85          <option value="ckRenderer" selected>CanvasKit (polyfill)</option>
86          <option value="nativeRenderer">Canvas2D (native)</option>
87        </select>
88      </div>
89    </div>
90  </div>
91</div>
92
93<script type="text/javascript" src="canvaskit.js"></script>
94
95<script type="text/javascript">
96  class MeshData {
97    constructor(size, renderer) {
98      const vertex_count = size*size;
99
100      // 2 floats per point
101      this.verts          = new Float32Array(vertex_count*2);
102      this.animated_verts = new Float32Array(vertex_count*2);
103      this.uvs            = new Float32Array(vertex_count*2);
104
105      let i = 0;
106      for (let y = 0; y < size; ++y) {
107        for (let x = 0; x < size; ++x) {
108          // To keep things simple, all vertices are normalized.
109          this.verts[i + 0] = this.uvs[i + 0] = x / (size - 1);
110          this.verts[i + 1] = this.uvs[i + 1] = y / (size - 1);
111
112          i += 2;
113        }
114      }
115
116      // 2 triangles per LOD square, 3 indices per triangle
117      this.indices = new Uint16Array((size - 1)*(size - 1)*6);
118      i = 0;
119      for (let y = 0; y < size - 1; ++y) {
120        for (let x = 0; x < size - 1; ++x) {
121          const vidx0 = x + y*size;
122          const vidx1 = vidx0 + size;
123
124          this.indices[i++] = vidx0;
125          this.indices[i++] = vidx0 + 1;
126          this.indices[i++] = vidx1 + 1;
127
128          this.indices[i++] = vidx0;
129          this.indices[i++] = vidx1;
130          this.indices[i++] = vidx1 + 1;
131        }
132      }
133
134      // These can be cached upfront (constant during animation).
135      this.uvBuffer    = renderer.makeUVBuffer(this.uvs);
136      this.indexBuffer = renderer.makeIndexBuffer(this.indices);
137    }
138
139    animate(animator) {
140      function bezier(t, p0, p1, p2, p3){
141        return (1 - t)*(1 - t)*(1 - t)*p0 +
142                   3*(1 - t)*(1 - t)*t*p1 +
143                         3*(1 - t)*t*t*p2 +
144                                 t*t*t*p3;
145      }
146
147      // Tuned for non-linear transition.
148      function ease(t) { return bezier(t, 0, 0.4, 1, 1); }
149
150      if (!animator) {
151        return;
152      }
153
154      const ms = Date.now() - timeBase;
155      const  t = Math.abs((ms / 1000) % 2 - 1);
156
157      animator(this.verts, this.animated_verts, t);
158    }
159
160    generateTriangles(func) {
161      for (let i = 0; i < this.indices.length; i += 3) {
162        const i0 = 2*this.indices[i + 0];
163        const i1 = 2*this.indices[i + 1];
164        const i2 = 2*this.indices[i + 2];
165
166        func(this.animated_verts[i0 + 0], this.animated_verts[i0 + 1],
167             this.animated_verts[i1 + 0], this.animated_verts[i1 + 1],
168             this.animated_verts[i2 + 0], this.animated_verts[i2 + 1]);
169      }
170    }
171  }
172
173  class PatchControls {
174    constructor() {
175      this.controls = [
176        { pos: [ 0.00, 0.33], color: '#0ff', deps: []      },
177        { pos: [ 0.00, 0.00], color: '#0f0', deps: [0, 2]  },
178        { pos: [ 0.33, 0.00], color: '#0ff', deps: []      },
179
180        { pos: [ 0.66, 0.00], color: '#0ff', deps: []      },
181        { pos: [ 1.00, 0.00], color: '#0f0', deps: [3, 5]  },
182        { pos: [ 1.00, 0.33], color: '#0ff', deps: []      },
183
184        { pos: [ 1.00, 0.66], color: '#0ff', deps: []      },
185        { pos: [ 1.00, 1.00], color: '#0f0', deps: [6, 8]  },
186        { pos: [ 0.66, 1.00], color: '#0ff', deps: []      },
187
188        { pos: [ 0.33, 1.00], color: '#0ff', deps: []      },
189        { pos: [ 0.00, 1.00], color: '#0f0', deps: [9, 11] },
190        { pos: [ 0.00, 0.66], color: '#0ff', deps: []      },
191      ];
192
193      this.radius = 0.01;
194      this.drag_target = null;
195    }
196
197    mapMouse(ev) {
198      const w = canvas2d.width,
199            h = canvas2d.height;
200      return [
201        (ev.offsetX - w*(1 - meshScale)*0.5)/(w*meshScale),
202        (ev.offsetY - h*(1 - meshScale)*0.5)/(h*meshScale),
203      ];
204    }
205
206    onMouseDown(ev) {
207      const mouse_pos = this.mapMouse(ev);
208
209      for (let i = this.controls.length - 1; i >= 0; --i) {
210        const dx = this.controls[i].pos[0] - mouse_pos[0],
211              dy = this.controls[i].pos[1] - mouse_pos[1];
212
213        if (dx*dx + dy*dy <= this.radius*this.radius) {
214          this.drag_target = this.controls[i];
215          this.drag_offset = [dx, dy];
216          break;
217        }
218      }
219    }
220
221    onMouseMove(ev) {
222      if (!this.drag_target) return;
223
224      const mouse_pos = this.mapMouse(ev),
225                   dx = mouse_pos[0] + this.drag_offset[0] - this.drag_target.pos[0],
226                   dy = mouse_pos[1] + this.drag_offset[1] - this.drag_target.pos[1];
227
228      this.drag_target.pos = [ this.drag_target.pos[0] + dx, this.drag_target.pos[1] + dy ];
229
230      for (let dep_index of this.drag_target.deps) {
231        const dep = this.controls[dep_index];
232        dep.pos = [ dep.pos[0] + dx, dep.pos[1] + dy ];
233      }
234
235      this.updateVerts();
236    }
237
238    onMouseUp(ev) {
239      this.drag_target = null;
240    }
241
242    updateVerts() {
243      this.samplePatch(parseInt(lodSelectUI.value), meshData.animated_verts);
244    }
245
246    drawUI(line_func, circle_func) {
247      for (let i = 0; i < this.controls.length; i += 3) {
248        const c0 = this.controls[i + 0],
249              c1 = this.controls[i + 1],
250              c2 = this.controls[i + 2];
251
252        line_func(c0.pos, c1.pos, '#f00');
253        line_func(c1.pos, c2.pos, '#f00');
254        circle_func(c0.pos, this.radius, c0.color);
255        circle_func(c1.pos, this.radius, c1.color);
256        circle_func(c2.pos, this.radius, c2.color);
257      }
258    }
259
260    // Based on https://github.com/google/skia/blob/de56f293eb41d65786b9e6224fdf9a4702b30f51/src/utils/SkPatchUtils.cpp#L84
261    sampleCubic(cind, lod) {
262      const divisions = lod - 1,
263                    h = 1/divisions,
264                   h2 = h*h,
265                   h3 = h*h2,
266                  pts = [
267                          this.controls[cind[0]].pos,
268                          this.controls[cind[1]].pos,
269                          this.controls[cind[2]].pos,
270                          this.controls[cind[3]].pos,
271                        ],
272               coeffs = [
273                          [
274                            pts[3][0] + 3*(pts[1][0] - pts[2][0]) - pts[0][0],
275                            pts[3][1] + 3*(pts[1][1] - pts[2][1]) - pts[0][1],
276                          ],
277                          [
278                            3*(pts[2][0] - 2*pts[1][0] + pts[0][0]),
279                            3*(pts[2][1] - 2*pts[1][1] + pts[0][1]),
280                          ],
281                          [
282                            3*(pts[1][0] - pts[0][0]),
283                            3*(pts[1][1] - pts[0][1]),
284                          ],
285                          pts[0],
286                        ],
287              fwDiff3 = [
288                          6*h3*coeffs[0][0],
289                          6*h3*coeffs[0][1],
290                        ];
291
292      let fwDiff = [
293                     coeffs[3],
294                     [
295                       h3*coeffs[0][0] + h2*coeffs[1][0] + h*coeffs[2][0],
296                       h3*coeffs[0][1] + h2*coeffs[1][1] + h*coeffs[2][1],
297                     ],
298                     [
299                       fwDiff3[0] + 2*h2*coeffs[1][0],
300                       fwDiff3[1] + 2*h2*coeffs[1][1],
301                     ],
302                     fwDiff3,
303                   ];
304
305      let verts = [];
306
307      for (let i = 0; i <= divisions; ++i) {
308        verts.push(fwDiff[0]);
309        fwDiff[0] = [ fwDiff[0][0] + fwDiff[1][0], fwDiff[0][1] + fwDiff[1][1] ];
310        fwDiff[1] = [ fwDiff[1][0] + fwDiff[2][0], fwDiff[1][1] + fwDiff[2][1] ];
311        fwDiff[2] = [ fwDiff[2][0] + fwDiff[3][0], fwDiff[2][1] + fwDiff[3][1] ];
312      }
313
314      return verts;
315    }
316
317    // Based on https://github.com/google/skia/blob/de56f293eb41d65786b9e6224fdf9a4702b30f51/src/utils/SkPatchUtils.cpp#L256
318    samplePatch(lod, verts) {
319      const top_verts = this.sampleCubic([  1,  2,  3,  4 ], lod),
320          right_verts = this.sampleCubic([  4,  5,  6,  7 ], lod),
321         bottom_verts = this.sampleCubic([ 10,  9,  8,  7 ], lod),
322           left_verts = this.sampleCubic([  1,  0, 11, 10 ], lod);
323
324      let i = 0;
325      for (let y = 0; y < lod; ++y) {
326        const v = y/(lod - 1),
327           left = left_verts[y],
328          right = right_verts[y];
329
330        for (let x = 0; x < lod; ++x) {
331          const u = x/(lod - 1),
332              top = top_verts[x],
333           bottom = bottom_verts[x],
334
335               s0 = [
336                      (1 - v)*top[0] + v*bottom[0],
337                      (1 - v)*top[1] + v*bottom[1],
338                    ],
339               s1 = [
340                      (1 - u)*left[0] + u*right[0],
341                      (1 - u)*left[1] + u*right[1],
342                    ],
343               s2 = [
344                      (1 - v)*((1 - u)*this.controls[ 1].pos[0] + u*this.controls[4].pos[0]) +
345                            v*((1 - u)*this.controls[10].pos[0] + u*this.controls[7].pos[0]),
346                      (1 - v)*((1 - u)*this.controls[ 1].pos[1] + u*this.controls[4].pos[1]) +
347                            v*((1 - u)*this.controls[10].pos[1] + u*this.controls[7].pos[1]),
348                    ];
349
350          verts[i++] = s0[0] + s1[0] - s2[0];
351          verts[i++] = s0[1] + s1[1] - s2[1];
352        }
353      }
354    }
355  }
356
357  class CKRenderer {
358    constructor(ck, img, canvasElement) {
359      this.ck = ck;
360      this.surface = ck.MakeCanvasSurface(canvasElement);
361      this.meshPaint = new ck.Paint();
362
363      // UVs are normalized, so we scale the image shader down to 1x1.
364      const skimg = ck.MakeImageFromCanvasImageSource(img);
365      const localMatrix = [1/skimg.width(),  0, 0,
366                           0, 1/skimg.height(), 0,
367                           0,                0, 1];
368
369      this.meshPaint.setShader(skimg.makeShaderOptions(ck.TileMode.Decal,
370                                                       ck.TileMode.Decal,
371                                                       ck.FilterMode.Linear,
372                                                       ck.MipmapMode.None,
373                                                       localMatrix));
374
375      this.gridPaint = new ck.Paint();
376      this.gridPaint.setColor(ck.BLUE);
377      this.gridPaint.setAntiAlias(true);
378      this.gridPaint.setStyle(ck.PaintStyle.Stroke);
379
380      this.controlsPaint = new ck.Paint();
381      this.controlsPaint.setAntiAlias(true);
382      this.controlsPaint.setStyle(ck.PaintStyle.Fill);
383    }
384
385    // Unlike the native renderer, CK drawVertices() takes typed arrays directly - so
386    // we don't need to allocate separate buffers.
387    makeVertexBuffer(buf) { return buf; }
388    makeUVBuffer    (buf) { return buf; }
389    makeIndexBuffer (buf) { return buf; }
390
391    meshPath(mesh) {
392      // 4 commands per triangle, 3 floats per cmd
393      const cmds = new Float32Array(mesh.indices.length*12);
394      let ci = 0;
395      mesh.generateTriangles((x0, y0, x1, y1, x2, y2) => {
396        cmds[ci++] = this.ck.MOVE_VERB; cmds[ci++] = x0; cmds[ci++] = y0;
397        cmds[ci++] = this.ck.LINE_VERB; cmds[ci++] = x1; cmds[ci++] = y1;
398        cmds[ci++] = this.ck.LINE_VERB; cmds[ci++] = x2; cmds[ci++] = y2;
399        cmds[ci++] = this.ck.LINE_VERB; cmds[ci++] = x0; cmds[ci++] = y0;
400      });
401      return this.ck.Path.MakeFromCmds(cmds);
402    }
403
404    drawMesh(mesh, ctrls) {
405      const vertices = this.ck.MakeVertices(this.ck.VertexMode.Triangles,
406                                            this.makeVertexBuffer(mesh.animated_verts),
407                                            mesh.uvBuffer, null, mesh.indexBuffer, false);
408
409      const canvas = this.surface.getCanvas();
410      const w = this.surface.width(),
411            h = this.surface.height();
412
413      canvas.save();
414        canvas.translate(w*(1-meshScale)*0.5, h*(1-meshScale)*0.5);
415        canvas.scale(w*meshScale, h*meshScale);
416
417        canvas.drawVertices(vertices, this.ck.BlendMode.Dst, this.meshPaint);
418
419        if (showMeshUI.checked) {
420          canvas.drawPath(this.meshPath(mesh), this.gridPaint);
421        }
422
423        ctrls?.drawUI(
424            (p0, p1, color) => {
425                this.controlsPaint.setColor(this.ck.parseColorString(color));
426                canvas.drawLine(p0[0], p0[1], p1[0], p1[1], this.controlsPaint);
427            },
428            (c, r, color) => {
429                this.controlsPaint.setColor(this.ck.parseColorString(color));
430                canvas.drawCircle(c[0], c[1], r, this.controlsPaint);
431            }
432        );
433      canvas.restore();
434      this.surface.flush();
435    }
436  }
437
438  class NativeRenderer {
439    constructor(img, canvasElement) {
440      this.img = img;
441      this.ctx = canvasElement.getContext("2d");
442    }
443
444    // New Mesh2D API: https://github.com/fserb/canvas2D/blob/master/spec/mesh2d.md#mesh2d-api
445    makeVertexBuffer(buf) { return this.ctx.createMesh2DVertexBuffer(buf); }
446    makeUVBuffer(buf) {
447        return this.ctx.createMesh2DUVBuffer(buf);
448    }
449    makeIndexBuffer(buf)  { return this.ctx.createMesh2DIndexBuffer(buf); }
450
451    meshPath(mesh) {
452      const path = new Path2D();
453      mesh.generateTriangles((x0, y0, x1, y1, x2, y2) => {
454        path.moveTo(x0, y0);
455        path.lineTo(x1, y1);
456        path.lineTo(x2, y2);
457        path.lineTo(x0, y0);
458      });
459      return path;
460    }
461
462    drawMesh(mesh, ctrls) {
463      const vbuf = this.ctx.createMesh2DVertexBuffer(mesh.animated_verts);
464      const w = canvas2d.width,
465            h = canvas2d.height;
466
467      this.ctx.clearRect(0, 0, canvas2d.width, canvas2d.height);
468      this.ctx.save();
469        this.ctx.translate(w*(1-meshScale)*0.5, h*(1-meshScale)*0.5);
470        this.ctx.scale(w*meshScale, h*meshScale);
471
472        this.ctx.drawMesh(vbuf, mesh.uvBuffer, mesh.indexBuffer, this.img);
473
474        if (showMeshUI.checked) {
475          this.ctx.strokeStyle = "blue";
476          this.ctx.lineWidth = 0.001;
477          this.ctx.stroke(this.meshPath(mesh));
478        }
479
480        ctrls?.drawUI(
481            (p0, p1, color) => {
482                this.ctx.lineWidth = 0.001;
483                this.ctx.strokeStyle = color;
484                this.ctx.beginPath();
485                this.ctx.moveTo(p0[0], p0[1]);
486                this.ctx.lineTo(p1[0], p1[1]);
487                this.ctx.stroke();
488            },
489            (c, r, color) => {
490                this.ctx.fillStyle = color;
491                this.ctx.beginPath();
492                this.ctx.arc(c[0], c[1], r, 0, 2*Math.PI);
493                this.ctx.fill();
494            }
495        );
496      this.ctx.restore();
497    }
498  }
499
500  function squircleAnimator(verts, animated_verts, t) {
501    function lerp(a, b, t) { return a + t*(b - a); }
502
503    for (let i = 0; i < verts.length; i += 2) {
504      const uvx = verts[i + 0] - 0.5,
505            uvy = verts[i + 1] - 0.5,
506              d = Math.sqrt(uvx*uvx + uvy*uvy)*0.5/Math.max(Math.abs(uvx), Math.abs(uvy)),
507              s = d > 0 ? lerp(1, (0.5/ d), t) : 1;
508      animated_verts[i + 0] = uvx*s + 0.5;
509      animated_verts[i + 1] = uvy*s + 0.5;
510    }
511  }
512
513  function twirlAnimator(verts, animated_verts, t) {
514    const kMaxRotate = Math.PI*4;
515
516    for (let i = 0; i < verts.length; i += 2) {
517      const uvx = verts[i + 0] - 0.5,
518            uvy = verts[i + 1] - 0.5,
519              r = Math.sqrt(uvx*uvx + uvy*uvy),
520              a = kMaxRotate * r * t;
521      animated_verts[i + 0] = uvx*Math.cos(a) - uvy*Math.sin(a) + 0.5;
522      animated_verts[i + 1] = uvy*Math.cos(a) + uvx*Math.sin(a) + 0.5;
523    }
524  }
525
526  function wiggleAnimator(verts, animated_verts, t) {
527    const radius = t*0.2/(Math.sqrt(verts.length/2) - 1);
528
529    for (let i = 0; i < verts.length; i += 2) {
530      const phase = i*Math.PI*0.1505;
531      const angle = phase + t*Math.PI*2;
532      animated_verts[i + 0] = verts[i + 0] + radius*Math.cos(angle);
533      animated_verts[i + 1] = verts[i + 1] + radius*Math.sin(angle);
534    }
535  }
536
537  function cylinderAnimator(verts, animated_verts, t) {
538    const kCylRadius = .2;
539    const cyl_pos = t;
540
541    for (let i = 0; i < verts.length; i += 2) {
542      const uvx = verts[i + 0],
543            uvy = verts[i + 1];
544
545      if (uvx <= cyl_pos) {
546        animated_verts[i + 0] = uvx;
547        animated_verts[i + 1] = uvy;
548        continue;
549      }
550
551      const arc_len = uvx - cyl_pos,
552            arc_ang = arc_len/kCylRadius;
553
554      animated_verts[i + 0] = cyl_pos + Math.sin(arc_ang)*kCylRadius;
555      animated_verts[i + 1] = uvy;
556    }
557  }
558
559  function drawFrame() {
560    meshData.animate(animator);
561    currentRenderer.drawMesh(meshData, patchControls);
562    requestAnimationFrame(drawFrame);
563  }
564
565  function switchRenderer(renderer) {
566    currentRenderer = renderer;
567    meshData = new MeshData(parseInt(lodSelectUI.value), currentRenderer);
568
569    const showCanvas = renderer == ckRenderer ? canvas3d : canvas2d;
570    const hideCanvas = renderer == ckRenderer ? canvas2d : canvas3d;
571    showCanvas.style.display = 'block';
572    hideCanvas.style.display = 'none';
573
574    patchControls?.updateVerts();
575  }
576
577  const canvas2d = document.getElementById("canvas2d");
578  const canvas3d = document.getElementById("canvas3d");
579  const hasMesh2DAPI = 'drawMesh' in CanvasRenderingContext2D.prototype;
580  const showMeshUI = document.getElementById("show_mesh");
581  const lodSelectUI = document.getElementById("lod");
582  const animatorSelectUI = document.getElementById("animator");
583  const rendererSelectUI = document.getElementById("renderer");
584
585  const meshScale = 0.75;
586
587  const loadCK = CanvasKitInit({ locateFile: (file) => 'https://demos.skia.org/demo/mesh2d/' + file });
588  const loadImage = new Promise(resolve => {
589    const image = new Image();
590    image.addEventListener('load', () => { resolve(image); });
591    image.src = 'baby_tux.png';
592  });
593
594  var ckRenderer;
595  var nativeRenderer;
596  var currentRenderer;
597  var meshData;
598  var image;
599
600  const timeBase = Date.now();
601
602  var animator = window[animatorSelectUI.value];
603  var patchControls = animator ? null : new PatchControls();
604
605  Promise.all([loadCK, loadImage]).then(([ck, img]) => {
606    ckRenderer = new CKRenderer(ck, img, canvas3d);
607    nativeRenderer = 'drawMesh' in CanvasRenderingContext2D.prototype
608        ? new NativeRenderer(img, canvas2d)
609        : null;
610
611    rendererSelectUI.disabled = !nativeRenderer;
612    rendererSelectUI.value = nativeRenderer ? "nativeRenderer" : "ckRenderer";
613
614    document.getElementById('loader').style.display = 'none';
615    switchRenderer(nativeRenderer ? nativeRenderer : ckRenderer);
616
617    requestAnimationFrame(drawFrame);
618  });
619
620  lodSelectUI.onchange      = () => { switchRenderer(currentRenderer); }
621  rendererSelectUI.onchange = () => { switchRenderer(window[rendererSelectUI.value]); }
622  animatorSelectUI.onchange = () => {
623    animator = window[animatorSelectUI.value];
624    patchControls = animator ? null : new PatchControls();
625    patchControls?.updateVerts();
626  }
627
628  const cwrapper = document.getElementById('canvas_wrapper');
629  cwrapper.onmousedown = (ev) => { patchControls?.onMouseDown(ev); }
630  cwrapper.onmousemove = (ev) => { patchControls?.onMouseMove(ev); }
631  cwrapper.onmouseup   = (ev) => { patchControls?.onMouseUp(ev); }
632</script>
633