• 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="/node_modules/canvaskit/bin/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) => '/node_modules/canvaskit/bin/'+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 loadSkp = fetch(cdn + 'picture2.skp').then((response) => response.arrayBuffer());
66  const loadFont = fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer());
67  const loadDog = fetch(cdn + 'dog.jpg').then((response) => response.arrayBuffer());
68  const loadMandrill = fetch(cdn + 'mandrill_256.png').then((response) => response.arrayBuffer());
69  const loadBrickTex = fetch(cdn + 'brickwork-texture.jpg').then((response) => response.arrayBuffer());
70  const loadBrickBump = fetch(cdn + 'brickwork_normal-map.jpg').then((response) => response.arrayBuffer());
71
72  const curves = {
73    "MaxCount": 1000,
74    "Drawable": {
75      "Type": "SkCircleDrawable",
76      "Radius": 2
77    },
78    "Code": [`
79      void effectSpawn(inout Effect effect) {
80        effect.rate = 200;
81        effect.color = float4(1, 0, 0, 1);
82      }
83      void spawn(inout Particle p) {
84        p.lifetime = 3 + rand(p.seed);
85        p.vel.y = -50;
86      }
87
88      void update(inout Particle p) {
89        float w = mix(15, 3, p.age);
90        p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand(p.seed));
91        if (rand(p.seed) < 0.5) { p.pos.x = -p.pos.x; }
92
93        p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand(p.seed))) / 255;
94      }
95      `
96    ],
97    "Bindings": []
98  };
99
100  const spiralSkSL = `
101  uniform float rad_scale;
102  uniform float2 in_center;
103  uniform float4 in_colors0;
104  uniform float4 in_colors1;
105
106  half4 main(float2 p) {
107      float2 pp = p - in_center;
108      float radius = sqrt(dot(pp, pp));
109      radius = sqrt(radius);
110      float angle = atan(pp.y / pp.x);
111      float t = (angle + 3.1415926/2) / (3.1415926);
112      t += radius * rad_scale;
113      t = fract(t);
114      return half4(mix(in_colors0, in_colors1, t));
115  }`;
116
117  // Examples which only require canvaskit
118  ckLoaded.then((CK) => {
119    CanvasKit = CK;
120    ParticlesAPI1(CanvasKit);
121    RTShaderAPI1(CanvasKit);
122    ColorSupport(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  Promise.all([ckLoaded, loadSkp]).then((results) => {SkpExample(...results)});
151
152  const rectLeft = 0;
153  const rectTop = 1;
154  const rectRight = 2;
155  const rectBottom = 3;
156
157  function SkottieExample(CanvasKit, id, jsonStr, bounds, assets) {
158    if (!CanvasKit || !jsonStr) {
159      return;
160    }
161    const animation = CanvasKit.MakeManagedAnimation(jsonStr, assets);
162    const duration = animation.duration() * 1000;
163    const size = animation.size();
164    let c = document.getElementById(id);
165    bounds = bounds || CanvasKit.LTRBRect(0, 0, size.w, size.h);
166
167    // Basic managed animation test.
168    if (id === 'sk_drinks') {
169      animation.setColor('BACKGROUND_FILL', CanvasKit.Color(0, 163, 199, 1.0));
170    }
171
172    const surface = CanvasKit.MakeCanvasSurface(id);
173    if (!surface) {
174      console.error('Could not make surface');
175      return;
176    }
177
178    let firstFrame = Date.now();
179
180    function drawFrame(canvas) {
181      let seek = ((Date.now() - firstFrame) / duration) % 1.0;
182      let damage = animation.seek(seek);
183
184      if (damage[rectRight] > damage[rectLeft] && damage[rectBottom] > damage[rectTop]) {
185        canvas.clear(CanvasKit.WHITE);
186        animation.render(canvas, bounds);
187      }
188      surface.requestAnimationFrame(drawFrame);
189    }
190    surface.requestAnimationFrame(drawFrame);
191
192    return surface;
193  }
194
195  function ParticlesAPI1(CanvasKit) {
196    const surface = CanvasKit.MakeCanvasSurface('particles');
197    if (!surface) {
198      console.error('Could not make surface');
199      return;
200    }
201    const context = CanvasKit.currentContext();
202    const canvas = surface.getCanvas();
203    canvas.translate(250, 450);
204
205    const particles = CanvasKit.MakeParticles(JSON.stringify(curves));
206    particles.start(Date.now() / 1000.0, true);
207
208    function drawFrame(canvas) {
209      canvas.clear(CanvasKit.BLACK);
210
211      particles.update(Date.now() / 1000.0);
212      particles.draw(canvas);
213      surface.requestAnimationFrame(drawFrame);
214    }
215    surface.requestAnimationFrame(drawFrame);
216  }
217
218  function ParagraphAPI1(CanvasKit, fontData) {
219    if (!CanvasKit || !fontData) {
220      return;
221    }
222
223    const surface = CanvasKit.MakeCanvasSurface('para1');
224    if (!surface) {
225      console.error('Could not make surface');
226      return;
227    }
228
229    const canvas = surface.getCanvas();
230    const fontMgr = CanvasKit.FontMgr.FromData([fontData]);
231
232    const paraStyle = new CanvasKit.ParagraphStyle({
233        textStyle: {
234            color: CanvasKit.BLACK,
235            fontFamilies: ['Roboto'],
236            fontSize: 50,
237        },
238        textAlign: CanvasKit.TextAlign.Left,
239        maxLines: 5,
240    });
241
242    const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
243    builder.addText('The quick brown fox ate a hamburgerfons and got sick.');
244    const paragraph = builder.build();
245
246    let wrapTo = 0;
247
248    let X = 100;
249    let Y = 100;
250
251    const tf = fontMgr.MakeTypefaceFromData(fontData);
252    const font = new CanvasKit.Font(tf, 50);
253    const fontPaint = new CanvasKit.Paint();
254    fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
255    fontPaint.setAntiAlias(true);
256
257    function drawFrame(canvas) {
258      canvas.clear(CanvasKit.WHITE);
259      wrapTo = 350 + 150 * Math.sin(Date.now() / 2000);
260      paragraph.layout(wrapTo);
261      canvas.drawParagraph(paragraph, 0, 0);
262
263      canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint);
264
265      surface.requestAnimationFrame(drawFrame);
266    }
267    surface.requestAnimationFrame(drawFrame);
268
269    let interact = (e) => {
270      X = e.offsetX*2; // multiply by 2 because the canvas is 300 css pixels wide,
271      Y = e.offsetY*2; // but the canvas itself is 600px wide
272    };
273
274    document.getElementById('para1').addEventListener('pointermove', interact);
275    return surface;
276  }
277
278    function ParagraphAPI2(CanvasKit, fontData) {
279      if (!CanvasKit || !fontData) {
280        return;
281      }
282
283      const surface = CanvasKit.MakeCanvasSurface('para2');
284      if (!surface) {
285        console.error('Could not make surface');
286        return;
287      }
288
289      const mouse = MakeMouse();
290      const cursor = MakeCursor(CanvasKit);
291      const canvas = surface.getCanvas();
292
293      const text0 = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
294                    "wet hole full of worms and oozy smells. This was a hobbit-hole and " +
295                    "that means good food, a warm hearth, and all the comforts of home.";
296      const LOC_X = 20,
297            LOC_Y = 20;
298
299      const bgPaint = new CanvasKit.Paint();
300      bgPaint.setColor([0.965, 0.965, 0.965, 1]);
301
302      const editor = MakeEditor(text0, {typeface:null, size:24}, cursor, 400);
303
304      editor.applyStyleToRange({size:100}, 0, 1);
305      editor.applyStyleToRange({italic:true}, 38, 38+6);
306      editor.applyStyleToRange({color:[1,0,0,1]}, 5, 5+4);
307
308      editor.setXY(LOC_X, LOC_Y);
309
310      function drawFrame(canvas) {
311        const lines = editor.getLines();
312
313        canvas.clear(CanvasKit.WHITE);
314
315        if (mouse.isActive()) {
316            const pos = mouse.getPos(-LOC_X, -LOC_Y);
317            const a = lines_pos_to_index(lines, pos[0], pos[1]);
318            const b = lines_pos_to_index(lines, pos[2], pos[3]);
319            if (a == b) {
320                editor.setIndex(a);
321            } else {
322                editor.setIndices(a, b);
323            }
324        }
325
326        canvas.drawRect(editor.bounds(), bgPaint);
327        editor.draw(canvas);
328
329        surface.requestAnimationFrame(drawFrame);
330      }
331      surface.requestAnimationFrame(drawFrame);
332
333      function interact(e) {
334        const type = e.type;
335        if (type === 'pointerup') {
336            mouse.setUp(e.offsetX, e.offsetY);
337        } else if (type === 'pointermove') {
338            mouse.setMove(e.offsetX, e.offsetY);
339        } else if (type === 'pointerdown') {
340            mouse.setDown(e.offsetX, e.offsetY);
341        }
342      };
343
344      function keyhandler(e) {
345          switch (e.key) {
346              case 'ArrowLeft':  editor.moveDX(-1); return;
347              case 'ArrowRight': editor.moveDX(1); return;
348              case 'ArrowUp':
349                e.preventDefault();
350                editor.moveDY(-1);
351                return;
352              case 'ArrowDown':
353                e.preventDefault();
354                editor.moveDY(1);
355                return;
356              case 'Backspace':
357                editor.deleteSelection();
358                return;
359              case 'Shift':
360                return;
361            }
362            if (e.ctrlKey) {
363                switch (e.key) {
364                    case 'r': editor.applyStyleToSelection({color:[1,0,0,1]}); return;
365                    case 'g': editor.applyStyleToSelection({color:[0,0.6,0,1]}); return;
366                    case 'u': editor.applyStyleToSelection({color:[0,0,1,1]}); return;
367                    case 'k': editor.applyStyleToSelection({color:[0,0,0,1]}); return;
368
369                    case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
370                    case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
371
372                    case ']': editor.applyStyleToSelection({size_add:1}); return;
373                    case '[': editor.applyStyleToSelection({size_add:-1}); return;
374                }
375            }
376            if (!e.ctrlKey && !e.metaKey) {
377                e.preventDefault(); // at least needed for 'space'
378                editor.insert(e.key);
379            }
380      }
381
382      document.getElementById('para2').addEventListener('pointermove', interact);
383      document.getElementById('para2').addEventListener('pointerdown', interact);
384      document.getElementById('para2').addEventListener('pointerup', interact);
385      document.getElementById('para2').addEventListener('keydown', keyhandler);
386      return surface;
387    }
388
389  function RTShaderAPI1(CanvasKit) {
390    if (!CanvasKit) {
391      return;
392    }
393
394    const surface = CanvasKit.MakeCanvasSurface('rtshader');
395    if (!surface) {
396      console.error('Could not make surface');
397      return;
398    }
399
400    const canvas = surface.getCanvas();
401
402    const effect = CanvasKit.RuntimeEffect.Make(spiralSkSL);
403    const shader = effect.makeShader([
404      0.5,
405      150, 150,
406      0, 1, 0, 1,
407      1, 0, 0, 1], true);
408    const paint = new CanvasKit.Paint();
409    paint.setShader(shader);
410    canvas.drawRect(CanvasKit.LTRBRect(0, 0, 300, 300), paint);
411
412    surface.flush();
413    shader.delete();
414    paint.delete();
415    effect.delete();
416  }
417
418  // RTShader2 demo
419  Promise.all([ckLoaded, loadDog, loadMandrill]).then((values) => {
420    const [CanvasKit, dogData, mandrillData] = values;
421    const dogImg = CanvasKit.MakeImageFromEncoded(dogData);
422    if (!dogImg) {
423      console.error('could not decode dog');
424      return;
425    }
426    const mandrillImg = CanvasKit.MakeImageFromEncoded(mandrillData);
427    if (!mandrillImg) {
428      console.error('could not decode mandrill');
429      return;
430    }
431    const quadrantSize = 150;
432
433    const dogShader = dogImg.makeShaderCubic(
434        CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
435        1/3, 1/3,
436        CanvasKit.Matrix.scaled(quadrantSize/dogImg.width(),
437        quadrantSize/dogImg.height()));
438    const mandrillShader = mandrillImg.makeShaderCubic(
439        CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
440        1/3, 1/3,
441        CanvasKit.Matrix.scaled(
442            quadrantSize/mandrillImg.width(),
443            quadrantSize/mandrillImg.height()));
444
445    const surface = CanvasKit.MakeCanvasSurface('rtshader2');
446    if (!surface) {
447      console.error('Could not make surface');
448      return;
449    }
450
451    const prog = `
452      uniform shader before_map;
453      uniform shader after_map;
454      uniform shader threshold_map;
455
456      uniform float cutoff;
457      uniform float slope;
458
459      float smooth_cutoff(float x) {
460          x = x * slope + (0.5 - slope * cutoff);
461          return clamp(x, 0, 1);
462      }
463
464      half4 main(float2 xy) {
465          half4 before = sample(before_map, xy);
466          half4 after = sample(after_map, xy);
467
468          float m = smooth_cutoff(sample(threshold_map, xy).r);
469          return mix(before, after, half(m));
470      }`;
471
472    const canvas = surface.getCanvas();
473
474    const thresholdEffect = CanvasKit.RuntimeEffect.Make(prog);
475    const spiralEffect = CanvasKit.RuntimeEffect.Make(spiralSkSL);
476
477    const draw = (x, y, shader) => {
478      const paint = new CanvasKit.Paint();
479      paint.setShader(shader);
480      canvas.save();
481      canvas.translate(x, y);
482      canvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint);
483      canvas.restore();
484      paint.delete();
485    };
486
487    const offscreenSurface = CanvasKit.MakeSurface(quadrantSize, quadrantSize);
488    const getBlurrySpiralShader = (rad_scale) => {
489      const oCanvas = offscreenSurface.getCanvas();
490
491      const spiralShader = spiralEffect.makeShader([
492      rad_scale,
493      quadrantSize/2, quadrantSize/2,
494      1, 1, 1, 1,
495      0, 0, 0, 1], true);
496
497      return spiralShader;
498      // TODO(kjlubick): The raster backend does not like atan or fract, so we can't
499      // draw the shader into the offscreen canvas and mess with it. When we can, that
500      // would be cool to show off.
501
502      const blur = CanvasKit.ImageFilter.MakeBlur(0.1, 0.1, CanvasKit.TileMode.Clamp, null);
503
504      const paint = new CanvasKit.Paint();
505      paint.setShader(spiralShader);
506      paint.setImageFilter(blur);
507      oCanvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint);
508
509      paint.delete();
510      blur.delete();
511      spiralShader.delete();
512      return offscreenSurface.makeImageSnapshot()
513                             .makeShader(CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp);
514
515    };
516
517    const drawFrame = () => {
518      surface.requestAnimationFrame(drawFrame);
519      const thresholdShader = getBlurrySpiralShader(Math.sin(Date.now() / 5000) / 2);
520
521      const blendShader = thresholdEffect.makeShaderWithChildren(
522        [0.5, 10],
523        true, [dogShader, mandrillShader, thresholdShader]);
524      draw(0, 0, blendShader);
525      draw(quadrantSize, 0, thresholdShader);
526      draw(0, quadrantSize, dogShader);
527      draw(quadrantSize, quadrantSize, mandrillShader);
528
529      blendShader.delete();
530    };
531
532    surface.requestAnimationFrame(drawFrame);
533  });
534
535  function SkpExample(CanvasKit, skpData) {
536    if (!skpData || !CanvasKit) {
537      return;
538    }
539
540    const surface = CanvasKit.MakeSWCanvasSurface('skp');
541    if (!surface) {
542      console.error('Could not make surface');
543      return;
544    }
545
546    const pic = CanvasKit.MakePicture(skpData);
547
548    function drawFrame(canvas) {
549      canvas.clear(CanvasKit.TRANSPARENT);
550      // this particular file has a path drawing at (68,582) that's 1300x1300 pixels
551      // scale it down to 500x500 and translate it to fit.
552      const scale = 500.0/1300;
553      canvas.scale(scale, scale);
554      canvas.translate(-68, -582);
555      canvas.drawPicture(pic);
556    }
557    // Intentionally just draw frame once
558    surface.drawOnce(drawFrame);
559  }
560
561  // Shows a hidden message by rotating all the characters in a kind of way that makes you
562  // search with your mouse.
563  function GlyphGame(canvas, robotoData) {
564    const surface = CanvasKit.MakeCanvasSurface('glyphgame');
565    if (!surface) {
566      console.error('Could not make surface');
567      return;
568    }
569    const sizeX = document.getElementById('glyphgame').width;
570    const sizeY = document.getElementById('glyphgame').height;
571    const halfDim = Math.min(sizeX, sizeY) / 2;
572    const margin = 50;
573    const marginTop = 25;
574    let rotX = 0; //  expected to be updated in interact()
575    let rotY = 0;
576    let pointer = [500, 450];
577    const radPerPixel = 0.005; // radians of subject rotation per pixel distance moved by mouse.
578
579    const camAngle = Math.PI / 12;
580    const cam = {
581      'eye'  : [0, 0, 1 / Math.tan(camAngle/2) - 1],
582      'coa'  : [0, 0, 0],
583      'up'   : [0, 1, 0],
584      'near' : 0.02,
585      'far'  : 4,
586      'angle': camAngle,
587    };
588
589    let lastImage = null;
590
591    const fontMgr = CanvasKit.FontMgr.FromData([robotoData]);
592
593    const paraStyle = new CanvasKit.ParagraphStyle({
594        textStyle: {
595            color: CanvasKit.Color(105, 56, 16), // brown
596            fontFamilies: ['Roboto'],
597            fontSize: 28,
598        },
599        textAlign: CanvasKit.TextAlign.Left,
600    });
601    const hStyle = CanvasKit.RectHeightStyle.Max;
602    const wStyle = CanvasKit.RectWidthStyle.Tight;
603
604    const quotes = [
605      '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.',
606      // Charles Stross - Accelerando
607      '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?',
608      // Aleksandr Solzhenitsyn - The Gulag Archipelago
609      '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.',
610      // G. K. Chesterton - What's Wrong With The World?
611    ];
612
613    // pick one at random
614    const text = quotes[Math.floor(Math.random()*3)];
615    const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
616    builder.addText(text);
617    const paragraph = builder.build();
618    const font = new CanvasKit.Font(null, 18);
619    // wrap the text to a given width.
620    paragraph.layout(sizeX - margin*2);
621
622    // to rotate every glyph individually, calculate the bounding rect of each one,
623    // construct an array of rects and paragraphs that would draw each glyph individually.
624    const letters = Array(text.length);
625    for (let i = 0; i < text.length; i++) {
626      const r = paragraph.getRectsForRange(i, i+1, hStyle, wStyle)[0];
627      // The character is drawn with drawParagraph so we can pass the paraStyle,
628      // and have our character be the exact size and shape the paragraph expected
629      // when it wrapped the text. canvas.drawText wouldn't cut it.
630      const tmpbuilder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
631      tmpbuilder.addText(text[i]);
632      const para = tmpbuilder.build();
633      para.layout(100);
634      letters[i] = {
635        'r': r,
636        'para': para,
637      };
638    }
639
640    function drawFrame(canvas) {
641      // persistence of vision effect is done by drawing the past frame as an image,
642      // then covering with semitransparent background color.
643      if (lastImage) {
644        canvas.drawImage(lastImage, 0, 0, null);
645        canvas.drawColor(CanvasKit.Color(171, 244, 255, 0.1)); // sky blue, almost transparent
646      } else {
647        canvas.clear(CanvasKit.Color(171, 244, 255)); // sky blue, opaque
648      }
649      canvas.save();
650      // Set up 3D view enviroment
651      canvas.concat(CanvasKit.M44.setupCamera(
652        CanvasKit.LTRBRect(0, 0, sizeX, sizeY), halfDim, cam));
653
654      // Rotate the whole paragraph as a unit.
655      const paraRotPoint = [halfDim, halfDim, 1];
656      canvas.concat(CanvasKit.M44.multiply(
657        CanvasKit.M44.translated(paraRotPoint),
658        CanvasKit.M44.rotated([0,1,0], rotX),
659        CanvasKit.M44.rotated([1,0,0], rotY * 0.2),
660        CanvasKit.M44.translated(CanvasKit.Vector.mulScalar(paraRotPoint, -1)),
661      ));
662
663      // Rotate every glyph in the paragraph individually.
664      let i = 0;
665      for (const letter of letters) {
666        canvas.save();
667        let r = letter['r'];
668        // rotate about the center of the glyph's rect.
669        rotationPoint = [
670          margin + r[rectLeft] + (r[rectRight] - r[rectLeft]) / 2,
671          marginTop + r[rectTop] + (r[rectBottom] - r[rectTop]) / 2,
672          0
673        ];
674        distanceFromPointer = CanvasKit.Vector.dist(pointer, rotationPoint.slice(0, 2));
675        // Rotate more around the Y-axis depending on the glyph's distance from the pointer.
676        canvas.concat(CanvasKit.M44.multiply(
677          CanvasKit.M44.translated(rotationPoint),
678          // note that I'm rotating around the x axis first, undoing some of the rotation done to the whole
679          // paragraph above, where x came second. If I rotated y first, a lot of letters would end up
680          // upside down, which is a bit too hard to unscramble.
681          CanvasKit.M44.rotated([1,0,0], rotY * -0.6),
682          CanvasKit.M44.rotated([0,1,0], distanceFromPointer * -0.035),
683          CanvasKit.M44.translated(CanvasKit.Vector.mulScalar(rotationPoint, -1)),
684        ));
685        canvas.drawParagraph(letter['para'], margin + r[rectLeft], marginTop + r[rectTop]);
686        i++;
687        canvas.restore();
688      }
689      canvas.restore();
690      lastImage = surface.makeImageSnapshot();
691    }
692
693    function interact(e) {
694      pointer = [e.offsetX, e.offsetY]
695      rotX = (pointer[0] - halfDim) * radPerPixel;
696      rotY = (pointer[1] - halfDim) * radPerPixel * -1;
697      surface.requestAnimationFrame(drawFrame);
698    };
699
700    document.getElementById('glyphgame').addEventListener('pointermove', interact);
701    surface.requestAnimationFrame(drawFrame);
702  }
703
704  function ColorSupport(CanvasKit) {
705    const surface = CanvasKit.MakeCanvasSurface('colorsupport', CanvasKit.ColorSpace.ADOBE_RGB);
706    if (!surface) {
707      console.error('Could not make surface');
708      return;
709    }
710    const canvas = surface.getCanvas();
711
712    // If the surface is correctly initialized with a higher bit depth color type,
713    // And chrome is compositing it into a buffer with the P3 color space,
714    // then the inner round rect should be distinct and less saturated than the full red background.
715    // Even if the monitor it is viewed on cannot accurately represent that color space.
716
717    let red = CanvasKit.Color4f(1, 0, 0, 1);
718    let paint = new CanvasKit.Paint();
719    paint.setColor(red, CanvasKit.ColorSpace.ADOBE_RGB);
720    canvas.drawPaint(paint);
721    paint.setColor(red, CanvasKit.ColorSpace.DISPLAY_P3);
722    canvas.drawRRect(CanvasKit.RRectXY([50, 50, 250, 250], 30, 30), paint);
723    paint.setColor(red, CanvasKit.ColorSpace.SRGB);
724    canvas.drawRRect(CanvasKit.RRectXY([100, 100, 200, 200], 30, 30), paint);
725
726    surface.flush();
727    surface.delete();
728  }
729</script>
730