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