18423cd5b3
Change-Id: I2da6d2da2cb0dd938d04d0b872be7fc5f2a91d6d Docs-Preview: https://skia.org/?cl=308787 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/308787 Commit-Queue: Nathaniel Nifong <nifong@google.com> Reviewed-by: Kevin Lubick <kjlubick@google.com>
365 lines
13 KiB
Markdown
365 lines
13 KiB
Markdown
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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
<canvas id=foo width=300 height=300></canvas>
|
|
|
|
<script type="text/javascript"
|
|
src="https://unpkg.com/canvaskit-wasm@0.17.2/bin/canvaskit.js"></script>
|
|
<script type="text/javascript">
|
|
const ckLoaded = CanvasKitInit({
|
|
locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.17.2/bin/'+file});
|
|
ckLoaded.then((CanvasKit) => {
|
|
const surface = CanvasKit.MakeCanvasSurface('foo');
|
|
|
|
const paint = new CanvasKit.SkPaint();
|
|
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>
|
|
```
|
|
|
|
<canvas id=foo width=300 height=300></canvas>
|
|
|
|
<script type="text/javascript"
|
|
src="https://unpkg.com/canvaskit-wasm@0.17.2/bin/canvaskit.js"></script>
|
|
<script type="text/javascript">
|
|
const ckLoaded = CanvasKitInit({
|
|
locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.17.2/bin/'+file});
|
|
ckLoaded.then((CanvasKit) => {
|
|
const surface = CanvasKit.MakeCanvasSurface('foo');
|
|
|
|
const paint = new CanvasKit.SkPaint();
|
|
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.
|
|
|
|
<!--?prettify?-->
|
|
``` html
|
|
<script type="text/javascript"
|
|
src="https://unpkg.com/canvaskit-wasm@0.17.2/bin/canvaskit.js"></script>
|
|
```
|
|
and
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
const ckLoaded = CanvasKitInit({
|
|
locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.17.2/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`.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
const surface = CanvasKit.MakeCanvasSurface('foo');
|
|
```
|
|
Creates an SkSurface 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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
const paint = new CanvasKit.SkPaint();
|
|
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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
function draw(canvas) {
|
|
canvas.clear(CanvasKit.WHITE);
|
|
canvas.drawRRect(rr, paint);
|
|
}
|
|
```
|
|
Defines a function that will draw our frame. The function is provided an SkCanvas 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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
ckLoaded.then((CanvasKit) => {
|
|
const surface = CanvasKit.MakeCanvasSurface('foo2');
|
|
|
|
const paint = new CanvasKit.SkPaint();
|
|
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);
|
|
});
|
|
```
|
|
|
|
<canvas id=foo2 width=300 height=300></canvas>
|
|
|
|
<script type="text/javascript">
|
|
ckLoaded.then((CanvasKit) => {
|
|
const surface = CanvasKit.MakeCanvasSurface('foo2');
|
|
|
|
const paint = new CanvasKit.SkPaint();
|
|
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;
|
|
// The box is always moving at a constant speed in one of the four diagonal directions
|
|
let dirX = 1;
|
|
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);
|
|
});
|
|
</script>
|
|
|
|
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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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.SkFontMgr.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();
|
|
});
|
|
```
|
|
|
|
<canvas id=foo3 width=300 height=300></canvas>
|
|
|
|
<script type="text/javascript">
|
|
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.SkFontMgr.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();
|
|
});
|
|
</script>
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
const fontMgr = CanvasKit.SkFontMgr.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.
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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. TODO(nifong): link to the full reference for ParagraphStyle / TextStyle
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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 `TextStyle`s in one paragraph with
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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
|
|
|
|
<!--?prettify?-->
|
|
``` js
|
|
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.
|
|
``` |