1CanvasKit - Skia + WebAssembly 2============================== 3 4Skia now offers a WebAssembly build for easy deployment of our graphics APIs on 5the web. 6 7CanvasKit provides a playground for testing new Canvas and SVG platform APIs, 8enabling fast-paced development on the web platform. 9It can also be used as a deployment mechanism for custom web apps requiring 10cutting-edge features, like Skia's [Lottie 11animation](https://skia.org/user/modules/skottie) support. 12 13 14Features 15-------- 16 17 - WebGL context encapsulated as an SkSurface, allowing for direct drawing to 18 an HTML canvas 19 - Core set of Skia canvas/paint/path/text APIs available, see bindings 20 - Draws to a hardware-accelerated backend 21 - Security tested with Skia's fuzzers 22 23Samples 24------- 25 26<style> 27 #demo canvas { 28 border: 1px dashed #AAA; 29 margin: 2px; 30 } 31 32 #patheffect, #ink, #shaping { 33 width: 400px; 34 height: 400px; 35 } 36 37 #sk_legos, #sk_drinks, #sk_party, #sk_onboarding, #shader1 { 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>Go beyond the HTML Canvas2D</h3> 55 <figure> 56 <canvas id=patheffect width=400 height=400></canvas> 57 <figcaption> 58 <a href="https://jsfiddle.skia.org/canvaskit/ea89749ae8c90bce807ea2e7e34fb7b09b950cee70d9db0a9cdfd2d67bd48ef0" 59 target=_blank rel=noopener> 60 Star JSFiddle</a> 61 </figcaption> 62 </figure> 63 <figure> 64 <canvas id=ink width=400 height=400></canvas> 65 <figcaption> 66 <a href="https://jsfiddle.skia.org/canvaskit/43475699d6d7d3d7dad1004c29f84015752a6a6dee2bb90f2e891b53e31d45cc" 67 target=_blank rel=noopener> 68 Ink JSFiddle</a> 69 </figcaption> 70 </figure> 71 72 <h3>Skottie (click for fiddles)</h3> 73 <a href="https://jsfiddle.skia.org/canvaskit/092690b273b41076d2f00f0d43d004893d6bb9992c387c0385efa8e6f6bc83d7" 74 target=_blank rel=noopener> 75 <canvas id=sk_legos width=300 height=300></canvas> 76 </a> 77 <a href="https://jsfiddle.skia.org/canvaskit/e7ac983d9859f89aff1b6d385190919202c2eb53d028a79992892cacceffd209" 78 target=_blank rel=noopener> 79 <canvas id=sk_drinks width=500 height=500></canvas> 80 </a> 81 <a href="https://jsfiddle.skia.org/canvaskit/0e06547181759731e7369d3e3613222a0826692f48c41b16504ed68d671583e1" 82 target=_blank rel=noopener> 83 <canvas id=sk_party width=500 height=500></canvas> 84 </a> 85 <a href="https://jsfiddle.skia.org/canvaskit/be3fc1c5c351e7f43cc2840033f80b44feb3475925264808f321bb9e2a21174a" 86 target=_blank rel=noopener> 87 <canvas id=sk_onboarding width=500 height=500></canvas> 88 </a> 89 90 <h3>SkParagraph (using ICU and Harfbuzz)</h3> 91 <figure> 92 <canvas id=shaping width=500 height=500></canvas> 93 <figcaption> 94 <a href="https://jsfiddle.skia.org/canvaskit/56cb197c724dfdfad0c3d8133d4fcab587e4c4e7f31576e62c17251637d3745c" 95 target=_blank rel=noopener> 96 SkParagraph JSFiddle</a> 97 </figcaption> 98 </figure> 99 100 <h3>SKSL for writing custom shaders</h3> 101 <a href="https://jsfiddle.skia.org/canvaskit/33ff9bed883cd5742b4770169da0b36fb0cbc18fd395ddd9563213e178362d30" 102 target=_blank rel=noopener> 103 <canvas id=shader1 width=512 height=512></canvas> 104 </a> 105 106</div> 107 108<script type="text/javascript" charset="utf-8"> 109(function() { 110 // Tries to load the WASM version if supported, shows error otherwise 111 let s = document.createElement('script'); 112 let locate_file = ''; 113 // Hey, if you are looking at this code for an example of how to do it yourself, please use 114 // an actual CDN, such as https://unpkg.com/canvaskit-wasm - it will have better reliability 115 // and niceties like brotli compression. 116 if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') { 117 console.log('WebAssembly is supported!'); 118 locate_file = 'https://particles.skia.org/static/'; 119 } else { 120 console.log('WebAssembly is not supported (yet) on this browser.'); 121 document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>"; 122 return; 123 } 124 s.src = locate_file + 'canvaskit.js'; 125 s.onload = () => { 126 let CanvasKit = null; 127 let legoJSON = null; 128 let drinksJSON = null; 129 let confettiJSON = null; 130 let onboardingJSON = null; 131 let fullBounds = {fLeft: 0, fTop: 0, fRight: 500, fBottom: 500}; 132 CanvasKitInit({ 133 locateFile: (file) => locate_file + file, 134 }).ready().then((CK) => { 135 CanvasKit = CK; 136 DrawingExample(CanvasKit); 137 InkExample(CanvasKit); 138 ShapingExample(CanvasKit); 139 // Set bounds to fix the 4:3 resolution of the legos 140 SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300}); 141 // Re-size to fit 142 SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds); 143 SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds); 144 SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds); 145 ShaderExample1(CanvasKit); 146 }); 147 148 fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => { 149 resp.text().then((str) => { 150 legoJSON = str; 151 SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300}); 152 }); 153 }); 154 155 fetch('https://storage.googleapis.com/skia-cdn/misc/drinks.json').then((resp) => { 156 resp.text().then((str) => { 157 drinksJSON = str; 158 SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds); 159 }); 160 }); 161 162 fetch('https://storage.googleapis.com/skia-cdn/misc/confetti.json').then((resp) => { 163 resp.text().then((str) => { 164 confettiJSON = str; 165 SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds); 166 }); 167 }); 168 169 fetch('https://storage.googleapis.com/skia-cdn/misc/onboarding.json').then((resp) => { 170 resp.text().then((str) => { 171 onboardingJSON = str; 172 SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds); 173 }); 174 }); 175 176 function preventScrolling(canvas) { 177 canvas.addEventListener('touchmove', (e) => { 178 // Prevents touch events in the canvas from scrolling the canvas. 179 e.preventDefault(); 180 e.stopPropagation(); 181 }); 182 } 183 184 function DrawingExample(CanvasKit) { 185 const surface = CanvasKit.MakeCanvasSurface('patheffect'); 186 if (!surface) { 187 console.log('Could not make surface'); 188 } 189 const context = CanvasKit.currentContext(); 190 191 const canvas = surface.getCanvas(); 192 193 const paint = new CanvasKit.SkPaint(); 194 195 const textPaint = new CanvasKit.SkPaint(); 196 textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0)); 197 textPaint.setAntiAlias(true); 198 199 const textFont = new CanvasKit.SkFont(null, 30); 200 201 let i = 0; 202 203 let X = 200; 204 let Y = 200; 205 206 function drawFrame() { 207 const path = starPath(CanvasKit, X, Y); 208 CanvasKit.setCurrentContext(context); 209 const dpe = CanvasKit.MakeSkDashPathEffect([15, 5, 5, 10], i/5); 210 i++; 211 212 paint.setPathEffect(dpe); 213 paint.setStyle(CanvasKit.PaintStyle.Stroke); 214 paint.setStrokeWidth(5.0 + -3 * Math.cos(i/30)); 215 paint.setAntiAlias(true); 216 paint.setColor(CanvasKit.Color(66, 129, 164, 1.0)); 217 218 canvas.clear(CanvasKit.Color(255, 255, 255, 1.0)); 219 220 canvas.drawPath(path, paint); 221 canvas.drawText('Try Clicking!', 10, 380, textPaint, textFont); 222 canvas.flush(); 223 dpe.delete(); 224 path.delete(); 225 window.requestAnimationFrame(drawFrame); 226 } 227 window.requestAnimationFrame(drawFrame); 228 229 // Make animation interactive 230 let interact = (e) => { 231 if (!e.buttons) { 232 return; 233 } 234 X = e.offsetX; 235 Y = e.offsetY; 236 }; 237 document.getElementById('patheffect').addEventListener('pointermove', interact); 238 document.getElementById('patheffect').addEventListener('pointerdown', interact); 239 preventScrolling(document.getElementById('patheffect')); 240 241 // A client would need to delete this if it didn't go on forever. 242 // font.delete(); 243 // paint.delete(); 244 } 245 246 function InkExample(CanvasKit) { 247 const surface = CanvasKit.MakeCanvasSurface('ink'); 248 if (!surface) { 249 console.log('Could not make surface'); 250 } 251 const context = CanvasKit.currentContext(); 252 253 const canvas = surface.getCanvas(); 254 255 let paint = new CanvasKit.SkPaint(); 256 paint.setAntiAlias(true); 257 paint.setColor(CanvasKit.Color(0, 0, 0, 1.0)); 258 paint.setStyle(CanvasKit.PaintStyle.Stroke); 259 paint.setStrokeWidth(4.0); 260 // This effect smooths out the drawn lines a bit. 261 paint.setPathEffect(CanvasKit.MakeSkCornerPathEffect(50)); 262 263 // Draw I N K 264 let path = new CanvasKit.SkPath(); 265 path.moveTo(80, 30); 266 path.lineTo(80, 80); 267 268 path.moveTo(100, 80); 269 path.lineTo(100, 15); 270 path.lineTo(130, 95); 271 path.lineTo(130, 30); 272 273 path.moveTo(150, 30); 274 path.lineTo(150, 80); 275 path.moveTo(170, 30); 276 path.lineTo(150, 55); 277 path.lineTo(170, 80); 278 279 let paths = [path]; 280 let paints = [paint]; 281 282 function drawFrame() { 283 CanvasKit.setCurrentContext(context); 284 285 for (let i = 0; i < paints.length && i < paths.length; i++) { 286 canvas.drawPath(paths[i], paints[i]); 287 } 288 canvas.flush(); 289 290 window.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.SkPath(); 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 window.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 requestAnimationFrame(drawFrame); 331 }); 332 }); 333 334 let emojiData = null; 335 fetch('https://storage.googleapis.com/skia-cdn/misc/NotoColorEmoji.ttf').then((resp) => { 336 resp.arrayBuffer().then((buffer) => { 337 emojiData = buffer; 338 requestAnimationFrame(drawFrame); 339 }); 340 }); 341 342 const skcanvas = surface.getCanvas(); 343 344 const font = new CanvasKit.SkFont(null, 18); 345 const fontPaint = new CanvasKit.SkPaint(); 346 fontPaint.setStyle(CanvasKit.PaintStyle.Fill); 347 fontPaint.setAntiAlias(true); 348 349 skcanvas.drawText(`Fetching Font data...`, 5, 450, fontPaint, font); 350 surface.flush(); 351 352 const context = CanvasKit.currentContext(); 353 354 let paragraph = null; 355 let X = 10; 356 let Y = 10; 357 const str = 'The quick brown fox ate a zesty hamburgerfons .\nThe laughed.'; 358 359 function drawFrame() { 360 if (robotoData && emojiData && !paragraph) { 361 const fontMgr = CanvasKit.SkFontMgr.FromData([robotoData, emojiData]); 362 363 const paraStyle = new CanvasKit.ParagraphStyle({ 364 textStyle: { 365 color: CanvasKit.BLACK, 366 fontFamilies: ['Roboto', 'Noto Color Emoji'], 367 fontSize: 50, 368 }, 369 textAlign: CanvasKit.TextAlign.Left, 370 maxLines: 7, 371 ellipsis: '...', 372 }); 373 374 const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); 375 builder.addText(str); 376 paragraph = builder.build(); 377 } 378 if (!paragraph) { 379 requestAnimationFrame(drawFrame); 380 return; 381 } 382 CanvasKit.setCurrentContext(context); 383 skcanvas.clear(CanvasKit.WHITE); 384 385 const wrapTo = 350 + 150 * Math.sin(Date.now() / 2000); 386 paragraph.layout(wrapTo); 387 skcanvas.drawParagraph(paragraph, 0, 0); 388 skcanvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint); 389 390 let posA = paragraph.getGlyphPositionAtCoordinate(X, Y); 391 const cp = str.codePointAt(posA.pos); 392 if (cp) { 393 const glyph = String.fromCodePoint(cp); 394 skcanvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is '${glyph}'`, 5, 450, fontPaint, font); 395 } 396 397 surface.flush(); 398 requestAnimationFrame(drawFrame); 399 } 400 401 // Make animation interactive 402 let interact = (e) => { 403 // multiply by 4/5 to account for the difference in the canvas width and the CSS width. 404 // The 10 accounts for where the mouse actually is compared to where it is drawn. 405 X = (e.offsetX * 4/5) - 10; 406 Y = e.offsetY * 4/5; 407 }; 408 document.getElementById('shaping').addEventListener('pointermove', interact); 409 document.getElementById('shaping').addEventListener('pointerdown', interact); 410 document.getElementById('shaping').addEventListener('lostpointercapture', interact); 411 document.getElementById('shaping').addEventListener('pointerup', interact); 412 preventScrolling(document.getElementById('shaping')); 413 window.requestAnimationFrame(drawFrame); 414 } 415 416 function starPath(CanvasKit, X=128, Y=128, R=116) { 417 let p = new CanvasKit.SkPath(); 418 p.moveTo(X + R, Y); 419 for (let i = 1; i < 8; i++) { 420 let a = 2.6927937 * i; 421 p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a)); 422 } 423 return p; 424 } 425 426 function SkottieExample(CanvasKit, id, jsonStr, bounds) { 427 if (!CanvasKit || !jsonStr) { 428 return; 429 } 430 const animation = CanvasKit.MakeAnimation(jsonStr); 431 const duration = animation.duration() * 1000; 432 const size = animation.size(); 433 let c = document.getElementById(id); 434 bounds = bounds || {fLeft: 0, fTop: 0, fRight: size.w, fBottom: size.h}; 435 436 const surface = CanvasKit.MakeCanvasSurface(id); 437 if (!surface) { 438 console.log('Could not make surface'); 439 } 440 const context = CanvasKit.currentContext(); 441 const canvas = surface.getCanvas(); 442 443 let firstFrame = new Date().getTime(); 444 445 function drawFrame() { 446 let now = new Date().getTime(); 447 let seek = ((now - firstFrame) / duration) % 1.0; 448 CanvasKit.setCurrentContext(context); 449 animation.seek(seek); 450 451 animation.render(canvas, bounds); 452 canvas.flush(); 453 window.requestAnimationFrame(drawFrame); 454 } 455 window.requestAnimationFrame(drawFrame); 456 //animation.delete(); 457 } 458 459 function ShaderExample1(CanvasKit) { 460 if (!CanvasKit) { 461 return; 462 } 463 const surface = CanvasKit.MakeCanvasSurface('shader1'); 464 if (!surface) { 465 throw 'Could not make surface'; 466 } 467 const skcanvas = surface.getCanvas(); 468 const paint = new CanvasKit.SkPaint(); 469 470 const prog = ` 471uniform float rad_scale; 472uniform float2 in_center; 473uniform float4 in_colors0; 474uniform float4 in_colors1; 475 476void main(float2 p, inout half4 color) { 477 float2 pp = p - in_center; 478 float radius = sqrt(dot(pp, pp)); 479 radius = sqrt(radius); 480 float angle = atan(pp.y / pp.x); 481 float t = (angle + 3.1415926/2) / (3.1415926); 482 t += radius * rad_scale; 483 t = fract(t); 484 color = half4(mix(in_colors0, in_colors1, t)); 485} 486`; 487 488 // If there are multiple contexts on the screen, we need to make sure 489 // we switch to this one before we draw. 490 const context = CanvasKit.currentContext(); 491 const fact = CanvasKit.SkRuntimeEffect.Make(prog); 492 function drawFrame() { 493 CanvasKit.setCurrentContext(context); 494 skcanvas.clear(CanvasKit.WHITE); 495 const shader = fact.makeShader([ 496 Math.sin(Date.now() / 2000) / 5, 497 256, 256, 498 1, 0, 0, 1, 499 0, 1, 0, 1], 500 true/*=opaque*/); 501 502 paint.setShader(shader); 503 skcanvas.drawRect(CanvasKit.LTRBRect(0, 0, 512, 512), paint); 504 surface.flush(); 505 requestAnimationFrame(drawFrame); 506 shader.delete(); 507 } 508 requestAnimationFrame(drawFrame); 509 } 510 511 } 512 document.head.appendChild(s); 513})(); 514</script> 515 516Lottie files courtesy of the lottiefiles.com community: 517[Lego Loader](https://www.lottiefiles.com/410-lego-loader), 518[I'm thirsty](https://www.lottiefiles.com/77-im-thirsty), 519[Confetti](https://www.lottiefiles.com/1370-confetti), 520[Onboarding](https://www.lottiefiles.com/1134-onboarding-1) 521 522Test server 523----------- 524Test your code on our [CanvasKit Fiddle](https://jsfiddle.skia.org/canvaskit) 525 526Download 527-------- 528Get [CanvasKit on NPM](https://www.npmjs.com/package/canvaskit-wasm) 529