Skia Canvas is a Node.js implementation of the HTML Canvas drawing API for both on- and off-screen rendering. Since it uses Google’s Skia graphics engine, its output is very similar to Chrome’s <canvas>
element — though it's also capable of things the browser’s Canvas still can't achieve.
In particular, Skia Canvas:
- generates images in vector (PDF & SVG) as well as bitmap (JPEG, PNG, & WEBP) formats
- can draw to interactive GUI windows and provides a browser-like event framework
- can save images to files, encode to dataURL strings, and return Buffers or Sharp objects
- uses native threads in a user-configurable worker pool for asynchronous rendering and file I/O
- can create multiple ‘pages’ on a given canvas and then output them as a single, multi-page PDF or an image-sequence saved to multiple files
- can simplify, blunt, combine, excerpt, and atomize Bézier paths using efficient boolean operations or point-by-point interpolation
- provides 3D perspective transformations in addition to scaling, rotation, and translation
- can fill shapes with vector-based Textures in addition to bitmap-based Patterns and supports line-drawing with custom markers
- supports the full set of CSS filter image processing operators
- offers rich typographic control including:
- multi-line, word-wrapped text
- line-by-line text metrics
- small-caps, ligatures, and other opentype features accessible using standard font-variant syntax
- proportional letter-spacing, word-spacing, and leading
- support for variable fonts and transparent mapping of weight values
- use of non-system fonts loaded from local files
- can be used for server-side image rendering on standard Linux hosts and ‘serverless’ platforms like Vercel and AWS Lambda
Example Usage
Generating image files
import {Canvas} from 'skia-canvas'
let canvas = new Canvas(400, 400),
ctx = canvas.getContext("2d"),
{width, height} = canvas;
let sweep = ctx.createConicGradient(Math.PI * 1.2, width/2, height/2)
sweep.addColorStop(0, "red")
sweep.addColorStop(0.25, "orange")
sweep.addColorStop(0.5, "yellow")
sweep.addColorStop(0.75, "green")
sweep.addColorStop(1, "red")
ctx.strokeStyle = sweep
ctx.lineWidth = 100
ctx.strokeRect(100,100, 200,200)
// render to multiple destinations using a background thread
async function render(){
// save a ‘retina’ image...
await canvas.saveAs("rainbox.png", {density:2})
// ...or use a shorthand for canvas.toBuffer("png")
let pngData = await canvas.png
// ...or embed it in a string
let pngEmbed = `<img src="${await canvas.toDataURL("png")}">`
}
render()
// ...or save the file synchronously from the main thread
canvas.saveAsSync("rainbox.pdf")
Multi-page sequences
import {Canvas} from 'skia-canvas'
let canvas = new Canvas(400, 400),
ctx = canvas.getContext("2d"),
{width, height} = canvas
for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
ctx = canvas.newPage()
ctx.fillStyle = color
ctx.fillRect(0,0, width, height)
ctx.fillStyle = 'white'
ctx.arc(width/2, height/2, 40, 0, 2 * Math.PI)
ctx.fill()
}
async function render(){
// save to a multi-page PDF file
await canvas.saveAs("all-pages.pdf")
// save to files named `page-01.png`, `page-02.png`, etc.
await canvas.saveAs("page-{2}.png")
}
render()
Rendering to a window
import {Window} from 'skia-canvas'
let win = new Window(300, 300)
win.title = "Canvas Window"
win.on("draw", e => {
let ctx = e.target.canvas.getContext("2d")
ctx.lineWidth = 25 + 25 * Math.cos(e.frame / 10)
ctx.beginPath()
ctx.arc(150, 150, 50, 0, 2 * Math.PI)
ctx.stroke()
ctx.beginPath()
ctx.arc(150, 150, 10, 0, 2 * Math.PI)
ctx.stroke()
ctx.fill()
})
Integrating with Sharp.js
import sharp from 'sharp'
import {Canvas, loadImage} from 'skia-canvas'
let canvas = new Canvas(400, 400),
ctx = canvas.getContext("2d"),
{width, height} = canvas,
[x, y] = [width/2, height/2]
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, x, y)
ctx.fillStyle = 'orange'
ctx.fillRect(x, y, x, y)
// Render the canvas to a Sharp object on a background thread then desaturate
await canvas.toSharp().modulate({saturation:.25}).jpeg().toFile("faded.jpg")
// Convert an ImageData to a Sharp object and save a grayscale version
let imgData = ctx.getImageData(0, 0, width, height, {matte:'white', density:2})
await imgData.toSharp().grayscale().png().toFile("black-and-white.png")
// Create an image using Sharp then draw it to the canvas as an Image object
let sharpImage = sharp({create:{ width:x, height:y, channels:4, background:"skyblue" }})
let canvasImage = await loadImage(sharpImage)
ctx.drawImage(canvasImage, x, 0)
await canvas.saveAs('mosaic.png')
Benchmarks
In these benchmarks, Skia Canvas is tested running in two modes: serial and async. When running serially, each rendering operation is awaited before continuing to the next test iteration. When running asynchronously, all the test iterations are begun at once and are executed in parallel using the library’s multi-threading support.
Startup latency
Library | Per Run | Total Time (100 iterations) |
---|---|---|
canvaskit-wasm | 25 ms | 2.46 s |
canvas | 88 ms | 8.76 s |
@napi-rs/canvas | 73 ms | 7.30 s |
skia-canvas | <1 ms | 33 ms |
Bezier curves
Library | Per Run | Total Time (20 iterations) |
---|---|---|
canvaskit-wasm 👁️ | 789 ms | 15.77 s |
canvas 👁️ | 488 ms | 9.76 s |
@napi-rs/canvas 👁️ | 233 ms | 4.65 s |
skia-canvas (serial) 👁️ | 137 ms | 2.74 s |
skia-canvas (async) 👁️ | 28 ms | 558 ms |
SVG to PNG
Library | Per Run | Total Time (100 iterations) |
---|---|---|
canvaskit-wasm | ————— | ————— not supported |
canvas 👁️ | 122 ms | 12.20 s |
@napi-rs/canvas 👁️ | 98 ms | 9.76 s |
skia-canvas (serial) 👁️ | 59 ms | 5.91 s |
skia-canvas (async) 👁️ | 11 ms | 1.06 s |
Scale/rotate images
Library | Per Run | Total Time (50 iterations) |
---|---|---|
canvaskit-wasm 👁️ | 279 ms | 13.95 s |
canvas 👁️ | 284 ms | 14.21 s |
@napi-rs/canvas 👁️ | 116 ms | 5.78 s |
skia-canvas (serial) 👁️ | 100 ms | 5.01 s |
skia-canvas (async) 👁️ | 19 ms | 937 ms |
Basic text
Library | Per Run | Total Time (200 iterations) |
---|---|---|
canvaskit-wasm 👁️ | 24 ms | 4.74 s |
canvas 👁️ | 24 ms | 4.86 s |
@napi-rs/canvas 👁️ | 19 ms | 3.82 s |
skia-canvas (serial) 👁️ | 21 ms | 4.24 s |
skia-canvas (async) 👁️ | 4 ms | 781 ms |