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