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