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