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