• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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>