• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1---
2title: 'CanvasKit - Skia + WebAssembly'
3linkTitle: 'CanvasKit - Skia + WebAssembly'
4
5weight: 20
6---
7
8Skia now offers a WebAssembly build for easy deployment of our graphics APIs on
9the web.
10
11CanvasKit provides a playground for testing new Canvas and SVG platform APIs,
12enabling fast-paced development on the web platform. It can also be used as a
13deployment mechanism for custom web apps requiring cutting-edge features, like
14Skia's [Lottie animation](https://skia.org/docs/user/modules/skottie) support.
15
16## Features
17
18- WebGL context encapsulated as an SkSurface, allowing for direct drawing to an
19  HTML canvas
20- Core set of Skia canvas/paint/path/text APIs available, see bindings
21- Draws to a hardware-accelerated backend
22- Security tested with Skia's fuzzers
23
24## Samples
25
26<style>
27  #demo canvas {
28    border: 1px dashed #AAA;
29    margin: 2px;
30  }
31
32  #patheffect, #ink, #shaping, #shader1, #camera3d {
33    width: 400px;
34    height: 400px;
35  }
36
37  #sk_legos, #sk_drinks, #sk_party, #sk_onboarding {
38    width: 300px;
39    height: 300px;
40  }
41
42  figure {
43    display: inline-block;
44    margin: 0;
45  }
46
47  figcaption > a {
48    margin: 2px 10px;
49  }
50
51</style>
52
53<div id=demo>
54  <h3>Paragraph shaping, custom shaders, and perspective transformation</h3>
55  <figure>
56    <canvas id=shaping width=500 height=500></canvas>
57    <figcaption>
58      <a href="https://jsfiddle.skia.org/canvaskit/6a5c211a8cb4a7752297674b3533f7e1bbc2a78dd37f117c29a77bcc68411f31"
59          target=_blank rel=noopener>
60        SkParagraph JSFiddle</a>
61    </figcaption>
62  </figure>
63  <figure>
64    <canvas id=shader1 width=512 height=512></canvas>
65    <figcaption>
66      <a href="https://jsfiddle.skia.org/canvaskit/b382d3b660c4f314eb6a6eae9c0f1e0aadc95c0a2747b707e0dbe3f65a8b0a14"
67          target=_blank rel=noopener>
68        Shader JSFiddle</a>
69    </figcaption>
70  </figure>
71  <figure>
72    <canvas id=camera3d width=400 height=400></canvas>
73    <figcaption>
74      <a href="https://jsfiddle.skia.org/canvaskit/a5f9e976f1af65ef13bd978a5e265bdcb92110f5e64699fba5e8871c54be22b6"
75          target=_blank rel=noopener>
76        3D Cube JSFiddle</a>
77    </figcaption>
78  </figure>
79
80  <h3>Play back bodymovin lottie files with skottie (click for fiddles)</h3>
81  <a href="https://jsfiddle.skia.org/canvaskit/cb0b72eadb45f7e75b4015db7251e9da2cc202a2ce1cbec8eb2e453d83a619a6"
82     target=_blank rel=noopener>
83    <canvas id=sk_legos width=300 height=300></canvas>
84  </a>
85  <a href="https://jsfiddle.skia.org/canvaskit/e77274c30d63645d3bb82fd366991e27c1e1c3df39def04e999b4fcce9f425a2"
86     target=_blank rel=noopener>
87    <canvas id=sk_drinks width=500 height=500></canvas>
88  </a>
89  <a href="https://jsfiddle.skia.org/canvaskit/e42700132d80efd3470b0f08334556028490ac08d1938210fa618504c6109c99"
90     target=_blank rel=noopener>
91    <canvas id=sk_party width=500 height=500></canvas>
92  </a>
93  <a href="https://jsfiddle.skia.org/canvaskit/987b1f99f4703f9f44dbfb2f43a5ed107672334f68d6262cd53ba44ed7a09236"
94     target=_blank rel=noopener>
95    <canvas id=sk_onboarding width=500 height=500></canvas>
96  </a>
97
98  <h3>Go beyond the HTML Canvas2D</h3>
99  <figure>
100    <canvas id=patheffect width=400 height=400></canvas>
101    <figcaption>
102      <a href="https://jsfiddle.skia.org/canvaskit/3588b3b0a7cc93f36d9fa4f08b397c38971dcb1f80a36107f9ad93c051f2cb28"
103          target=_blank rel=noopener>
104        Star JSFiddle</a>
105    </figcaption>
106  </figure>
107  <figure>
108    <canvas id=ink width=400 height=400></canvas>
109    <figcaption>
110      <a href="https://jsfiddle.skia.org/canvaskit/bd42c174a0dcb2f65ff1f3c803397df14014d1e66b92185e9980dc631a49f258"
111          target=_blank rel=noopener>
112        Ink JSFiddle</a>
113    </figcaption>
114  </figure>
115
116</div>
117
118<script type="text/javascript" charset="utf-8">
119(function() {
120  // Tries to load the WASM version if supported, shows error otherwise
121  let s = document.createElement('script');
122  let locate_file = '';
123  // Hey, if you are looking at this code for an example of how to do it yourself, please use
124  // an actual CDN, such as https://unpkg.com/canvaskit-wasm - it will have better reliability
125  // and niceties like brotli compression.
126  if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') {
127    console.log('WebAssembly is supported!');
128    locate_file = 'https://particles.skia.org/dist/';
129  } else {
130    console.log('WebAssembly is not supported (yet) on this browser.');
131    document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>";
132    return;
133  }
134  s.src = locate_file + 'canvaskit.js';
135  s.onload = () => {
136  let CanvasKit = null;
137  let legoJSON = null;
138  let drinksJSON = null;
139  let confettiJSON = null;
140  let onboardingJSON = null;
141  let fullBounds = [0, 0, 500, 500];
142  const ckLoaded = CanvasKitInit({
143    locateFile: (file) => locate_file + file,
144  });
145
146  ckLoaded.then((CK) => {
147    CanvasKit = CK;
148    DrawingExample(CanvasKit);
149    InkExample(CanvasKit);
150    ShapingExample(CanvasKit);
151     // Set bounds to fix the 4:3 resolution of the legos
152    SkottieExample(CanvasKit, 'sk_legos', legoJSON, [-183, -100, 483, 400]);
153    // Re-size to fit
154    SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
155    SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
156    SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
157    ShaderExample1(CanvasKit);
158  });
159
160  fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
161    resp.text().then((str) => {
162      legoJSON = str;
163      SkottieExample(CanvasKit, 'sk_legos', legoJSON, [-183, -100, 483, 400]);
164    });
165  });
166
167  fetch('https://storage.googleapis.com/skia-cdn/misc/drinks.json').then((resp) => {
168    resp.text().then((str) => {
169      drinksJSON = str;
170      SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
171    });
172  });
173
174  fetch('https://storage.googleapis.com/skia-cdn/misc/confetti.json').then((resp) => {
175    resp.text().then((str) => {
176      confettiJSON = str;
177      SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
178    });
179  });
180
181  fetch('https://storage.googleapis.com/skia-cdn/misc/onboarding.json').then((resp) => {
182    resp.text().then((str) => {
183      onboardingJSON = str;
184      SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
185    });
186  });
187
188  const loadBrickTex = fetch('https://storage.googleapis.com/skia-cdn/misc/brickwork-texture.jpg').then((response) => response.arrayBuffer());
189  const loadBrickBump = fetch('https://storage.googleapis.com/skia-cdn/misc/brickwork_normal-map.jpg').then((response) => response.arrayBuffer());
190  Promise.all([ckLoaded, loadBrickTex, loadBrickBump]).then((results) => {Camera3D(...results)});
191
192  function preventScrolling(canvas) {
193    canvas.addEventListener('touchmove', (e) => {
194      // Prevents touch events in the canvas from scrolling the canvas.
195      e.preventDefault();
196      e.stopPropagation();
197    });
198  }
199
200  function DrawingExample(CanvasKit) {
201    const surface = CanvasKit.MakeCanvasSurface('patheffect');
202    if (!surface) {
203      console.log('Could not make surface');
204    }
205    const paint = new CanvasKit.Paint();
206
207    const textPaint = new CanvasKit.Paint();
208    textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
209    textPaint.setAntiAlias(true);
210
211    const textFont = new CanvasKit.Font(null, 30);
212
213    let i = 0;
214
215    let X = 200;
216    let Y = 200;
217
218    function drawFrame(canvas) {
219      const path = starPath(CanvasKit, X, Y);
220      const dpe = CanvasKit.PathEffect.MakeDash([15, 5, 5, 10], i/5);
221      i++;
222
223      paint.setPathEffect(dpe);
224      paint.setStyle(CanvasKit.PaintStyle.Stroke);
225      paint.setStrokeWidth(5.0 + -3 * Math.cos(i/30));
226      paint.setAntiAlias(true);
227      paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
228
229      canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
230
231      canvas.drawPath(path, paint);
232      canvas.drawText('Try Clicking!', 10, 380, textPaint, textFont);
233      dpe.delete();
234      path.delete();
235      surface.requestAnimationFrame(drawFrame);
236    }
237    surface.requestAnimationFrame(drawFrame);
238
239    // Make animation interactive
240    let interact = (e) => {
241      if (!e.buttons) {
242        return;
243      }
244      X = e.offsetX;
245      Y = e.offsetY;
246    };
247    document.getElementById('patheffect').addEventListener('pointermove', interact);
248    document.getElementById('patheffect').addEventListener('pointerdown', interact);
249    preventScrolling(document.getElementById('patheffect'));
250
251    // A client would need to delete this if it didn't go on forever.
252    // font.delete();
253    // paint.delete();
254  }
255
256  function InkExample(CanvasKit) {
257    const surface = CanvasKit.MakeCanvasSurface('ink');
258    if (!surface) {
259      console.log('Could not make surface');
260    }
261    let paint = new CanvasKit.Paint();
262    paint.setAntiAlias(true);
263    paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
264    paint.setStyle(CanvasKit.PaintStyle.Stroke);
265    paint.setStrokeWidth(4.0);
266    // This effect smooths out the drawn lines a bit.
267    paint.setPathEffect(CanvasKit.PathEffect.MakeCorner(50));
268
269    // Draw I N K
270    let path = new CanvasKit.Path();
271    path.moveTo(80, 30);
272    path.lineTo(80, 80);
273
274    path.moveTo(100, 80);
275    path.lineTo(100, 15);
276    path.lineTo(130, 95);
277    path.lineTo(130, 30);
278
279    path.moveTo(150, 30);
280    path.lineTo(150, 80);
281    path.moveTo(170, 30);
282    path.lineTo(150, 55);
283    path.lineTo(170, 80);
284
285    let paths = [path];
286    let paints = [paint];
287
288    function drawFrame(canvas) {
289      canvas.clear(CanvasKit.WHITE);
290      for (let i = 0; i < paints.length && i < paths.length; i++) {
291        canvas.drawPath(paths[i], paints[i]);
292      }
293      surface.requestAnimationFrame(drawFrame);
294    }
295
296    let hold = false;
297    let interact = (e) => {
298      let type = e.type;
299      if (type === 'lostpointercapture' || type === 'pointerup' || !e.pressure ) {
300        hold = false;
301        return;
302      }
303      if (hold) {
304        path.lineTo(e.offsetX, e.offsetY);
305      } else {
306        paint = paint.copy();
307        paint.setColor(CanvasKit.Color(Math.random() * 255, Math.random() * 255, Math.random() * 255, Math.random() + .2));
308        paints.push(paint);
309        path = new CanvasKit.Path();
310        paths.push(path);
311        path.moveTo(e.offsetX, e.offsetY);
312      }
313      hold = true;
314    };
315    document.getElementById('ink').addEventListener('pointermove', interact);
316    document.getElementById('ink').addEventListener('pointerdown', interact);
317    document.getElementById('ink').addEventListener('lostpointercapture', interact);
318    document.getElementById('ink').addEventListener('pointerup', interact);
319    preventScrolling(document.getElementById('ink'));
320    surface.requestAnimationFrame(drawFrame);
321  }
322
323  function ShapingExample(CanvasKit) {
324    const surface = CanvasKit.MakeCanvasSurface('shaping');
325    if (!surface) {
326      console.log('Could not make surface');
327      return;
328    }
329    let robotoData = null;
330    fetch('https://storage.googleapis.com/skia-cdn/google-web-fonts/Roboto-Regular.ttf').then((resp) => {
331      resp.arrayBuffer().then((buffer) => {
332        robotoData = buffer;
333      });
334    });
335
336    let emojiData = null;
337    fetch('https://storage.googleapis.com/skia-cdn/misc/NotoColorEmoji.ttf').then((resp) => {
338      resp.arrayBuffer().then((buffer) => {
339        emojiData = buffer;
340      });
341    });
342
343    const font = new CanvasKit.Font(null, 18);
344    const fontPaint = new CanvasKit.Paint();
345    fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
346    fontPaint.setAntiAlias(true);
347
348    let paragraph = null;
349    let X = 250;
350    let Y = 250;
351    const str = 'The quick brown fox �� ate a zesty hamburgerfons ��.\nThe ��‍��‍��‍�� laughed.';
352
353    function drawFrame(canvas) {
354      surface.requestAnimationFrame(drawFrame);
355      if (robotoData && emojiData && !paragraph) {
356        const fontMgr = CanvasKit.FontMgr.FromData([robotoData, emojiData]);
357
358        const paraStyle = new CanvasKit.ParagraphStyle({
359          textStyle: {
360            color: CanvasKit.BLACK,
361            fontFamilies: ['Roboto', 'Noto Color Emoji'],
362            fontSize: 50,
363          },
364          textAlign: CanvasKit.TextAlign.Left,
365          maxLines: 7,
366          ellipsis: '...',
367        });
368
369        const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
370        builder.addText(str);
371        paragraph = builder.build();
372      }
373      if (!paragraph) {
374        canvas.drawText(`Fetching Font data...`, 5, 450, fontPaint, font);
375        return;
376      }
377      canvas.clear(CanvasKit.WHITE);
378
379      let wrapTo = 350 + 150 * Math.sin(Date.now() / 2000);
380      paragraph.layout(wrapTo);
381      canvas.drawParagraph(paragraph, 0, 0);
382      canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint);
383
384      const posA = paragraph.getGlyphPositionAtCoordinate(X, Y);
385      const cp = str.codePointAt(posA.pos);
386      if (cp) {
387        const glyph = String.fromCodePoint(cp);
388        canvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is '${glyph}'`, 5, 450, fontPaint, font);
389      }
390    }
391
392    surface.requestAnimationFrame(drawFrame);
393    // Make animation interactive
394    let interact = (e) => {
395      // multiply by 4/5 to account for the difference in the canvas width and the CSS width.
396      // The 10 accounts for where the mouse actually is compared to where it is drawn.
397      X = (e.offsetX * 4/5) - 10;
398      Y = e.offsetY * 4/5;
399    };
400    document.getElementById('shaping').addEventListener('pointermove', interact);
401    document.getElementById('shaping').addEventListener('pointerdown', interact);
402    document.getElementById('shaping').addEventListener('lostpointercapture', interact);
403    document.getElementById('shaping').addEventListener('pointerup', interact);
404    preventScrolling(document.getElementById('shaping'));
405    surface.requestAnimationFrame(drawFrame);
406  }
407
408  function starPath(CanvasKit, X=128, Y=128, R=116) {
409    let p = new CanvasKit.Path();
410    p.moveTo(X + R, Y);
411    for (let i = 1; i < 8; i++) {
412      let a = 2.6927937 * i;
413      p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
414    }
415    return p;
416  }
417
418  function SkottieExample(CanvasKit, id, jsonStr, bounds) {
419    if (!CanvasKit || !jsonStr) {
420      return;
421    }
422    const animation = CanvasKit.MakeAnimation(jsonStr);
423    const duration = animation.duration() * 1000;
424    const size = animation.size();
425    let c = document.getElementById(id);
426    bounds = bounds || {fLeft: 0, fTop: 0, fRight: size.w, fBottom: size.h};
427
428    const surface = CanvasKit.MakeCanvasSurface(id);
429    if (!surface) {
430      console.log('Could not make surface');
431    }
432    let firstFrame = new Date().getTime();
433
434    function drawFrame(canvas) {
435      let now = new Date().getTime();
436      let seek = ((now - firstFrame) / duration) % 1.0;
437
438      animation.seek(seek);
439      animation.render(canvas, bounds);
440
441      surface.requestAnimationFrame(drawFrame);
442    }
443    surface.requestAnimationFrame(drawFrame);
444    //animation.delete();
445  }
446
447  function ShaderExample1(CanvasKit) {
448    if (!CanvasKit) {
449      return;
450    }
451    const surface = CanvasKit.MakeCanvasSurface('shader1');
452    if (!surface) {
453      throw 'Could not make surface';
454    }
455    const paint = new CanvasKit.Paint();
456
457    const prog = `
458uniform float rad_scale;
459uniform float2 in_center;
460uniform float4 in_colors0;
461uniform float4 in_colors1;
462
463half4 main(float2 p) {
464    float2 pp = p - in_center;
465    float radius = sqrt(dot(pp, pp));
466    radius = sqrt(radius);
467    float angle = atan(pp.y / pp.x);
468    float t = (angle + 3.1415926/2) / (3.1415926);
469    t += radius * rad_scale;
470    t = fract(t);
471    return half4(mix(in_colors0, in_colors1, t));
472}
473`;
474
475    const fact = CanvasKit.RuntimeEffect.Make(prog);
476    function drawFrame(canvas) {
477      canvas.clear(CanvasKit.WHITE);
478      const shader = fact.makeShader([
479        Math.sin(Date.now() / 2000) / 5,
480        256, 256,
481        1, 0, 0, 1,
482        0, 1, 0, 1],
483        true/*=opaque*/);
484
485      paint.setShader(shader);
486      canvas.drawRect(CanvasKit.LTRBRect(0, 0, 512, 512), paint);
487      shader.delete();
488      surface.requestAnimationFrame(drawFrame);
489    }
490    surface.requestAnimationFrame(drawFrame);
491  }
492
493  function Camera3D(canvas, textureImgData, normalImgData) {
494    const surface = CanvasKit.MakeCanvasSurface('camera3d');
495    if (!surface) {
496      console.error('Could not make surface');
497      return;
498    }
499
500    const sizeX = document.getElementById('camera3d').width;
501    const sizeY = document.getElementById('camera3d').height;
502
503    let clickToWorld = CanvasKit.M44.identity();
504    let worldToClick = CanvasKit.M44.identity();
505    // rotation of the cube shown in the demo
506    let rotation = CanvasKit.M44.identity();
507    // temporary during a click and drag
508    let clickRotation = CanvasKit.M44.identity();
509
510    // A virtual sphere used for tumbling the object on screen.
511    const vSphereCenter = [sizeX/2, sizeY/2];
512    const vSphereRadius = Math.min(...vSphereCenter);
513
514    // The rounded rect used for each face
515    const margin = vSphereRadius / 20;
516    const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(margin, margin,
517      vSphereRadius - margin, vSphereRadius - margin), margin*2.5, margin*2.5);
518
519    const camAngle = Math.PI / 12;
520    const cam = {
521      'eye'  : [0, 0, 1 / Math.tan(camAngle/2) - 1],
522      'coa'  : [0, 0, 0],
523      'up'   : [0, 1, 0],
524      'near' : 0.05,
525      'far'  : 4,
526      'angle': camAngle,
527    };
528
529    let mouseDown = false;
530    let clickDown = [0, 0]; // location of click down
531    let lastMouse = [0, 0]; // last mouse location
532
533    // keep spinning after mouse up. Also start spinning on load
534    let axis = [0.4, 1, 1];
535    let totalSpin = 0;
536    let spinRate = 0.1;
537    let lastRadians = 0;
538    let spinning = setInterval(keepSpinning, 30);
539
540    const imgscale = CanvasKit.Matrix.scaled(2, 2);
541    const textureShader = CanvasKit.MakeImageFromEncoded(textureImgData).makeShaderCubic(
542      CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3, imgscale);
543    const normalShader = CanvasKit.MakeImageFromEncoded(normalImgData).makeShaderCubic(
544      CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3, imgscale);
545    const children = [textureShader, normalShader];
546
547    const prog = `
548      uniform shader color_map;
549      uniform shader normal_map;
550
551      uniform float3   lightPos;
552      uniform float4x4 localToWorld;
553      uniform float4x4 localToWorldAdjInv;
554
555      float3 convert_normal_sample(half4 c) {
556        float3 n = 2 * c.rgb - 1;
557        n.y = -n.y;
558        return n;
559      }
560
561      half4 main(float2 p) {
562        float3 norm = convert_normal_sample(normal_map.eval(p));
563        float3 plane_norm = normalize(localToWorldAdjInv * float4(norm, 0)).xyz;
564
565        float3 plane_pos = (localToWorld * float4(p, 0, 1)).xyz;
566        float3 light_dir = normalize(lightPos - plane_pos);
567
568        float ambient = 0.2;
569        float dp = dot(plane_norm, light_dir);
570        float scale = min(ambient + max(dp, 0), 1);
571
572        return color_map.eval(p) * half4(float4(scale, scale, scale, 1));
573      }
574`;
575
576    const fact = CanvasKit.RuntimeEffect.Make(prog);
577
578    // properties of light
579    let lightLocation = [...vSphereCenter];
580    let lightDistance = vSphereRadius;
581    let lightIconRadius = 12;
582    let draggingLight = false;
583
584    function computeLightWorldPos() {
585      return CanvasKit.Vector.add(CanvasKit.Vector.mulScalar([...vSphereCenter, 0], 0.5),
586        CanvasKit.Vector.mulScalar(vSphereUnitV3(lightLocation), lightDistance));
587    }
588
589    let lightWorldPos = computeLightWorldPos();
590
591    function drawLight(canvas) {
592      const paint = new CanvasKit.Paint();
593      paint.setAntiAlias(true);
594      paint.setColor(CanvasKit.WHITE);
595      canvas.drawCircle(...lightLocation, lightIconRadius + 2, paint);
596      paint.setColor(CanvasKit.BLACK);
597      canvas.drawCircle(...lightLocation, lightIconRadius, paint);
598    }
599
600    // Takes an x and y rotation in radians and a scale and returns a 4x4 matrix used to draw a
601    // face of the cube in that orientation.
602    function faceM44(rx, ry, scale) {
603      return CanvasKit.M44.multiply(
604        CanvasKit.M44.rotated([0,1,0], ry),
605        CanvasKit.M44.rotated([1,0,0], rx),
606        CanvasKit.M44.translated([0, 0, scale]));
607    }
608
609    const faceScale = vSphereRadius/2
610    const faces = [
611      {matrix: faceM44(         0,         0, faceScale ), color:CanvasKit.RED}, // front
612      {matrix: faceM44(         0,   Math.PI, faceScale ), color:CanvasKit.GREEN}, // back
613
614      {matrix: faceM44( Math.PI/2,         0, faceScale ), color:CanvasKit.BLUE}, // top
615      {matrix: faceM44(-Math.PI/2,         0, faceScale ), color:CanvasKit.CYAN}, // bottom
616
617      {matrix: faceM44(         0, Math.PI/2, faceScale ), color:CanvasKit.MAGENTA}, // left
618      {matrix: faceM44(         0,-Math.PI/2, faceScale ), color:CanvasKit.YELLOW}, // right
619    ];
620
621    // Returns a component of the matrix m indicating whether it faces the camera.
622    // If it's positive for one of the matrices representing the face of the cube,
623    // that face is currently in front.
624    function front(m) {
625      // Is this invertible?
626      var m2 = CanvasKit.M44.invert(m);
627      if (m2 === null) {
628        m2 = CanvasKit.M44.identity();
629      }
630      // look at the sign of the z-scale of the inverse of m.
631      // that's the number in row 2, col 2.
632      return m2[10]
633    }
634
635    function setClickToWorld(canvas, matrix) {
636      const l2d = canvas.getLocalToDevice();
637      worldToClick = CanvasKit.M44.multiply(CanvasKit.M44.mustInvert(matrix), l2d);
638      clickToWorld = CanvasKit.M44.mustInvert(worldToClick);
639    }
640
641    function normalMatrix(m) {
642      m[3]  = 0;
643      m[7]  = 0;
644      m[11] = 0;
645      m[12] = 0;
646      m[13] = 0;
647      m[14] = 0;
648      m[15] = 1;
649      return CanvasKit.M44.transpose(CanvasKit.M44.mustInvert(m));
650    }
651
652    function drawCubeFace(canvas, m, color) {
653      const trans = new CanvasKit.M44.translated([vSphereRadius/2, vSphereRadius/2, 0]);
654      const localToWorld = new CanvasKit.M44.multiply(m, CanvasKit.M44.mustInvert(trans));
655      canvas.concat(CanvasKit.M44.multiply(trans, localToWorld));
656      const znormal = front(canvas.getLocalToDevice());
657      if (znormal < 0) {
658        return; // skip faces facing backwards
659      }
660      const uniforms = [...lightWorldPos, ...localToWorld, ...normalMatrix(localToWorld)];
661      const paint = new CanvasKit.Paint();
662      paint.setAntiAlias(true);
663      const shader = fact.makeShaderWithChildren(uniforms, true /*=opaque*/, children);
664      paint.setShader(shader);
665      canvas.drawRRect(rr, paint);
666    }
667
668    function drawFrame(canvas) {
669      const clickM = canvas.getLocalToDevice();
670      canvas.save();
671      canvas.translate(vSphereCenter[0] - vSphereRadius/2, vSphereCenter[1] - vSphereRadius/2);
672      // pass surface dimensions as viewport size.
673      canvas.concat(CanvasKit.M44.setupCamera(
674        CanvasKit.LTRBRect(0, 0, vSphereRadius, vSphereRadius), vSphereRadius/2, cam));
675      setClickToWorld(canvas, clickM);
676      for (let f of faces) {
677        const saveCount = canvas.getSaveCount();
678        canvas.save();
679        drawCubeFace(canvas, CanvasKit.M44.multiply(clickRotation, rotation, f.matrix), f.color);
680        canvas.restoreToCount(saveCount);
681      }
682      canvas.restore();  // camera
683      canvas.restore();  // center the following content in the window
684
685      // draw virtual sphere outline.
686      const paint = new CanvasKit.Paint();
687      paint.setAntiAlias(true);
688      paint.setStyle(CanvasKit.PaintStyle.Stroke);
689      paint.setColor(CanvasKit.Color(64, 255, 0, 1.0));
690      canvas.drawCircle(vSphereCenter[0], vSphereCenter[1], vSphereRadius, paint);
691      canvas.drawLine(vSphereCenter[0], vSphereCenter[1] - vSphereRadius,
692                       vSphereCenter[0], vSphereCenter[1] + vSphereRadius, paint);
693      canvas.drawLine(vSphereCenter[0] - vSphereRadius, vSphereCenter[1],
694                       vSphereCenter[0] + vSphereRadius, vSphereCenter[1], paint);
695
696      drawLight(canvas);
697    }
698
699    // convert a 2D point in the circle displayed on screen to a 3D unit vector.
700    // the virtual sphere is a technique selecting a 3D direction by clicking on a the projection
701    // of a hemisphere.
702    function vSphereUnitV3(p) {
703      // v = (v - fCenter) * (1 / fRadius);
704      let v = CanvasKit.Vector.mulScalar(CanvasKit.Vector.sub(p, vSphereCenter), 1/vSphereRadius);
705
706      // constrain the clicked point within the circle.
707      let len2 = CanvasKit.Vector.lengthSquared(v);
708      if (len2 > 1) {
709          v = CanvasKit.Vector.normalize(v);
710          len2 = 1;
711      }
712      // the closer to the edge of the circle you are, the closer z is to zero.
713      const z = Math.sqrt(1 - len2);
714      v.push(z);
715      return v;
716    }
717
718    function computeVSphereRotation(start, end) {
719      const u = vSphereUnitV3(start);
720      const v = vSphereUnitV3(end);
721      // Axis is in the scope of the Camera3D function so it can be used in keepSpinning.
722      axis = CanvasKit.Vector.cross(u, v);
723      const sinValue = CanvasKit.Vector.length(axis);
724      const cosValue = CanvasKit.Vector.dot(u, v);
725
726      let m = new CanvasKit.M44.identity();
727      if (Math.abs(sinValue) > 0.000000001) {
728          m = CanvasKit.M44.rotatedUnitSinCos(
729            CanvasKit.Vector.mulScalar(axis, 1/sinValue), sinValue, cosValue);
730          const radians = Math.atan(cosValue / sinValue);
731          spinRate = lastRadians - radians;
732          lastRadians = radians;
733      }
734      return m;
735    }
736
737    function keepSpinning() {
738      totalSpin += spinRate;
739      clickRotation = CanvasKit.M44.rotated(axis, totalSpin);
740      spinRate *= .998;
741      if (spinRate < 0.01) {
742        stopSpinning();
743      }
744      surface.requestAnimationFrame(drawFrame);
745    }
746
747    function stopSpinning() {
748        clearInterval(spinning);
749        rotation = CanvasKit.M44.multiply(clickRotation, rotation);
750        clickRotation = CanvasKit.M44.identity();
751    }
752
753    function interact(e) {
754      const type = e.type;
755      let eventPos = [e.offsetX, e.offsetY];
756      if (type === 'lostpointercapture' || type === 'pointerup' || type == 'pointerleave') {
757        if (draggingLight) {
758          draggingLight = false;
759        } else if (mouseDown) {
760          mouseDown = false;
761          if (spinRate > 0.02) {
762            stopSpinning();
763            spinning = setInterval(keepSpinning, 30);
764          }
765        } else {
766          return;
767        }
768        return;
769      } else if (type === 'pointermove') {
770        if (draggingLight) {
771          lightLocation = eventPos;
772          lightWorldPos = computeLightWorldPos();
773        } else if (mouseDown) {
774          lastMouse = eventPos;
775          clickRotation = computeVSphereRotation(clickDown, lastMouse);
776        } else {
777          return;
778        }
779      } else if (type === 'pointerdown') {
780        // Are we repositioning the light?
781        if (CanvasKit.Vector.dist(eventPos, lightLocation) < lightIconRadius) {
782          draggingLight = true;
783          return;
784        }
785        stopSpinning();
786        mouseDown = true;
787        clickDown = eventPos;
788        lastMouse = eventPos;
789      }
790      surface.requestAnimationFrame(drawFrame);
791    };
792
793    document.getElementById('camera3d').addEventListener('pointermove', interact);
794    document.getElementById('camera3d').addEventListener('pointerdown', interact);
795    document.getElementById('camera3d').addEventListener('lostpointercapture', interact);
796    document.getElementById('camera3d').addEventListener('pointerleave', interact);
797    document.getElementById('camera3d').addEventListener('pointerup', interact);
798
799    surface.requestAnimationFrame(drawFrame);
800  }
801
802  }
803  document.head.appendChild(s);
804})();
805</script>
806
807Lottie files courtesy of the lottiefiles.com community:
808[Lego Loader](https://www.lottiefiles.com/410-lego-loader),
809[I'm thirsty](https://www.lottiefiles.com/77-im-thirsty),
810[Confetti](https://www.lottiefiles.com/1370-confetti),
811[Onboarding](https://www.lottiefiles.com/1134-onboarding-1)
812
813## Test server
814
815Test your code on our [CanvasKit Fiddle](https://jsfiddle.skia.org/canvaskit)
816
817## Download
818
819Get [CanvasKit on NPM](https://www.npmjs.com/package/canvaskit-wasm).
820Documentation and Typescript definitions are available in the `types/` subfolder
821of the npm package or from the
822[Skia repo](https://github.com/google/skia/tree/main/modules/canvaskit/npm_build/types).
823
824Check out the [quickstart guide](../quickstart) as well.
825