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