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