CanvasKit - Quickstart

CanvasKit is a wasm module that uses Skia to draw to canvas elements a more advance feature set than the canvas API.

Minimal application

This example is a minimal Canvaskit application that draws a rounded rect for one frame. It pulls the wasm binary from unpkg.com but you can also build and host it yourself.

<canvas id=foo width=300 height=300></canvas>

<script type="text/javascript"
  src="https://unpkg.com/canvaskit-wasm@0.19.0/bin/canvaskit.js"></script>
<script type="text/javascript">
  const ckLoaded = CanvasKitInit({
    locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/'+file});
  ckLoaded.then((CanvasKit) => {
    const surface = CanvasKit.MakeCanvasSurface('foo');

    const paint = new CanvasKit.Paint();
    paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0));
    paint.setStyle(CanvasKit.PaintStyle.Stroke);
    paint.setAntiAlias(true);
    const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15);

    function draw(canvas) {
      canvas.clear(CanvasKit.WHITE);
      canvas.drawRRect(rr, paint);
    }
    surface.drawOnce(draw);
  });
</script>

Let’s break it down into parts and explain what they are doing:

<canvas id=foo width=300 height=300></canvas> Creates the canvas to which CanvasKit will draw. This element is where we control the width and height of the drawing buffer, while it’s css style would control any scaling applied after drawing to those pixels. Despite using a canvas element, CanvasKit isn’t calling the HTML canvas’s own draw methods. It is using this canvas element to get a WebGL2 context and performing most of the drawing work in C++ code compiled to WebAssembly, then sending commands to the GPU at the end of each frame.

<script type="text/javascript"
  src="https://unpkg.com/canvaskit-wasm@0.19.0/bin/canvaskit.js"></script>

and

