• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<title>CanvasKit Extra features (Skia via Web Assembly)</title>
3<meta charset="utf-8" />
4<meta http-equiv="X-UA-Compatible" content="IE=edge">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
7<style>
8  canvas {
9    border: 1px dashed #AAA;
10  }
11  #sk_legos,#sk_drinks,#sk_party,#sk_onboarding, #sk_animated_gif {
12      width: 300px;
13      height: 300px;
14  }
15
16</style>
17
18<h2> Skottie </h2>
19<canvas id=sk_legos width=300 height=300></canvas>
20<canvas id=sk_drinks width=500 height=500></canvas>
21<canvas id=sk_party width=500 height=500></canvas>
22<canvas id=sk_onboarding width=500 height=500></canvas>
23<canvas id=sk_animated_gif width=500 height=500
24        title='This is an animated gif being animated in Skottie'></canvas>
25
26<h2> RT Shader </h2>
27<canvas id=rtshader width=300 height=300></canvas>
28<canvas id=rtshader2 width=300 height=300></canvas>
29
30<h2> Particles </h2>
31<canvas id=particles width=500 height=500></canvas>
32
33<h2> Paragraph </h2>
34<canvas id=para1 width=600 height=600></canvas>
35<canvas id=para2 width=600 height=600 tabindex='-1'></canvas>
36
37<h2> CanvasKit can serialize/deserialize .skp files</h2>
38<canvas id=skp width=500 height=500></canvas>
39
40<h2> 3D perspective transformations </h2>
41<canvas id=glyphgame width=500 height=500></canvas>
42
43<h2> Support for extended color spaces </h2>
44<a href="chrome://flags/#force-color-profile">Force P3 profile</a>
45<canvas id=colorsupport width=300 height=300></canvas>
46
47<script type="text/javascript" src="/build/canvaskit.js"></script>
48
49<script type="text/javascript" src="textapi_utils.js"></script>
50
51<script type="text/javascript" charset="utf-8">
52
53  var CanvasKit = null;
54  var cdn = 'https://storage.googleapis.com/skia-cdn/misc/';
55
56  const ckLoaded = CanvasKitInit({locateFile: (file) => '/build/'+file});
57
58  const loadLegoJSON = fetch(cdn + 'lego_loader.json').then((response) => response.text());
59  const loadDrinksJSON = fetch(cdn + 'drinks.json').then((response) => response.text());
60  const loadConfettiJSON = fetch(cdn + 'confetti.json').then((response) => response.text());
61  const loadOnboardingJSON = fetch(cdn + 'onboarding.json').then((response) => response.text());
62  const loadMultiframeJSON = fetch(cdn + 'skottie_sample_multiframe.json').then((response) => response.text());
63
64  const loadFlightGif = fetch(cdn + 'flightAnim.gif').then((response) => response.arrayBuffer());
65  const loadFont = fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer());
66  const loadDog = fetch(cdn + 'dog.jpg').then((response) => response.arrayBuffer());
67  const loadMandrill = fetch(cdn + 'mandrill_256.png').then((response) => response.arrayBuffer());
68  const loadBrickTex = fetch(cdn + 'brickwork-texture.jpg').then((response) => response.arrayBuffer());
69  const loadBrickBump = fetch(cdn + 'brickwork_normal-map.jpg').then((response) => response.arrayBuffer());
70
71  const curves = {
72    "MaxCount": 1000,
73    "Drawable": {
74      "Type": "SkCircleDrawable",
75      "Radius": 2
76    },
77    "Code": [`
78      void effectSpawn(inout Effect effect) {
79        effect.rate = 200;
80        effect.color = float4(1, 0, 0, 1);
81      }
82      void spawn(inout Particle p) {
83        p.lifetime = 3 + rand(p.seed);
84        p.vel.y = -50;
85      }
86
87      void update(inout Particle p) {
88        float w = mix(15, 3, p.age);
89        p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand(p.seed));
90        if (rand(p.seed) < 0.5) { p.pos.x = -p.pos.x; }
91
92        p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand(p.seed))) / 255;
93      }
94      `
95    ],
96    "Bindings": []
97  };
98
99  const spiralSkSL = `
100  uniform float rad_scale;
101  uniform float2 in_center;
102  uniform float4 in_colors0;
103  uniform float4 in_colors1;
104
105  half4 main(float2 p) {
106      float2 pp = p - in_center;
107      float radius = sqrt(dot(pp, pp));
108      radius = sqrt(radius);
109      float angle = atan(pp.y / pp.x);
110      float t = (angle + 3.1415926/2) / (3.1415926);
111      t += radius * rad_scale;
112      t = fract(t);
113      return half4(mix(in_colors0, in_colors1, t));
114  }`;
115
116  // Examples which only require canvaskit
117  ckLoaded.then((CK) => {
118    CanvasKit = CK;
119    ParticlesAPI1(CanvasKit);
120    RTShaderAPI1(CanvasKit);
121    ColorSupport(CanvasKit);
122    SkpExample(CanvasKit);
123  });
124
125  // Examples requiring external resources.
126  // Set bounds to fix the 4:3 resolution of the legos
127  Promise.all([ckLoaded, loadLegoJSON]).then(([ck, jsonstr]) => {
128    SkottieExample(ck, 'sk_legos', jsonstr, [-50, 0, 350, 300]);
129  });
130  // Re-size to fit
131  let fullBounds = [0, 0, 500, 500];
132  Promise.all([ckLoaded, loadDrinksJSON]).then(([ck, jsonstr]) => {
133    SkottieExample(ck, 'sk_drinks', jsonstr, fullBounds);
134  });
135  Promise.all([ckLoaded, loadConfettiJSON]).then(([ck, jsonstr]) => {
136    SkottieExample(ck, 'sk_party', jsonstr, fullBounds);
137  });
138  Promise.all([ckLoaded, loadOnboardingJSON]).then(([ck, jsonstr]) => {
139    SkottieExample(ck, 'sk_onboarding', jsonstr, fullBounds);
140  });
141  Promise.all([ckLoaded, loadMultiframeJSON, loadFlightGif]).then(([ck, jsonstr, gif]) => {
142    SkottieExample(ck, 'sk_animated_gif', jsonstr, fullBounds, {'image_0.png': gif});
143  });
144
145  Promise.all([ckLoaded, loadFont]).then((results) => {
146    ParagraphAPI1(...results);
147    ParagraphAPI2(...results);
148    GlyphGame(...results)
149  });
150
151  const rectLeft = 0;
152  const rectTop = 1;
153  const rectRight = 2;
154  const rectBottom = 3;
155
156  function SkottieExample(CanvasKit, id, jsonStr, bounds, assets) {
157    if (!CanvasKit || !jsonStr) {
158      return;
159    }
160    const animation = CanvasKit.MakeManagedAnimation(jsonStr, assets);
161    const duration = animation.duration() * 1000;
162    const size = animation.size();
163    let c = document.getElementById(id);
164    bounds = bounds || CanvasKit.LTRBRect(0, 0, size.w, size.h);
165
166    // Basic managed animation test.
167    if (id === 'sk_drinks') {
168      animation.setColor('BACKGROUND_FILL', CanvasKit.Color(0, 163, 199, 1.0));
169    }
170
171    const surface = CanvasKit.MakeCanvasSurface(id);
172    if (!surface) {
173      console.error('Could not make surface');
174      return;
175    }
176
177    let firstFrame = Date.now();
178
179    function drawFrame(canvas) {
180      let seek = ((Date.now() - firstFrame) / duration) % 1.0;
181      let damage = animation.seek(seek);
182
183      if (damage[rectRight] > damage[rectLeft] && damage[rectBottom] > damage[rectTop]) {
184        canvas.clear(CanvasKit.WHITE);
185        animation.render(canvas, bounds);
186      }
187      surface.requestAnimationFrame(drawFrame);
188    }
189    surface.requestAnimationFrame(drawFrame);
190
191    return surface;
192  }
193
194  function ParticlesAPI1(CanvasKit) {
195    const surface = CanvasKit.MakeCanvasSurface('particles');
196    if (!surface) {
197      console.error('Could not make surface');
198      return;
199    }
200    const canvas = surface.getCanvas();
201    canvas.translate(250, 450);
202
203    const particles = CanvasKit.MakeParticles(JSON.stringify(curves));
204    particles.start(Date.now() / 1000.0, true);
205
206    function drawFrame(canvas) {
207      canvas.clear(CanvasKit.BLACK);
208
209      particles.update(Date.now() / 1000.0);
210      particles.draw(canvas);
211      surface.requestAnimationFrame(drawFrame);
212    }
213    surface.requestAnimationFrame(drawFrame);
214  }
215
216  function ParagraphAPI1(CanvasKit, fontData) {
217    if (!CanvasKit || !fontData) {
218      return;
219    }
220
221    const surface = CanvasKit.MakeCanvasSurface('para1');
222    if (!surface) {
223      console.error('Could not make surface');
224      return;
225    }
226
227    const canvas = surface.getCanvas();
228    const fontMgr = CanvasKit.FontMgr.FromData([fontData]);
229
230    const paraStyle = new CanvasKit.ParagraphStyle({
231        textStyle: {
232            color: CanvasKit.BLACK,
233            fontFamilies: ['Roboto'],
234            fontSize: 50,
235        },
236        textAlign: CanvasKit.TextAlign.Left,
237        maxLines: 5,
238    });
239
240    const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
241    builder.addText('The quick brown fox ate a hamburgerfons and got sick.');
242    const paragraph = builder.build();
243
244    let wrapTo = 0;
245
246    let X = 100;
247    let Y = 100;
248
249    const fontPaint = new CanvasKit.Paint();
250    fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
251    fontPaint.setAntiAlias(true);
252
253    function drawFrame(canvas) {
254      canvas.clear(CanvasKit.WHITE);
255      wrapTo = 350 + 150 * Math.sin(Date.now() / 2000);
256      paragraph.layout(wrapTo);
257      canvas.drawParagraph(paragraph, 0, 0);
258
259      canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint);
260
261      surface.requestAnimationFrame(drawFrame);
262    }
263    surface.requestAnimationFrame(drawFrame);
264
265    let interact = (e) => {
266      X = e.offsetX*2; // multiply by 2 because the canvas is 300 css pixels wide,
267      Y = e.offsetY*2; // but the canvas itself is 600px wide
268    };
269
270    document.getElementById('para1').addEventListener('pointermove', interact);
271    return surface;
272  }
273
274    function ParagraphAPI2(CanvasKit, fontData) {
275      if (!CanvasKit || !fontData) {
276        return;
277      }
278
279      const surface = CanvasKit.MakeCanvasSurface('para2');
280      if (!surface) {
281        console.error('Could not make surface');
282        return;
283      }
284
285      const mouse = MakeMouse();
286      const cursor = MakeCursor(CanvasKit);
287      const canvas = surface.getCanvas();
288
289      const text0 = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
290                    "wet hole full of worms and oozy smells. This was a hobbit-hole and " +
291                    "that means good food, a warm hearth, and all the comforts of home.";
292      const LOC_X = 20,
293            LOC_Y = 20;
294
295      const bgPaint = new CanvasKit.Paint();
296      bgPaint.setColor([0.965, 0.965, 0.965, 1]);
297
298      const editor = MakeEditor(text0, {typeface:null, size:24}, cursor, 400);
299
300      editor.applyStyleToRange({size:100}, 0, 1);
301      editor.applyStyleToRange({italic:true}, 38, 38+6);
302      editor.applyStyleToRange({color:[1,0,0,1]}, 5, 5+4);
303
304      editor.setXY(LOC_X, LOC_Y);
305
306      function drawFrame(canvas) {
307        const lines = editor.getLines();
308
309        canvas.clear(CanvasKit.WHITE);
310
311        if (mouse.isActive()) {
312            const pos = mouse.getPos(-LOC_X, -LOC_Y);
313            const a = lines_pos_to_index(lines, pos[0], pos[1]);
314            const b = lines_pos_to_index(lines, pos[2], pos[3]);
315            if (a == b) {
316                editor.setIndex(a);
317            } else {
318                editor.setIndices(a, b);
319            }
320        }
321
322        canvas.drawRect(editor.bounds(), bgPaint);
323        editor.draw(canvas);
324
325        surface.requestAnimationFrame(drawFrame);
326      }
327      surface.requestAnimationFrame(drawFrame);
328
329      function interact(e) {
330        const type = e.type;
331        if (type === 'pointerup') {
332            mouse.setUp(e.offsetX, e.offsetY);
333        } else if (type === 'pointermove') {
334            mouse.setMove(e.offsetX, e.offsetY);
335        } else if (type === 'pointerdown') {
336            mouse.setDown(e.offsetX, e.offsetY);
337        }
338      };
339
340      function keyhandler(e) {
341          switch (e.key) {
342              case 'ArrowLeft':  editor.moveDX(-1); return;
343              case 'ArrowRight': editor.moveDX(1); return;
344              case 'ArrowUp':
345                e.preventDefault();
346                editor.moveDY(-1);
347                return;
348              case 'ArrowDown':
349                e.preventDefault();
350                editor.moveDY(1);
351                return;
352              case 'Backspace':
353                editor.deleteSelection();
354                return;
355              case 'Shift':
356                return;
357            }
358            if (e.ctrlKey) {
359                switch (e.key) {
360                    case 'r': editor.applyStyleToSelection({color:[1,0,0,1]}); return;
361                    case 'g': editor.applyStyleToSelection({color:[0,0.6,0,1]}); return;
362                    case 'u': editor.applyStyleToSelection({color:[0,0,1,1]}); return;
363                    case 'k': editor.applyStyleToSelection({color:[0,0,0,1]}); return;
364
365                    case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
366                    case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
367                    case 'l': editor.applyStyleToSelection({underline:'toggle'}); return;
368
369                    case ']': editor.applyStyleToSelection({size_add:1}); return;
370                    case '[': editor.applyStyleToSelection({size_add:-1}); return;
371                }
372            }
373            if (!e.ctrlKey && !e.metaKey) {
374                e.preventDefault(); // at least needed for 'space'
375                editor.insert(e.key);
376            }
377      }
378
379      document.getElementById('para2').addEventListener('pointermove', interact);
380      document.getElementById('para2').addEventListener('pointerdown', interact);
381      document.getElementById('para2').addEventListener('pointerup', interact);
382      document.getElementById('para2').addEventListener('keydown', keyhandler);
383      return surface;
384    }
385
386  function RTShaderAPI1(CanvasKit) {
387    if (!CanvasKit) {
388      return;
389    }
390
391    const surface = CanvasKit.MakeCanvasSurface('rtshader');
392    if (!surface) {
393      console.error('Could not make surface');
394      return;
395    }
396
397    const canvas = surface.getCanvas();
398
399    const effect = CanvasKit.RuntimeEffect.Make(spiralSkSL);
400    const shader = effect.makeShader([
401      0.5,
402      150, 150,
403      0, 1, 0, 1,
404      1, 0, 0, 1], true);
405    const paint = new CanvasKit.Paint();
406    paint.setShader(shader);
407    canvas.drawRect(CanvasKit.LTRBRect(0, 0, 300, 300), paint);
408
409    surface.flush();
410    shader.delete();
411    paint.delete();
412    effect.delete();
413  }
414
415  // RTShader2 demo
416  Promise.all([ckLoaded, loadDog, loadMandrill]).then((values) => {
417    const [CanvasKit, dogData, mandrillData] = values;
418    const dogImg = CanvasKit.MakeImageFromEncoded(dogData);
419    if (!dogImg) {
420      console.error('could not decode dog');
421      return;
422    }
423    const mandrillImg = CanvasKit.MakeImageFromEncoded(mandrillData);
424    if (!mandrillImg) {
425      console.error('could not decode mandrill');
426      return;
427    }
428    const quadrantSize = 150;
429
430    const dogShader = dogImg.makeShaderCubic(
431        CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
432        1/3, 1/3,
433        CanvasKit.Matrix.scaled(quadrantSize/dogImg.width(),
434        quadrantSize/dogImg.height()));
435    const mandrillShader = mandrillImg.makeShaderCubic(
436        CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
437        1/3, 1/3,
438        CanvasKit.Matrix.scaled(
439            quadrantSize/mandrillImg.width(),
440            quadrantSize/mandrillImg.height()));
441
442    const surface = CanvasKit.MakeCanvasSurface('rtshader2');
443    if (!surface) {
444      console.error('Could not make surface');
445      return;
446    }
447
448    const prog = `
449      uniform shader before_map;
450      uniform shader after_map;
451      uniform shader threshold_map;
452
453      uniform float cutoff;
454      uniform float slope;
455
456      float smooth_cutoff(float x) {
457          x = x * slope + (0.5 - slope * cutoff);
458          return clamp(x, 0, 1);
459      }
460
461      half4 main(float2 xy) {
462          half4 before = before_map.eval(xy);
463          half4 after = after_map.eval(xy);
464
465          float m = smooth_cutoff(threshold_map.eval(xy).r);
466          return mix(before, after, half(m));
467      }`;
468
469    const canvas = surface.getCanvas();
470
471    const thresholdEffect = CanvasKit.RuntimeEffect.Make(prog);
472    const spiralEffect = CanvasKit.RuntimeEffect.Make(spiralSkSL);
473
474    const draw = (x, y, shader) => {
475      const paint = new CanvasKit.Paint();
476      paint.setShader(shader);
477      canvas.save();
478      canvas.translate(x, y);
479      canvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint);
480      canvas.restore();
481      paint.delete();
482    };
483
484    const offscreenSurface = CanvasKit.MakeSurface(quadrantSize, quadrantSize);
485    const getBlurrySpiralShader = (rad_scale) => {
486      const oCanvas = offscreenSurface.getCanvas();
487
488      const spiralShader = spiralEffect.makeShader([
489      rad_scale,
490      quadrantSize/2, quadrantSize/2,
491      1, 1, 1, 1,
492      0, 0, 0, 1], true);
493
494      const blur = CanvasKit.ImageFilter.MakeBlur(0.1, 0.1, CanvasKit.TileMode.Clamp, null);
495
496      const paint = new CanvasKit.Paint();
497      paint.setShader(spiralShader);
498      paint.setImageFilter(blur);
499      oCanvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint);
500
501      paint.delete();
502      blur.delete();
503      spiralShader.delete();
504      return offscreenSurface.makeImageSnapshot()
505                             .makeShaderCubic(CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
506                             1/3, 1/3);
507
508    };
509
510    const drawFrame = () => {
511      surface.requestAnimationFrame(drawFrame);
512      const thresholdShader = getBlurrySpiralShader(Math.sin(Date.now() / 5000) / 2);
513
514      const blendShader = thresholdEffect.makeShaderWithChildren(
515        [0.5, 10],
516        true, [dogShader, mandrillShader, thresholdShader]);
517      draw(0, 0, blendShader);
518      draw(quadrantSize, 0, thresholdShader);
519      draw(0, quadrantSize, dogShader);
520      draw(quadrantSize, quadrantSize, mandrillShader);
521
522      blendShader.delete();
523    };
524
525    surface.requestAnimationFrame(drawFrame);
526  });
527
528  function SkpExample(CanvasKit) {
529    if (!CanvasKit) {
530      return;
531    }
532
533    const surface = CanvasKit.MakeSWCanvasSurface('skp');
534    if (!surface) {
535      console.error('Could not make surface');
536      return;
537    }
538
539    const paint = new CanvasKit.Paint();
540    paint.setColor(CanvasKit.RED);
541
542    const textPaint = new CanvasKit.Paint();
543    const textFont = new CanvasKit.Font(null, 20);
544    const pr = new CanvasKit.PictureRecorder();
545    const skpCanvas = pr.beginRecording(CanvasKit.LTRBRect(0, 0, 200, 200));
546    skpCanvas.drawRect(CanvasKit.LTRBRect(10, 10, 50, 50), paint);
547    skpCanvas.drawText('If you see this, CanvasKit loaded!!', 5, 100, textPaint, textFont);
548
549    const pic = pr.finishRecordingAsPicture();
550    const skpData = pic.serialize();
551
552    paint.delete();
553    pr.delete();
554
555    const deserialized = CanvasKit.MakePicture(skpData);
556
557    function drawFrame(canvas) {
558      if (deserialized) {
559        canvas.drawPicture(deserialized);
560      } else {
561        canvas.drawText('SKP did not deserialize', 5, 100, textPaint, textFont);
562      }
563    }
564    surface.drawOnce(drawFrame);
565    textPaint.delete();
566    textFont.delete();
567  }
568
569  // Shows a hidden message by rotating all the characters in a kind of way that makes you
570  // search with your mouse.
571  function GlyphGame(canvas, robotoData) {
572    const surface = CanvasKit.MakeCanvasSurface('glyphgame');
573    if (!surface) {
574      console.error('Could not make surface');
575      return;
576    }
577    const sizeX = document.getElementById('glyphgame').width;
578    const sizeY = document.getElementById('glyphgame').height;
579    const halfDim = Math.min(sizeX, sizeY) / 2;
580    const margin = 50;
581    const marginTop = 25;
582    let rotX = 0; //  expected to be updated in interact()
583    let rotY = 0;
584    let pointer = [500, 450];
585    const radPerPixel = 0.005; // radians of subject rotation per pixel distance moved by mouse.
586
587    const camAngle = Math.PI / 12;
588    const cam = {
589      'eye'  : [0, 0, 1 / Math.tan(camAngle/2) - 1],
590      'coa'  : [0, 0, 0],
591      'up'   : [0, 1, 0],
592      'near' : 0.02,
593      'far'  : 4,
594      'angle': camAngle,
595    };
596
597    let lastImage = null;
598
599    const fontMgr = CanvasKit.FontMgr.FromData([robotoData]);
600
601    const paraStyle = new CanvasKit.ParagraphStyle({
602        textStyle: {
603            color: CanvasKit.Color(105, 56, 16), // brown
604            fontFamilies: ['Roboto'],
605            fontSize: 28,
606        },
607        textAlign: CanvasKit.TextAlign.Left,
608    });
609    const hStyle = CanvasKit.RectHeightStyle.Max;
610    const wStyle = CanvasKit.RectWidthStyle.Tight;
611
612    const quotes = [
613      'Some activities superficially familiar to you are merely stupid and should be avoided for your safety, although they are not illegal as such. These include: giving your bank account details to the son of the Nigerian Minister of Finance; buying title to bridges, skyscrapers, spacecraft, planets, or other real assets; murder; selling your identity; and entering into financial contracts with entities running Economics 2.0 or higher.',
614      // Charles Stross - Accelerando
615      'If only there were evil people somewhere insidiously committing evil deeds, and it were necessary only to separate them from the rest of us and destroy them. But the line dividing good and evil cuts through the heart of every human being. And who is willing to destroy a piece of his own heart?',
616      // Aleksandr Solzhenitsyn - The Gulag Archipelago
617      'There is one metaphor of which the moderns are very fond; they are always saying, “You can’t put the clock back.” The simple and obvious answer is “You can.” A clock, being a piece of human construction, can be restored by the human finger to any figure or hour. In the same way society, being a piece of human construction, can be reconstructed upon any plan that has ever existed.',
618      // G. K. Chesterton - What's Wrong With The World?
619    ];
620
621    // pick one at random
622    const text = quotes[Math.floor(Math.random()*3)];
623    const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
624    builder.addText(text);
625    const paragraph = builder.build();
626    const font = new CanvasKit.Font(null, 18);
627    // wrap the text to a given width.
628    paragraph.layout(sizeX - margin*2);
629
630    // to rotate every glyph individually, calculate the bounding rect of each one,
631    // construct an array of rects and paragraphs that would draw each glyph individually.
632    const letters = Array(text.length);
633    for (let i = 0; i < text.length; i++) {
634      const r = paragraph.getRectsForRange(i, i+1, hStyle, wStyle)[0];
635      // The character is drawn with drawParagraph so we can pass the paraStyle,
636      // and have our character be the exact size and shape the paragraph expected
637      // when it wrapped the text. canvas.drawText wouldn't cut it.
638      const tmpbuilder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
639      tmpbuilder.addText(text[i]);
640      const para = tmpbuilder.build();
641      para.layout(100);
642      letters[i] = {
643        'r': r,
644        'para': para,
645      };
646    }
647
648    function drawFrame(canvas) {
649      // persistence of vision effect is done by drawing the past frame as an image,
650      // then covering with semitransparent background color.
651      if (lastImage) {
652        canvas.drawImage(lastImage, 0, 0, null);
653        canvas.drawColor(CanvasKit.Color(171, 244, 255, 0.1)); // sky blue, almost transparent
654      } else {
655        canvas.clear(CanvasKit.Color(171, 244, 255)); // sky blue, opaque
656      }
657      canvas.save();
658      // Set up 3D view enviroment
659      canvas.concat(CanvasKit.M44.setupCamera(
660        CanvasKit.LTRBRect(0, 0, sizeX, sizeY), halfDim, cam));
661
662      // Rotate the whole paragraph as a unit.
663      const paraRotPoint = [halfDim, halfDim, 1];
664      canvas.concat(CanvasKit.M44.multiply(
665        CanvasKit.M44.translated(paraRotPoint),
666        CanvasKit.M44.rotated([0,1,0], rotX),
667        CanvasKit.M44.rotated([1,0,0], rotY * 0.2),
668        CanvasKit.M44.translated(CanvasKit.Vector.mulScalar(paraRotPoint, -1)),
669      ));
670
671      // Rotate every glyph in the paragraph individually.
672      let i = 0;
673      for (const letter of letters) {
674        canvas.save();
675        let r = letter['r'];
676        // rotate about the center of the glyph's rect.
677        rotationPoint = [
678          margin + r[rectLeft] + (r[rectRight] - r[rectLeft]) / 2,
679          marginTop + r[rectTop] + (r[rectBottom] - r[rectTop]) / 2,
680          0
681        ];
682        distanceFromPointer = CanvasKit.Vector.dist(pointer, rotationPoint.slice(0, 2));
683        // Rotate more around the Y-axis depending on the glyph's distance from the pointer.
684        canvas.concat(CanvasKit.M44.multiply(
685          CanvasKit.M44.translated(rotationPoint),
686          // note that I'm rotating around the x axis first, undoing some of the rotation done to the whole
687          // paragraph above, where x came second. If I rotated y first, a lot of letters would end up
688          // upside down, which is a bit too hard to unscramble.
689          CanvasKit.M44.rotated([1,0,0], rotY * -0.6),
690          CanvasKit.M44.rotated([0,1,0], distanceFromPointer * -0.035),
691          CanvasKit.M44.translated(CanvasKit.Vector.mulScalar(rotationPoint, -1)),
692        ));
693        canvas.drawParagraph(letter['para'], margin + r[rectLeft], marginTop + r[rectTop]);
694        i++;
695        canvas.restore();
696      }
697      canvas.restore();
698      lastImage = surface.makeImageSnapshot();
699    }
700
701    function interact(e) {
702      pointer = [e.offsetX, e.offsetY]
703      rotX = (pointer[0] - halfDim) * radPerPixel;
704      rotY = (pointer[1] - halfDim) * radPerPixel * -1;
705      surface.requestAnimationFrame(drawFrame);
706    };
707
708    document.getElementById('glyphgame').addEventListener('pointermove', interact);
709    surface.requestAnimationFrame(drawFrame);
710  }
711
712  function ColorSupport(CanvasKit) {
713    const surface = CanvasKit.MakeCanvasSurface('colorsupport', CanvasKit.ColorSpace.ADOBE_RGB);
714    if (!surface) {
715      console.error('Could not make surface');
716      return;
717    }
718    const canvas = surface.getCanvas();
719
720    // If the surface is correctly initialized with a higher bit depth color type,
721    // And chrome is compositing it into a buffer with the P3 color space,
722    // then the inner round rect should be distinct and less saturated than the full red background.
723    // Even if the monitor it is viewed on cannot accurately represent that color space.
724
725    let red = CanvasKit.Color4f(1, 0, 0, 1);
726    let paint = new CanvasKit.Paint();
727    paint.setColor(red, CanvasKit.ColorSpace.ADOBE_RGB);
728    canvas.drawPaint(paint);
729    paint.setColor(red, CanvasKit.ColorSpace.DISPLAY_P3);
730    canvas.drawRRect(CanvasKit.RRectXY([50, 50, 250, 250], 30, 30), paint);
731    paint.setColor(red, CanvasKit.ColorSpace.SRGB);
732    canvas.drawRRect(CanvasKit.RRectXY([100, 100, 200, 200], 30, 30), paint);
733
734    surface.flush();
735    surface.delete();
736  }
737</script>
738