Joe Gregorio | 02f7202 | 2021-03-27 10:12:45 -0400 | [diff] [blame] | 1 | |
| 2 | --- |
| 3 | title: "CanvasKit - Quickstart" |
| 4 | linkTitle: "CanvasKit - Quickstart" |
| 5 | |
| 6 | --- |
| 7 | |
| 8 | |
| 9 | CanvasKit is a wasm module that uses Skia to draw to canvas elements a more advance feature set than the canvas API. |
| 10 | |
| 11 | Minimal application |
| 12 | ------------------- |
| 13 | |
| 14 | This example is a minimal Canvaskit application that draws a rounded rect for one frame. |
| 15 | It pulls the wasm binary from unpkg.com but you can also build and host it yourself. |
| 16 | |
| 17 | <!--?prettify?--> |
| 18 | ``` js |
| 19 | <canvas id=foo width=300 height=300></canvas> |
| 20 | |
| 21 | <script type="text/javascript" |
| 22 | src="https://unpkg.com/canvaskit-wasm@0.19.0/bin/canvaskit.js"></script> |
| 23 | <script type="text/javascript"> |
| 24 | const ckLoaded = CanvasKitInit({ |
| 25 | locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/'+file}); |
| 26 | ckLoaded.then((CanvasKit) => { |
| 27 | const surface = CanvasKit.MakeCanvasSurface('foo'); |
| 28 | |
| 29 | const paint = new CanvasKit.Paint(); |
| 30 | paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0)); |
| 31 | paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| 32 | paint.setAntiAlias(true); |
| 33 | const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15); |
| 34 | |
| 35 | function draw(canvas) { |
| 36 | canvas.clear(CanvasKit.WHITE); |
| 37 | canvas.drawRRect(rr, paint); |
| 38 | } |
| 39 | surface.drawOnce(draw); |
| 40 | }); |
| 41 | </script> |
| 42 | ``` |
| 43 | |
| 44 | <canvas id=foo width=300 height=300></canvas> |
| 45 | |
| 46 | <script type="text/javascript" |
| 47 | src="https://unpkg.com/canvaskit-wasm@0.19.0/bin/canvaskit.js"></script> |
| 48 | <script type="text/javascript"> |
| 49 | const ckLoaded = CanvasKitInit({ |
| 50 | locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/'+file}); |
| 51 | ckLoaded.then((CanvasKit) => { |
| 52 | const surface = CanvasKit.MakeCanvasSurface('foo'); |
| 53 | |
| 54 | const paint = new CanvasKit.Paint(); |
| 55 | paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0)); |
| 56 | paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| 57 | paint.setAntiAlias(true); |
| 58 | const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15); |
| 59 | |
| 60 | function draw(canvas) { |
| 61 | canvas.clear(CanvasKit.WHITE); |
| 62 | canvas.drawRRect(rr, paint); |
| 63 | } |
| 64 | surface.drawOnce(draw); |
| 65 | }); |
| 66 | </script> |
| 67 | |
| 68 | Let's break it down into parts and explain what they are doing: |
| 69 | |
| 70 | `<canvas id=foo width=300 height=300></canvas>` Creates the canvas to which CanvasKit will draw. |
| 71 | This element is where we control the width and height of the drawing buffer, while it's css style |
| 72 | would control any scaling applied after drawing to those pixels. Despite using a canvas element, |
| 73 | CanvasKit isn't calling the HTML canvas's own draw methods. It is using this canvas element to |
| 74 | get a WebGL2 context and performing most of the drawing work in C++ code compiled to WebAssembly, |
| 75 | then sending commands to the GPU at the end of each frame. |
| 76 | |
| 77 | <!--?prettify?--> |
| 78 | ``` html |
| 79 | <script type="text/javascript" |
| 80 | src="https://unpkg.com/canvaskit-wasm@0.19.0/bin/canvaskit.js"></script> |
| 81 | ``` |
| 82 | and |
| 83 | |
| 84 | <!--?prettify?--> |
| 85 | ``` js |
| 86 | const ckLoaded = CanvasKitInit({ |
| 87 | locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/'+file}); |
| 88 | ckLoaded.then((CanvasKit) => { |
| 89 | ``` |
| 90 | are loading the canvaskit helper js and wasm binary respectively. CanvasKitInit accepts a function |
| 91 | for allowing you to alter the path where it will try to find `canvaskit.wasm` and returns a promise |
| 92 | that resolves with the loaded module, which we typically name `CanvasKit`. |
| 93 | |
| 94 | <!--?prettify?--> |
| 95 | ``` js |
| 96 | const surface = CanvasKit.MakeCanvasSurface('foo'); |
| 97 | ``` |
| 98 | Creates a Surface associated with the HTML canvas element above. |
| 99 | Hardware acceleration is the default behavior, but can be overridden by calling |
| 100 | `MakeSWCanvasSurface` instead. `MakeCanvasSurface` is also where alternative color spaces or gl |
| 101 | attrtributes can be specified. |
| 102 | |
| 103 | <!--?prettify?--> |
| 104 | ``` js |
| 105 | const paint = new CanvasKit.Paint(); |
| 106 | paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0)); |
| 107 | paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| 108 | paint.setAntiAlias(true); |
| 109 | const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15); |
| 110 | ``` |
| 111 | Creates a paint, a description of how to fill or stroke rects, paths, text and other geometry in |
| 112 | canvaskit. `rr` is a rounded rect, with corners having a radius of 25 in the x axis, and 15 pixels |
| 113 | in the y axis. |
| 114 | |
| 115 | <!--?prettify?--> |
| 116 | ``` js |
| 117 | function draw(canvas) { |
| 118 | canvas.clear(CanvasKit.WHITE); |
| 119 | canvas.drawRRect(rr, paint); |
| 120 | } |
| 121 | ``` |
| 122 | Defines a function that will draw our frame. The function is provided a Canvas object on which we |
| 123 | make draw calls. One to clear the entire canvas, and one to draw the rounded rect with the |
| 124 | paint from above. |
| 125 | |
| 126 | We also delete the paint object. CanvasKit objects created with `new` or methods prefixed with |
| 127 | `make` must be deleted for the wasm memory to be released. Javascript's GC will not take care of |
| 128 | it automatically. `rr` is just an array, wasn't created with `new` and doesn't point to any WASM |
| 129 | memory, so we don't have to call delete on it. |
| 130 | |
| 131 | <!--?prettify?--> |
| 132 | ``` js |
| 133 | surface.drawOnce(draw); |
| 134 | paint.delete() |
| 135 | ``` |
| 136 | Hand the drawing function to `surface.drawOnce` which makes the calls and flushes the surface. |
| 137 | Upon flushing, Skia will batch and send WebGL commands, making visible changes appear onscreen. |
| 138 | This example draws once and disposes of the surface. As promised, it is is a minimal |
| 139 | application. |
| 140 | |
| 141 | Basic Draw Loop |
| 142 | --------------- |
| 143 | |
| 144 | What if we need to redraw to our canvas every frame? This example |
| 145 | bounces a rounded rect around like a 90s screensaver. |
| 146 | |
| 147 | <!--?prettify?--> |
| 148 | ``` js |
| 149 | ckLoaded.then((CanvasKit) => { |
| 150 | const surface = CanvasKit.MakeCanvasSurface('foo2'); |
| 151 | |
| 152 | const paint = new CanvasKit.Paint(); |
| 153 | paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0)); |
| 154 | paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| 155 | paint.setAntiAlias(true); |
| 156 | // const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15); |
| 157 | const w = 100; // size of rect |
| 158 | const h = 60; |
| 159 | let x = 10; // initial position of top left corner. |
| 160 | let y = 60; |
| 161 | let dirX = 1; // box is always moving at a constant speed in one of the four diagonal directions |
| 162 | let dirY = 1; |
| 163 | |
| 164 | function drawFrame(canvas) { |
| 165 | // boundary check |
| 166 | if (x < 0 || x+w > 300) { |
| 167 | dirX *= -1; // reverse x direction when hitting side walls |
| 168 | } |
| 169 | if (y < 0 || y+h > 300) { |
| 170 | dirY *= -1; // reverse y direction when hitting top and bottom walls |
| 171 | } |
| 172 | // move |
| 173 | x += dirX; |
| 174 | y += dirY; |
| 175 | |
| 176 | canvas.clear(CanvasKit.WHITE); |
| 177 | const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(x, y, x+w, y+h), 25, 15); |
| 178 | canvas.drawRRect(rr, paint); |
| 179 | surface.requestAnimationFrame(drawFrame); |
| 180 | } |
| 181 | surface.requestAnimationFrame(drawFrame); |
| 182 | }); |
| 183 | ``` |
| 184 | |
| 185 | <canvas id=foo2 width=300 height=300></canvas> |
| 186 | |
| 187 | <script type="text/javascript"> |
| 188 | ckLoaded.then((CanvasKit) => { |
| 189 | const surface = CanvasKit.MakeCanvasSurface('foo2'); |
| 190 | |
| 191 | const paint = new CanvasKit.Paint(); |
| 192 | paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0)); |
| 193 | paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| 194 | paint.setAntiAlias(true); |
| 195 | // const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15); |
| 196 | const w = 100; // size of rect |
| 197 | const h = 60; |
| 198 | let x = 10; // initial position of top left corner. |
| 199 | let y = 60; |
| 200 | // The box is always moving at a constant speed in one of the four diagonal directions |
| 201 | let dirX = 1; |
| 202 | let dirY = 1; |
| 203 | |
| 204 | function drawFrame(canvas) { |
| 205 | // boundary check |
| 206 | if (x < 0 || x+w > 300) { |
| 207 | dirX *= -1; // reverse x direction when hitting side walls |
| 208 | } |
| 209 | if (y < 0 || y+h > 300) { |
| 210 | dirY *= -1; // reverse y direction when hitting top and bottom walls |
| 211 | } |
| 212 | // move |
| 213 | x += dirX; |
| 214 | y += dirY; |
| 215 | |
| 216 | canvas.clear(CanvasKit.WHITE); |
| 217 | const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(x, y, x+w, y+h), 25, 15); |
| 218 | canvas.drawRRect(rr, paint); |
| 219 | surface.requestAnimationFrame(drawFrame); |
| 220 | } |
| 221 | surface.requestAnimationFrame(drawFrame); |
| 222 | }); |
| 223 | </script> |
| 224 | |
| 225 | The main difference here is that we define a function to be called before each frame is drawn and |
| 226 | pass it to `surface.requestAnimationFrame(drawFrame);` That callback is handed a `canvas` and |
| 227 | flushing is taken care of. |
| 228 | |
| 229 | <!--?prettify?--> |
| 230 | ``` js |
| 231 | function drawFrame(canvas) { |
| 232 | canvas.clear(CanvasKit.WHITE); |
| 233 | // code to update and draw the frame goes here |
| 234 | surface.requestAnimationFrame(drawFrame); |
| 235 | } |
| 236 | surface.requestAnimationFrame(drawFrame); |
| 237 | ``` |
| 238 | |
| 239 | Creates a function to serve as our main drawing loop. Each time a frame is about to be rendered |
| 240 | (the browser will typically target 60fps), our function is called, we clear the canvas with white, |
| 241 | redraw the round rect, and call `surface.requestAnimationFrame(drawFrame)` registering the |
| 242 | function to be called again before the next frame. |
| 243 | |
| 244 | `surface.requestAnimationFrame(drawFrame)` combines window.requestAnimationFrame with |
| 245 | `surface.flush()` and should be used in all the same ways. If your application would only make |
| 246 | visible changes as a result of mouse events, |
| 247 | don't call `surface.requestAnimationFrame` at the end of your drawFrame function. Call it only |
| 248 | after handling mouse input. |
| 249 | |
| 250 | Text Shaping |
| 251 | ------------ |
| 252 | |
| 253 | One of the biggest features that CanvasKit offers over the HTML Canvas API is paragraph shaping. |
| 254 | To use text your applicatoin, supply a font file and use Promise.all to run your code when both |
| 255 | CanvasKit and the font file are ready. |
| 256 | |
| 257 | <!--?prettify?--> |
| 258 | ``` js |
| 259 | const loadFont = fetch('https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf') |
| 260 | .then((response) => response.arrayBuffer()); |
| 261 | |
| 262 | Promise.all([ckLoaded, loadFont]).then(([CanvasKit, robotoData]) => { |
| 263 | const surface = CanvasKit.MakeCanvasSurface('foo3'); |
| 264 | const canvas = surface.getCanvas(); |
| 265 | canvas.clear(CanvasKit.Color4f(0.9, 0.9, 0.9, 1.0)); |
| 266 | |
| 267 | const fontMgr = CanvasKit.FontMgr.FromData([robotoData]); |
| 268 | const paraStyle = new CanvasKit.ParagraphStyle({ |
| 269 | textStyle: { |
| 270 | color: CanvasKit.BLACK, |
| 271 | fontFamilies: ['Roboto'], |
| 272 | fontSize: 28, |
| 273 | }, |
| 274 | textAlign: CanvasKit.TextAlign.Left, |
| 275 | }); |
| 276 | const text = 'Any sufficiently entrenched technology is indistinguishable from Javascript'; |
| 277 | const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); |
| 278 | builder.addText(text); |
| 279 | const paragraph = builder.build(); |
| 280 | paragraph.layout(290); // width in pixels to use when wrapping text |
| 281 | canvas.drawParagraph(paragraph, 10, 10); |
| 282 | surface.flush(); |
| 283 | }); |
| 284 | ``` |
| 285 | |
| 286 | <canvas id=foo3 width=300 height=300></canvas> |
| 287 | |
| 288 | <script type="text/javascript"> |
| 289 | const loadFont = fetch('https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf') |
| 290 | .then((response) => response.arrayBuffer()); |
| 291 | |
| 292 | Promise.all([ckLoaded, loadFont]).then(([CanvasKit, robotoData]) => { |
| 293 | const surface = CanvasKit.MakeCanvasSurface('foo3'); |
| 294 | const canvas = surface.getCanvas(); |
| 295 | canvas.clear(CanvasKit.Color4f(0.9, 0.9, 0.9, 1.0)); |
| 296 | |
| 297 | const fontMgr = CanvasKit.FontMgr.FromData([robotoData]); |
| 298 | const paraStyle = new CanvasKit.ParagraphStyle({ |
| 299 | textStyle: { |
| 300 | color: CanvasKit.BLACK, |
| 301 | fontFamilies: ['Roboto'], |
| 302 | fontSize: 28, |
| 303 | }, |
| 304 | textAlign: CanvasKit.TextAlign.Left, |
| 305 | }); |
| 306 | const text = 'Any sufficiently entrenched technology is indistinguishable from Javascript'; |
| 307 | const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); |
| 308 | builder.addText(text); |
| 309 | const paragraph = builder.build(); |
| 310 | paragraph.layout(290); // width in pixels to use when wrapping text |
| 311 | canvas.drawParagraph(paragraph, 10, 10); |
| 312 | surface.flush(); |
| 313 | }); |
| 314 | </script> |
| 315 | |
| 316 | <!--?prettify?--> |
| 317 | ``` js |
| 318 | const fontMgr = CanvasKit.FontMgr.FromData([robotoData]); |
| 319 | ``` |
| 320 | Creates an object that provides fonts by name to various text facilities in CanvasKit. You could |
| 321 | load more than one font in this statement if needed. |
| 322 | |
| 323 | <!--?prettify?--> |
| 324 | ``` js |
| 325 | const paraStyle = new CanvasKit.ParagraphStyle({ |
| 326 | textStyle: { |
| 327 | color: CanvasKit.BLACK, |
| 328 | fontFamilies: ['Roboto'], |
| 329 | fontSize: 28, |
| 330 | }, |
| 331 | textAlign: CanvasKit.TextAlign.Left, |
| 332 | }); |
| 333 | ``` |
| 334 | Specifies the style of the text. The font's name, Roboto, will be used to fetch it from the font |
| 335 | manager. You can specify either (color) or (foregroundColor and backgroundColor) in order to have |
| 336 | a highlight. For the full documentation of the API, check out the Typescript definitions in the |
| 337 | `types/` subfolder of the npm package or in the |
Eric Boren | 04fe267 | 2021-09-27 10:15:39 -0400 | [diff] [blame] | 338 | [Skia repo](https://github.com/google/skia/tree/main/modules/canvaskit/npm_build/types). |
Joe Gregorio | 02f7202 | 2021-03-27 10:12:45 -0400 | [diff] [blame] | 339 | |
| 340 | <!--?prettify?--> |
| 341 | ``` js |
| 342 | const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); |
| 343 | builder.addText(text); |
| 344 | const paragraph = builder.build(); |
| 345 | ``` |
| 346 | Next, we create a `ParagraphBuilder` with a style, add some text, and finalize it with `build()`. |
| 347 | Alternatively, we could use multiple `TextStyle`s in one paragraph with |
| 348 | |
| 349 | <!--?prettify?--> |
| 350 | ``` js |
| 351 | const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); |
| 352 | builder.addText(text1); |
| 353 | const boldTextStyle = CanvasKit.TextStyle({ |
| 354 | color: CanvasKit.BLACK, |
| 355 | fontFamilies: ['Roboto'], |
| 356 | fontSize: 28, |
| 357 | fontStyle: {'weight': CanvasKit.FontWeight.Bold}, |
| 358 | }) |
| 359 | builder.pushStyle(boldTextStyle); |
| 360 | builder.addText(text2); |
| 361 | builder.pop(); |
| 362 | builder.addText(text3); |
| 363 | const paragraph = builder.build(); |
| 364 | ``` |
| 365 | Finally, we *layout* the paragraph, meaning wrap the text to a particular width, and draw it to |
| 366 | the canvas with |
| 367 | |
| 368 | <!--?prettify?--> |
| 369 | ``` js |
| 370 | paragraph.layout(290); // width in pixels to use when wrapping text |
| 371 | canvas.drawParagraph(paragraph, 10, 10); // (x, y) position of left top corner of paragraph. |
| 372 | ``` |
| 373 | |