const ckLoaded = CanvasKitInit({
  locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/'+file});
ckLoaded.then((CanvasKit) => {

are loading the canvaskit helper js and wasm binary respectively. CanvasKitInit accepts a function for allowing you to alter the path where it will try to find canvaskit.wasm and returns a promise that resolves with the loaded module, which we typically name CanvasKit.

const surface = CanvasKit.MakeCanvasSurface('foo');

Creates a Surface associated with the HTML canvas element above. Hardware acceleration is the default behavior, but can be overridden by calling MakeSWCanvasSurface instead. MakeCanvasSurface is also where alternative color spaces or gl attrtributes can be specified.

const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setAntiAlias(true);
const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15);

Creates a paint, a description of how to fill or stroke rects, paths, text and other geometry in canvaskit. rr is a rounded rect, with corners having a radius of 25 in the x axis, and 15 pixels in the y axis.

function draw(canvas) {
  canvas.clear(CanvasKit.WHITE);
  canvas.drawRRect(rr, paint);
}

Defines a function that will draw our frame. The function is provided a Canvas object on which we make draw calls. One to clear the entire canvas, and one to draw the rounded rect with the paint from above.

We also delete the paint object. CanvasKit objects created with new or methods prefixed with make must be deleted for the wasm memory to be released. Javascript’s GC will not take care of it automatically. rr is just an array, wasn’t created with new and doesn’t point to any WASM memory, so we don’t have to call delete on it.

surface.drawOnce(draw);
paint.delete()

Hand the drawing function to surface.drawOnce which makes the calls and flushes the surface. Upon flushing, Skia will batch and send WebGL commands, making visible changes appear onscreen. This example draws once and disposes of the surface. As promised, it is is a minimal application.

Basic Draw Loop

What if we need to redraw to our canvas every frame? This example bounces a rounded rect around like a 90s screensaver.

ckLoaded.then((CanvasKit) => {
  const surface = CanvasKit.MakeCanvasSurface('foo2');

  const paint = new CanvasKit.Paint();
  paint.setColor(CanvasKit.Color4f(0.9, 0, 0, 1.0));
  paint.setStyle(CanvasKit.PaintStyle.Stroke);
  paint.setAntiAlias(true);
  // const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 25, 15);
  const w = 100; // size of rect
  const h = 60;
  let x = 10; // initial position of top left corner.
  let y = 60;
  let dirX = 1; // box is always moving at a constant speed in one of the four diagonal directions
  let dirY = 1;

  function drawFrame(canvas) {
    // boundary check
    if (x < 0 || x+w > 300) {
      dirX *= -1; // reverse x direction when hitting side walls
    }
    if (y < 0 || y+h > 300) {
      dirY *= -1; // reverse y direction when hitting top and bottom walls
    }
    // move
    x += dirX;
    y += dirY;

    canvas.clear(CanvasKit.WHITE);
    const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(x, y, x+w, y+h), 25, 15);
    canvas.drawRRect(rr, paint);
    surface.requestAnimationFrame(drawFrame);
  }
  surface.requestAnimationFrame(drawFrame);
});

The main difference here is that we define a function to be called before each frame is drawn and pass it to surface.requestAnimationFrame(drawFrame); That callback is handed a canvas and flushing is taken care of.

function drawFrame(canvas) {
  canvas.clear(CanvasKit.WHITE);
  // code to update and draw the frame goes here
  surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);

Creates a function to serve as our main drawing loop. Each time a frame is about to be rendered (the browser will typically target 60fps), our function is called, we clear the canvas with white, redraw the round rect, and call surface.requestAnimationFrame(drawFrame) registering the function to be called again before the next frame.

surface.requestAnimationFrame(drawFrame) combines window.requestAnimationFrame with surface.flush() and should be used in all the same ways. If your application would only make visible changes as a result of mouse events, don’t call surface.requestAnimationFrame at the end of your drawFrame function. Call it only after handling mouse input.

Text Shaping

One of the biggest features that CanvasKit offers over the HTML Canvas API is paragraph shaping. To use text your applicatoin, supply a font file and use Promise.all to run your code when both CanvasKit and the font file are ready.

const loadFont = fetch('https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf')
  .then((response) => response.arrayBuffer());

Promise.all([ckLoaded, loadFont]).then(([CanvasKit, robotoData]) => {
  const surface = CanvasKit.MakeCanvasSurface('foo3');
  const canvas = surface.getCanvas();
  canvas.clear(CanvasKit.Color4f(0.9, 0.9, 0.9, 1.0));

  const fontMgr = CanvasKit.FontMgr.FromData([robotoData]);
  const paraStyle = new CanvasKit.ParagraphStyle({
    textStyle: {
      color: CanvasKit.BLACK,
      fontFamilies: ['Roboto'],
      fontSize: 28,
    },
    textAlign: CanvasKit.TextAlign.Left,
  });
  const text = 'Any sufficiently entrenched technology is indistinguishable from Javascript';
  const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
  builder.addText(text);
  const paragraph = builder.build();
  paragraph.layout(290); // width in pixels to use when wrapping text
  canvas.drawParagraph(paragraph, 10, 10);
  surface.flush();
});

const fontMgr = CanvasKit.FontMgr.FromData([robotoData]);

Creates an object that provides fonts by name to various text facilities in CanvasKit. You could load more than one font in this statement if needed.

const paraStyle = new CanvasKit.ParagraphStyle({
  textStyle: {
    color: CanvasKit.BLACK,
    fontFamilies: ['Roboto'],
    fontSize: 28,
  },
  textAlign: CanvasKit.TextAlign.Left,
});

Specifies the style of the text. The font’s name, Roboto, will be used to fetch it from the font manager. You can specify either (color) or (foregroundColor and backgroundColor) in order to have a highlight. For the full documentation of the API, check out the Typescript definitions in the types/ subfolder of the npm package or in the Skia repo.

const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
builder.addText(text);
const paragraph = builder.build();

Next, we create a ParagraphBuilder with a style, add some text, and finalize it with build(). Alternatively, we could use multiple TextStyles in one paragraph with

const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
builder.addText(text1);
const boldTextStyle = CanvasKit.TextStyle({
    color: CanvasKit.BLACK,
    fontFamilies: ['Roboto'],
    fontSize: 28,
    fontStyle: {'weight': CanvasKit.FontWeight.Bold},
})
builder.pushStyle(boldTextStyle);
builder.addText(text2);
builder.pop();
builder.addText(text3);
const paragraph = builder.build();

Finally, we layout the paragraph, meaning wrap the text to a particular width, and draw it to the canvas with

paragraph.layout(290); // width in pixels to use when wrapping text
canvas.drawParagraph(paragraph, 10, 10); // (x, y) position of left top corner of paragraph.