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