1--- 2title: 'CanvasKit - Skia + WebAssembly' 3linkTitle: 'CanvasKit - Skia + WebAssembly' 4 5weight: 20 6--- 7 8Skia now offers a WebAssembly build for easy deployment of our graphics APIs on 9the web. 10 11CanvasKit provides a playground for testing new Canvas and SVG platform APIs, 12enabling fast-paced development on the web platform. It can also be used as a 13deployment mechanism for custom web apps requiring cutting-edge features, like 14Skia's [Lottie animation](https://skia.org/docs/user/modules/skottie) support. 15 16## Features 17 18- WebGL context encapsulated as an SkSurface, allowing for direct drawing to an 19 HTML canvas 20- Core set of Skia canvas/paint/path/text APIs available, see bindings 21- Draws to a hardware-accelerated backend 22- Security tested with Skia's fuzzers 23 24## Samples 25 26<style> 27 #demo canvas { 28 border: 1px dashed #AAA; 29 margin: 2px; 30 } 31 32 #patheffect, #ink, #shaping, #shader1, #camera3d { 33 width: 400px; 34 height: 400px; 35 } 36 37 #sk_legos, #sk_drinks, #sk_party, #sk_onboarding { 38 width: 300px; 39 height: 300px; 40 } 41 42 figure { 43 display: inline-block; 44 margin: 0; 45 } 46 47 figcaption > a { 48 margin: 2px 10px; 49 } 50 51</style> 52 53<div id=demo> 54 <h3>Paragraph shaping, custom shaders, and perspective transformation</h3> 55 <figure> 56 <canvas id=shaping width=500 height=500></canvas> 57 <figcaption> 58 <a href="https://jsfiddle.skia.org/canvaskit/6a5c211a8cb4a7752297674b3533f7e1bbc2a78dd37f117c29a77bcc68411f31" 59 target=_blank rel=noopener> 60 SkParagraph JSFiddle</a> 61 </figcaption> 62 </figure> 63 <figure> 64 <canvas id=shader1 width=512 height=512></canvas> 65 <figcaption> 66 <a href="https://jsfiddle.skia.org/canvaskit/ac0574825f9e517f2dfa8e822126ee75b005e8156c3de4a95d4ffd17ab6ca57b" 67 target=_blank rel=noopener> 68 Shader JSFiddle</a> 69 </figcaption> 70 </figure> 71 <figure> 72 <canvas id=camera3d width=400 height=400></canvas> 73 <figcaption> 74 <a href="https://jsfiddle.skia.org/canvaskit/289946b783390c3242cb5cc117d7bcaf2bcb610bf3d1e67a1dd9c46c1e66b968" 75 target=_blank rel=noopener> 76 3D Cube JSFiddle</a> 77 </figcaption> 78 </figure> 79 80 <h3>Play back bodymovin lottie files with skottie (click for fiddles)</h3> 81 <a href="https://jsfiddle.skia.org/canvaskit/cb0b72eadb45f7e75b4015db7251e9da2cc202a2ce1cbec8eb2e453d83a619a6" 82 target=_blank rel=noopener> 83 <canvas id=sk_legos width=300 height=300></canvas> 84 </a> 85 <a href="https://jsfiddle.skia.org/canvaskit/e77274c30d63645d3bb82fd366991e27c1e1c3df39def04e999b4fcce9f425a2" 86 target=_blank rel=noopener> 87 <canvas id=sk_drinks width=500 height=500></canvas> 88 </a> 89 <a href="https://jsfiddle.skia.org/canvaskit/e42700132d80efd3470b0f08334556028490ac08d1938210fa618504c6109c99" 90 target=_blank rel=noopener> 91 <canvas id=sk_party width=500 height=500></canvas> 92 </a> 93 <a href="https://jsfiddle.skia.org/canvaskit/987b1f99f4703f9f44dbfb2f43a5ed107672334f68d6262cd53ba44ed7a09236" 94 target=_blank rel=noopener> 95 <canvas id=sk_onboarding width=500 height=500></canvas> 96 </a> 97 98 <h3>Go beyond the HTML Canvas2D</h3> 99 <figure> 100 <canvas id=patheffect width=400 height=400></canvas> 101 <figcaption> 102 <a href="https://jsfiddle.skia.org/canvaskit/3588b3b0a7cc93f36d9fa4f08b397c38971dcb1f80a36107f9ad93c051f2cb28" 103 target=_blank rel=noopener> 104 Star JSFiddle</a> 105 </figcaption> 106 </figure> 107 <figure> 108 <canvas id=ink width=400 height=400></canvas> 109 <figcaption> 110 <a href="https://jsfiddle.skia.org/canvaskit/bd42c174a0dcb2f65ff1f3c803397df14014d1e66b92185e9980dc631a49f258" 111 target=_blank rel=noopener> 112 Ink JSFiddle</a> 113 </figcaption> 114 </figure> 115 116</div> 117 118<script type="text/javascript" charset="utf-8"> 119(function() { 120 // Tries to load the WASM version if supported, shows error otherwise 121 let s = document.createElement('script'); 122 let locate_file = ''; 123 if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') { 124 console.log('WebAssembly is supported!'); 125 locate_file = 'https://unpkg.com/[email protected]/bin/full/'; 126 } else { 127 console.log('WebAssembly is not supported (yet) on this browser.'); 128 document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>"; 129 return; 130 } 131 s.src = locate_file + 'canvaskit.js'; 132 s.onload = () => { 133 let CanvasKit = null; 134 let legoJSON = null; 135 let drinksJSON = null; 136 let confettiJSON = null; 137 let onboardingJSON = null; 138 let fullBounds = [0, 0, 500, 500]; 139 const ckLoaded = CanvasKitInit({ 140 locateFile: (file) => locate_file + file, 141 }); 142 143 ckLoaded.then((CK) => { 144 CanvasKit = CK; 145 DrawingExample(CanvasKit); 146 InkExample(CanvasKit); 147 ShapingExample(CanvasKit); 148 // Set bounds to fix the 4:3 resolution of the legos 149 SkottieExample(CanvasKit, 'sk_legos', legoJSON, [-183, -100, 483, 400]); 150 // Re-size to fit 151 SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds); 152 SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds); 153 SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds); 154 ShaderExample1(CanvasKit); 155 }); 156 157 fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => { 158 resp.text().then((str) => { 159 legoJSON = str; 160 SkottieExample(CanvasKit, 'sk_legos', legoJSON, [-183, -100, 483, 400]); 161 }); 162 }); 163 164 fetch('https://storage.googleapis.com/skia-cdn/misc/drinks.json').then((resp) => { 165 resp.text().then((str) => { 166 drinksJSON = str; 167 SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds); 168 }); 169 }); 170 171 fetch('https://storage.googleapis.com/skia-cdn/misc/confetti.json').then((resp) => { 172 resp.text().then((str) => { 173 confettiJSON = str; 174 SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds); 175 }); 176 }); 177 178 fetch('https://storage.googleapis.com/skia-cdn/misc/onboarding.json').then((resp) => { 179 resp.text().then((str) => { 180 onboardingJSON = str; 181 SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds); 182 }); 183 }); 184 185 const loadBrickTex = fetch('https://storage.googleapis.com/skia-cdn/misc/brickwork-texture.jpg').then((response) => response.arrayBuffer()); 186 const loadBrickBump = fetch('https://storage.googleapis.com/skia-cdn/misc/brickwork_normal-map.jpg').then((response) => response.arrayBuffer()); 187 Promise.all([ckLoaded, loadBrickTex, loadBrickBump]).then((results) => {Camera3D(...results)}); 188 189 function preventScrolling(canvas) { 190 canvas.addEventListener('touchmove', (e) => { 191 // Prevents touch events in the canvas from scrolling the canvas. 192 e.preventDefault(); 193 e.stopPropagation(); 194 }); 195 } 196 197 function DrawingExample(CanvasKit) { 198 const surface = CanvasKit.MakeCanvasSurface('patheffect'); 199 if (!surface) { 200 console.log('Could not make surface'); 201 } 202 const paint = new CanvasKit.Paint(); 203 204 const textPaint = new CanvasKit.Paint(); 205 textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0)); 206 textPaint.setAntiAlias(true); 207 208 const textFont = new CanvasKit.Font(null, 30); 209 210 let i = 0; 211 212 let X = 200; 213 let Y = 200; 214 215 function drawFrame(canvas) { 216 const path = starPath(CanvasKit, X, Y); 217 const dpe = CanvasKit.PathEffect.MakeDash([15, 5, 5, 10], i/5); 218 i++; 219 220 paint.setPathEffect(dpe); 221 paint.setStyle(CanvasKit.PaintStyle.Stroke); 222 paint.setStrokeWidth(5.0 + -3 * Math.cos(i/30)); 223 paint.setAntiAlias(true); 224 paint.setColor(CanvasKit.Color(66, 129, 164, 1.0)); 225 226 canvas.clear(CanvasKit.Color(255, 255, 255, 1.0)); 227 228 canvas.drawPath(path, paint); 229 canvas.drawText('Try Clicking!', 10, 380, textPaint, textFont); 230 dpe.delete(); 231 path.delete(); 232 surface.requestAnimationFrame(drawFrame); 233 } 234 surface.requestAnimationFrame(drawFrame); 235 236 // Make animation interactive 237 let interact = (e) => { 238 if (!e.buttons) { 239 return; 240 } 241 X = e.offsetX; 242 Y = e.offsetY; 243 }; 244 document.getElementById('patheffect').addEventListener('pointermove', interact); 245 document.getElementById('patheffect').addEventListener('pointerdown', interact); 246 preventScrolling(document.getElementById('patheffect')); 247 248 // A client would need to delete this if it didn't go on forever. 249 // font.delete(); 250 // paint.delete(); 251 } 252 253 function InkExample(CanvasKit) { 254 const surface = CanvasKit.MakeCanvasSurface('ink'); 255 if (!surface) { 256 console.log('Could not make surface'); 257 } 258 let paint = new CanvasKit.Paint(); 259 paint.setAntiAlias(true); 260 paint.setColor(CanvasKit.Color(0, 0, 0, 1.0)); 261 paint.setStyle(CanvasKit.PaintStyle.Stroke); 262 paint.setStrokeWidth(4.0); 263 // This effect smooths out the drawn lines a bit. 264 paint.setPathEffect(CanvasKit.PathEffect.MakeCorner(50)); 265 266 // Draw I N K 267 let path = new CanvasKit.Path(); 268 path.moveTo(80, 30); 269 path.lineTo(80, 80); 270 271 path.moveTo(100, 80); 272 path.lineTo(100, 15); 273 path.lineTo(130, 95); 274 path.lineTo(130, 30); 275 276 path.moveTo(150, 30); 277 path.lineTo(150, 80); 278 path.moveTo(170, 30); 279 path.lineTo(150, 55); 280 path.lineTo(170, 80); 281 282 let paths = [path]; 283 let paints = [paint]; 284 285 function drawFrame(canvas) { 286 canvas.clear(CanvasKit.WHITE); 287 for (let i = 0; i < paints.length && i < paths.length; i++) { 288 canvas.drawPath(paths[i], paints[i]); 289 } 290 surface.requestAnimationFrame(drawFrame); 291 } 292 293 let hold = false; 294 let interact = (e) => { 295 let type = e.type; 296 if (type === 'lostpointercapture' || type === 'pointerup' || !e.pressure ) { 297 hold = false; 298 return; 299 } 300 if (hold) { 301 path.lineTo(e.offsetX, e.offsetY); 302 } else { 303 paint = paint.copy(); 304 paint.setColor(CanvasKit.Color(Math.random() * 255, Math.random() * 255, Math.random() * 255, Math.random() + .2)); 305 paints.push(paint); 306 path = new CanvasKit.Path(); 307 paths.push(path); 308 path.moveTo(e.offsetX, e.offsetY); 309 } 310 hold = true; 311 }; 312 document.getElementById('ink').addEventListener('pointermove', interact); 313 document.getElementById('ink').addEventListener('pointerdown', interact); 314 document.getElementById('ink').addEventListener('lostpointercapture', interact); 315 document.getElementById('ink').addEventListener('pointerup', interact); 316 preventScrolling(document.getElementById('ink')); 317 surface.requestAnimationFrame(drawFrame); 318 } 319 320 function ShapingExample(CanvasKit) { 321 const surface = CanvasKit.MakeCanvasSurface('shaping'); 322 if (!surface) { 323 console.log('Could not make surface'); 324 return; 325 } 326 let robotoData = null; 327 fetch('https://storage.googleapis.com/skia-cdn/google-web-fonts/Roboto-Regular.ttf').then((resp) => { 328 resp.arrayBuffer().then((buffer) => { 329 robotoData = buffer; 330 }); 331 }); 332 333 let emojiData = null; 334 fetch('https://storage.googleapis.com/skia-cdn/misc/NotoColorEmoji.ttf').then((resp) => { 335 resp.arrayBuffer().then((buffer) => { 336 emojiData = buffer; 337 }); 338 }); 339 340 const font = new CanvasKit.Font(null, 18); 341 const fontPaint = new CanvasKit.Paint(); 342 fontPaint.setStyle(CanvasKit.PaintStyle.Fill); 343 fontPaint.setAntiAlias(true); 344 345 let paragraph = null; 346 let X = 250; 347 let Y = 250; 348 const str = 'The quick brown fox ate a zesty hamburgerfons .\nThe laughed.'; 349 350 function drawFrame(canvas) { 351 surface.requestAnimationFrame(drawFrame); 352 if (robotoData && emojiData && !paragraph) { 353 const fontMgr = CanvasKit.FontMgr.FromData([robotoData, emojiData]); 354 355 const paraStyle = new CanvasKit.ParagraphStyle({ 356 textStyle: { 357 color: CanvasKit.BLACK, 358 fontFamilies: ['Roboto', 'Noto Color Emoji'], 359 fontSize: 50, 360 }, 361 textAlign: CanvasKit.TextAlign.Left, 362 maxLines: 7, 363 ellipsis: '...', 364 }); 365 366 const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); 367 builder.addText(str); 368 paragraph = builder.build(); 369 } 370 if (!paragraph) { 371 canvas.drawText(`Fetching Font data...`, 5, 450, fontPaint, font); 372 return; 373 } 374 canvas.clear(CanvasKit.WHITE); 375 376 let wrapTo = 350 + 150 * Math.sin(Date.now() / 2000); 377 paragraph.layout(wrapTo); 378 canvas.drawParagraph(paragraph, 0, 0); 379 canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint); 380 381 const posA = paragraph.getGlyphPositionAtCoordinate(X, Y); 382 const cp = str.codePointAt(posA.pos); 383 if (cp) { 384 const glyph = String.fromCodePoint(cp); 385 canvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is '${glyph}'`, 5, 450, fontPaint, font); 386 } 387 } 388 389 surface.requestAnimationFrame(drawFrame); 390 // Make animation interactive 391 let interact = (e) => { 392 // multiply by 4/5 to account for the difference in the canvas width and the CSS width. 393 // The 10 accounts for where the mouse actually is compared to where it is drawn. 394 X = (e.offsetX * 4/5) - 10; 395 Y = e.offsetY * 4/5; 396 }; 397 document.getElementById('shaping').addEventListener('pointermove', interact); 398 document.getElementById('shaping').addEventListener('pointerdown', interact); 399 document.getElementById('shaping').addEventListener('lostpointercapture', interact); 400 document.getElementById('shaping').addEventListener('pointerup', interact); 401 preventScrolling(document.getElementById('shaping')); 402 surface.requestAnimationFrame(drawFrame); 403 } 404 405 function starPath(CanvasKit, X=128, Y=128, R=116) { 406 let p = new CanvasKit.Path(); 407 p.moveTo(X + R, Y); 408 for (let i = 1; i < 8; i++) { 409 let a = 2.6927937 * i; 410 p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a)); 411 } 412 return p; 413 } 414 415 function SkottieExample(CanvasKit, id, jsonStr, bounds) { 416 if (!CanvasKit || !jsonStr) { 417 return; 418 } 419 const animation = CanvasKit.MakeAnimation(jsonStr); 420 const duration = animation.duration() * 1000; 421 const size = animation.size(); 422 let c = document.getElementById(id); 423 bounds = bounds || {fLeft: 0, fTop: 0, fRight: size.w, fBottom: size.h}; 424 425 const surface = CanvasKit.MakeCanvasSurface(id); 426 if (!surface) { 427 console.log('Could not make surface'); 428 } 429 let firstFrame = new Date().getTime(); 430 431 function drawFrame(canvas) { 432 let now = new Date().getTime(); 433 let seek = ((now - firstFrame) / duration) % 1.0; 434 435 animation.seek(seek); 436 animation.render(canvas, bounds); 437 438 surface.requestAnimationFrame(drawFrame); 439 } 440 surface.requestAnimationFrame(drawFrame); 441 //animation.delete(); 442 } 443 444 function ShaderExample1(CanvasKit) { 445 if (!CanvasKit) { 446 return; 447 } 448 const surface = CanvasKit.MakeCanvasSurface('shader1'); 449 if (!surface) { 450 throw 'Could not make surface'; 451 } 452 const paint = new CanvasKit.Paint(); 453 454 const prog = ` 455uniform float rad_scale; 456uniform float2 in_center; 457uniform float4 in_colors0; 458uniform float4 in_colors1; 459 460half4 main(float2 p) { 461 float2 pp = p - in_center; 462 float radius = sqrt(dot(pp, pp)); 463 radius = sqrt(radius); 464 float angle = atan(pp.y / pp.x); 465 float t = (angle + 3.1415926/2) / (3.1415926); 466 t += radius * rad_scale; 467 t = fract(t); 468 return half4(mix(in_colors0, in_colors1, t)); 469} 470`; 471 472 const fact = CanvasKit.RuntimeEffect.Make(prog); 473 function drawFrame(canvas) { 474 canvas.clear(CanvasKit.WHITE); 475 const shader = fact.makeShader([ 476 Math.sin(Date.now() / 2000) / 5, 477 256, 256, 478 1, 0, 0, 1, 479 0, 1, 0, 1]); 480 481 paint.setShader(shader); 482 canvas.drawRect(CanvasKit.LTRBRect(0, 0, 512, 512), paint); 483 shader.delete(); 484 surface.requestAnimationFrame(drawFrame); 485 } 486 surface.requestAnimationFrame(drawFrame); 487 } 488 489 function Camera3D(canvas, textureImgData, normalImgData) { 490 const surface = CanvasKit.MakeCanvasSurface('camera3d'); 491 if (!surface) { 492 console.error('Could not make surface'); 493 return; 494 } 495 496 const sizeX = document.getElementById('camera3d').width; 497 const sizeY = document.getElementById('camera3d').height; 498 499 let clickToWorld = CanvasKit.M44.identity(); 500 let worldToClick = CanvasKit.M44.identity(); 501 // rotation of the cube shown in the demo 502 let rotation = CanvasKit.M44.identity(); 503 // temporary during a click and drag 504 let clickRotation = CanvasKit.M44.identity(); 505 506 // A virtual sphere used for tumbling the object on screen. 507 const vSphereCenter = [sizeX/2, sizeY/2]; 508 const vSphereRadius = Math.min(...vSphereCenter); 509 510 // The rounded rect used for each face 511 const margin = vSphereRadius / 20; 512 const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(margin, margin, 513 vSphereRadius - margin, vSphereRadius - margin), margin*2.5, margin*2.5); 514 515 const camAngle = Math.PI / 12; 516 const cam = { 517 'eye' : [0, 0, 1 / Math.tan(camAngle/2) - 1], 518 'coa' : [0, 0, 0], 519 'up' : [0, 1, 0], 520 'near' : 0.05, 521 'far' : 4, 522 'angle': camAngle, 523 }; 524 525 let mouseDown = false; 526 let clickDown = [0, 0]; // location of click down 527 let lastMouse = [0, 0]; // last mouse location 528 529 // keep spinning after mouse up. Also start spinning on load 530 let axis = [0.4, 1, 1]; 531 let totalSpin = 0; 532 let spinRate = 0.1; 533 let lastRadians = 0; 534 let spinning = setInterval(keepSpinning, 30); 535 536 const imgscale = CanvasKit.Matrix.scaled(2, 2); 537 const textureShader = CanvasKit.MakeImageFromEncoded(textureImgData).makeShaderCubic( 538 CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3, imgscale); 539 const normalShader = CanvasKit.MakeImageFromEncoded(normalImgData).makeShaderCubic( 540 CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3, imgscale); 541 const children = [textureShader, normalShader]; 542 543 const prog = ` 544 uniform shader color_map; 545 uniform shader normal_map; 546 547 uniform float3 lightPos; 548 uniform float4x4 localToWorld; 549 uniform float4x4 localToWorldAdjInv; 550 551 float3 convert_normal_sample(half4 c) { 552 float3 n = 2 * c.rgb - 1; 553 n.y = -n.y; 554 return n; 555 } 556 557 half4 main(float2 p) { 558 float3 norm = convert_normal_sample(normal_map.eval(p)); 559 float3 plane_norm = normalize(localToWorldAdjInv * float4(norm, 0)).xyz; 560 561 float3 plane_pos = (localToWorld * float4(p, 0, 1)).xyz; 562 float3 light_dir = normalize(lightPos - plane_pos); 563 564 float ambient = 0.2; 565 float dp = dot(plane_norm, light_dir); 566 float scale = min(ambient + max(dp, 0), 1); 567 568 return color_map.eval(p) * half4(float4(scale, scale, scale, 1)); 569 } 570`; 571 572 const fact = CanvasKit.RuntimeEffect.Make(prog); 573 574 // properties of light 575 let lightLocation = [...vSphereCenter]; 576 let lightDistance = vSphereRadius; 577 let lightIconRadius = 12; 578 let draggingLight = false; 579 580 function computeLightWorldPos() { 581 return CanvasKit.Vector.add(CanvasKit.Vector.mulScalar([...vSphereCenter, 0], 0.5), 582 CanvasKit.Vector.mulScalar(vSphereUnitV3(lightLocation), lightDistance)); 583 } 584 585 let lightWorldPos = computeLightWorldPos(); 586 587 function drawLight(canvas) { 588 const paint = new CanvasKit.Paint(); 589 paint.setAntiAlias(true); 590 paint.setColor(CanvasKit.WHITE); 591 canvas.drawCircle(...lightLocation, lightIconRadius + 2, paint); 592 paint.setColor(CanvasKit.BLACK); 593 canvas.drawCircle(...lightLocation, lightIconRadius, paint); 594 } 595 596 // Takes an x and y rotation in radians and a scale and returns a 4x4 matrix used to draw a 597 // face of the cube in that orientation. 598 function faceM44(rx, ry, scale) { 599 return CanvasKit.M44.multiply( 600 CanvasKit.M44.rotated([0,1,0], ry), 601 CanvasKit.M44.rotated([1,0,0], rx), 602 CanvasKit.M44.translated([0, 0, scale])); 603 } 604 605 const faceScale = vSphereRadius/2 606 const faces = [ 607 {matrix: faceM44( 0, 0, faceScale ), color:CanvasKit.RED}, // front 608 {matrix: faceM44( 0, Math.PI, faceScale ), color:CanvasKit.GREEN}, // back 609 610 {matrix: faceM44( Math.PI/2, 0, faceScale ), color:CanvasKit.BLUE}, // top 611 {matrix: faceM44(-Math.PI/2, 0, faceScale ), color:CanvasKit.CYAN}, // bottom 612 613 {matrix: faceM44( 0, Math.PI/2, faceScale ), color:CanvasKit.MAGENTA}, // left 614 {matrix: faceM44( 0,-Math.PI/2, faceScale ), color:CanvasKit.YELLOW}, // right 615 ]; 616 617 // Returns a component of the matrix m indicating whether it faces the camera. 618 // If it's positive for one of the matrices representing the face of the cube, 619 // that face is currently in front. 620 function front(m) { 621 // Is this invertible? 622 var m2 = CanvasKit.M44.invert(m); 623 if (m2 === null) { 624 m2 = CanvasKit.M44.identity(); 625 } 626 // look at the sign of the z-scale of the inverse of m. 627 // that's the number in row 2, col 2. 628 return m2[10] 629 } 630 631 function setClickToWorld(canvas, matrix) { 632 const l2d = canvas.getLocalToDevice(); 633 worldToClick = CanvasKit.M44.multiply(CanvasKit.M44.mustInvert(matrix), l2d); 634 clickToWorld = CanvasKit.M44.mustInvert(worldToClick); 635 } 636 637 function normalMatrix(m) { 638 m[3] = 0; 639 m[7] = 0; 640 m[11] = 0; 641 m[12] = 0; 642 m[13] = 0; 643 m[14] = 0; 644 m[15] = 1; 645 return CanvasKit.M44.transpose(CanvasKit.M44.mustInvert(m)); 646 } 647 648 function drawCubeFace(canvas, m, color) { 649 const trans = new CanvasKit.M44.translated([vSphereRadius/2, vSphereRadius/2, 0]); 650 const localToWorld = new CanvasKit.M44.multiply(m, CanvasKit.M44.mustInvert(trans)); 651 canvas.concat(CanvasKit.M44.multiply(trans, localToWorld)); 652 const znormal = front(canvas.getLocalToDevice()); 653 if (znormal < 0) { 654 return; // skip faces facing backwards 655 } 656 const uniforms = [...lightWorldPos, ...localToWorld, ...normalMatrix(localToWorld)]; 657 const paint = new CanvasKit.Paint(); 658 paint.setAntiAlias(true); 659 const shader = fact.makeShaderWithChildren(uniforms, children); 660 paint.setShader(shader); 661 canvas.drawRRect(rr, paint); 662 } 663 664 function drawFrame(canvas) { 665 const clickM = canvas.getLocalToDevice(); 666 canvas.save(); 667 canvas.translate(vSphereCenter[0] - vSphereRadius/2, vSphereCenter[1] - vSphereRadius/2); 668 // pass surface dimensions as viewport size. 669 canvas.concat(CanvasKit.M44.setupCamera( 670 CanvasKit.LTRBRect(0, 0, vSphereRadius, vSphereRadius), vSphereRadius/2, cam)); 671 setClickToWorld(canvas, clickM); 672 for (let f of faces) { 673 const saveCount = canvas.getSaveCount(); 674 canvas.save(); 675 drawCubeFace(canvas, CanvasKit.M44.multiply(clickRotation, rotation, f.matrix), f.color); 676 canvas.restoreToCount(saveCount); 677 } 678 canvas.restore(); // camera 679 canvas.restore(); // center the following content in the window 680 681 // draw virtual sphere outline. 682 const paint = new CanvasKit.Paint(); 683 paint.setAntiAlias(true); 684 paint.setStyle(CanvasKit.PaintStyle.Stroke); 685 paint.setColor(CanvasKit.Color(64, 255, 0, 1.0)); 686 canvas.drawCircle(vSphereCenter[0], vSphereCenter[1], vSphereRadius, paint); 687 canvas.drawLine(vSphereCenter[0], vSphereCenter[1] - vSphereRadius, 688 vSphereCenter[0], vSphereCenter[1] + vSphereRadius, paint); 689 canvas.drawLine(vSphereCenter[0] - vSphereRadius, vSphereCenter[1], 690 vSphereCenter[0] + vSphereRadius, vSphereCenter[1], paint); 691 692 drawLight(canvas); 693 } 694 695 // convert a 2D point in the circle displayed on screen to a 3D unit vector. 696 // the virtual sphere is a technique selecting a 3D direction by clicking on a the projection 697 // of a hemisphere. 698 function vSphereUnitV3(p) { 699 // v = (v - fCenter) * (1 / fRadius); 700 let v = CanvasKit.Vector.mulScalar(CanvasKit.Vector.sub(p, vSphereCenter), 1/vSphereRadius); 701 702 // constrain the clicked point within the circle. 703 let len2 = CanvasKit.Vector.lengthSquared(v); 704 if (len2 > 1) { 705 v = CanvasKit.Vector.normalize(v); 706 len2 = 1; 707 } 708 // the closer to the edge of the circle you are, the closer z is to zero. 709 const z = Math.sqrt(1 - len2); 710 v.push(z); 711 return v; 712 } 713 714 function computeVSphereRotation(start, end) { 715 const u = vSphereUnitV3(start); 716 const v = vSphereUnitV3(end); 717 // Axis is in the scope of the Camera3D function so it can be used in keepSpinning. 718 axis = CanvasKit.Vector.cross(u, v); 719 const sinValue = CanvasKit.Vector.length(axis); 720 const cosValue = CanvasKit.Vector.dot(u, v); 721 722 let m = new CanvasKit.M44.identity(); 723 if (Math.abs(sinValue) > 0.000000001) { 724 m = CanvasKit.M44.rotatedUnitSinCos( 725 CanvasKit.Vector.mulScalar(axis, 1/sinValue), sinValue, cosValue); 726 const radians = Math.atan(cosValue / sinValue); 727 spinRate = lastRadians - radians; 728 lastRadians = radians; 729 } 730 return m; 731 } 732 733 function keepSpinning() { 734 totalSpin += spinRate; 735 clickRotation = CanvasKit.M44.rotated(axis, totalSpin); 736 spinRate *= .998; 737 if (spinRate < 0.01) { 738 stopSpinning(); 739 } 740 surface.requestAnimationFrame(drawFrame); 741 } 742 743 function stopSpinning() { 744 clearInterval(spinning); 745 rotation = CanvasKit.M44.multiply(clickRotation, rotation); 746 clickRotation = CanvasKit.M44.identity(); 747 } 748 749 function interact(e) { 750 const type = e.type; 751 let eventPos = [e.offsetX, e.offsetY]; 752 if (type === 'lostpointercapture' || type === 'pointerup' || type == 'pointerleave') { 753 if (draggingLight) { 754 draggingLight = false; 755 } else if (mouseDown) { 756 mouseDown = false; 757 if (spinRate > 0.02) { 758 stopSpinning(); 759 spinning = setInterval(keepSpinning, 30); 760 } 761 } else { 762 return; 763 } 764 return; 765 } else if (type === 'pointermove') { 766 if (draggingLight) { 767 lightLocation = eventPos; 768 lightWorldPos = computeLightWorldPos(); 769 } else if (mouseDown) { 770 lastMouse = eventPos; 771 clickRotation = computeVSphereRotation(clickDown, lastMouse); 772 } else { 773 return; 774 } 775 } else if (type === 'pointerdown') { 776 // Are we repositioning the light? 777 if (CanvasKit.Vector.dist(eventPos, lightLocation) < lightIconRadius) { 778 draggingLight = true; 779 return; 780 } 781 stopSpinning(); 782 mouseDown = true; 783 clickDown = eventPos; 784 lastMouse = eventPos; 785 } 786 surface.requestAnimationFrame(drawFrame); 787 }; 788 789 document.getElementById('camera3d').addEventListener('pointermove', interact); 790 document.getElementById('camera3d').addEventListener('pointerdown', interact); 791 document.getElementById('camera3d').addEventListener('lostpointercapture', interact); 792 document.getElementById('camera3d').addEventListener('pointerleave', interact); 793 document.getElementById('camera3d').addEventListener('pointerup', interact); 794 795 surface.requestAnimationFrame(drawFrame); 796 } 797 798 } 799 document.head.appendChild(s); 800})(); 801</script> 802 803Lottie files courtesy of the lottiefiles.com community: 804[Lego Loader](https://www.lottiefiles.com/410-lego-loader), 805[I'm thirsty](https://www.lottiefiles.com/77-im-thirsty), 806[Confetti](https://www.lottiefiles.com/1370-confetti), 807[Onboarding](https://www.lottiefiles.com/1134-onboarding-1) 808 809## Test server 810 811Test your code on our [CanvasKit Fiddle](https://jsfiddle.skia.org/canvaskit) 812 813## Download 814 815Get [CanvasKit on NPM](https://www.npmjs.com/package/canvaskit-wasm). 816Documentation and Typescript definitions are available in the `types/` subfolder 817of the npm package or from the 818[Skia repo](https://github.com/google/skia/tree/main/modules/canvaskit/npm_build/types). 819 820Check out the [quickstart guide](../quickstart) as well. 821