skia-canvas 3.0.8 → 4.0.0-rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -231
- package/lib/classes/canvas.js +86 -25
- package/lib/classes/context.js +58 -73
- package/lib/classes/css.js +178 -47
- package/lib/classes/geometry.js +21 -19
- package/lib/classes/gui.js +100 -29
- package/lib/classes/imagery.js +286 -66
- package/lib/classes/neon.js +107 -4
- package/lib/classes/path.js +281 -31
- package/lib/classes/typography.js +66 -3
- package/lib/index.d.ts +321 -81
- package/lib/index.js +2 -2
- package/lib/index.mjs +2 -2
- package/lib/prebuild.mjs +190 -27
- package/lib/urls.js +17 -6
- package/package.json +38 -23
package/README.md
CHANGED
|
@@ -18,200 +18,58 @@
|
|
|
18
18
|
|
|
19
19
|
<div align="center">
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
<!--### [Version 3.0 now available](https://github.com/samizdatco/skia-canvas/discussions/255)-->
|
|
22
22
|
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
Skia Canvas is
|
|
27
|
+
Skia Canvas is an implementation of the [HTML Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) drawing API that runs in [Node.js](https://nodejs.org/en) on Mac, Linux, and Windows systems. Depending on your needs, you can use it as:
|
|
28
|
+
1. **A spec-compliant offscreen canvas:** it accepts the same drawing code you'd write for a browser but can run on servers and in other ‘headless’ contexts to generate image files and buffers.
|
|
29
|
+
2. **A windowing toolkit:** it can open native [windows][window] on macOS, Windows, and Linux with [display-synced][win_animation] drawing and browser-inspired [event handling][win_events].
|
|
30
|
+
3. **A JavaScript interface for the Skia graphics library:** it uses familiar web APIs as a front-end to Google’s sophisticated [imaging engine](https://skia.org), rendering with high-performance native code (and optional GPU acceleration).
|
|
28
31
|
|
|
29
|
-
In particular, Skia Canvas:
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
33
|
+
|
|
34
|
+
### A More Capable Canvas
|
|
35
|
+
|
|
36
|
+
In addition to being a faithful emulation of the [canvas standard](https://html.spec.whatwg.org/multipage/canvas.html), Skia Canvas includes a raft of extensions, adding 2D capabilities that reach well beyond what the browser’s `<canvas>` can do.
|
|
37
|
+
|
|
38
|
+
In particular, Skia Canvas can:
|
|
39
|
+
|
|
40
|
+
- generate images in vector (PDF & SVG) as well as bitmap (JPEG, PNG, WEBP, & RAW) formats
|
|
41
|
+
- save images to [files][toFile], encode to [dataURL][toURL] strings, and return [Buffers][toBuffer] or [Sharp][sharp] objects
|
|
42
|
+
- create [multiple ‘pages’][newPage] on a given canvas and [output][toFile] them as a multi-page PDF or an image-sequence saved to multiple files
|
|
43
|
+
- load [PDFs & SVGs][loadimage] as scalable vector images or open a [multi-page PDF][loadcanvas] as an editable canvas
|
|
44
|
+
- render in wide-gamut Display P3 color with [CSS Color 4][ctx_colors] syntax support
|
|
45
|
+
- [slice][p2d_slice] & [sample][p2d_points] Path2D objects, combine them with [boolean operators][bool-ops], and decompose them into [contours][p2d_contours], [verbs][edges], or [points][p2d_positionAt]
|
|
46
|
+
- transform coordinates using [3D perspective][createProjection()] in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
|
|
47
|
+
- fill paths with vector-based [Textures][createTexture()] or bitmap [Patterns][createPattern()] and draw strokes with custom [markers][lineDashMarker]
|
|
48
|
+
- apply the full set of [CSS filter][filter] image processing operators
|
|
49
|
+
- provide rich typographic control including:
|
|
41
50
|
- multi-line, [word-wrapped][textwrap] text
|
|
42
51
|
- line-by-line [text metrics][c2d_measuretext]
|
|
43
|
-
- small-caps, ligatures, and other opentype features accessible using standard [font-variant][fontvariant] syntax
|
|
52
|
+
- small-caps, ligatures, and other [opentype features][fontfeatures] accessible using standard [font-variant][fontvariant] syntax
|
|
44
53
|
- proportional [letter-spacing][letterSpacing], [word-spacing][wordSpacing], and [leading][c2d_font]
|
|
45
|
-
- support for [variable fonts][VariableFonts] and
|
|
54
|
+
- support for [variable fonts][VariableFonts] and automatic use of weight, width, and optical-sizing axes
|
|
46
55
|
- use of non-system fonts [loaded][fontlibrary-use] from local files
|
|
47
|
-
-
|
|
56
|
+
- use native threads in a [user-configurable][multithreading] worker pool for asynchronous rendering and file I/O
|
|
57
|
+
- render images server-side on standard Linux hosts and ‘serverless’ platforms like Vercel, Cloudflare Containers, and AWS Lambda
|
|
48
58
|
|
|
49
|
-
##
|
|
59
|
+
## Installing Skia Canvas
|
|
50
60
|
|
|
51
61
|
If you’re running on a supported platform, installation should be as simple as:
|
|
52
|
-
```bash
|
|
53
|
-
npm install skia-canvas
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
This will download a pre-compiled library from the project’s most recent [release](https://github.com/samizdatco/skia-canvas/releases).
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### `pnpm`
|
|
60
|
-
If you use the `pnpm` package manager, it will not download `skia-canvas`'s platform-native binary unless you explicitly allow it. You can do this interactively via the ‘approve builds’ command (note that you need to press `<space>` to toggle the selection and then `<enter>` to proceed):
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
pnpm install skia-canvas
|
|
64
|
-
pnpm approve-builds
|
|
65
|
-
```
|
|
66
|
-
In non-interactive scenarios (like building via CI), you can approve the build step when you add `skia-canvas` to your project:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
pnpm install skia-canvas --allow-build=skia-canvas
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Alternatively, you can add a [`pnpm.onlyBuiltDependencies`](https://pnpm.io/9.x/package_json#pnpmonlybuiltdependencies) entry to your `package.json` file to mark the build-step as allowed:
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"pnpm": {
|
|
76
|
-
"onlyBuiltDependencies": ["skia-canvas"]
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Platform Support
|
|
84
|
-
|
|
85
|
-
Skia Canvas runs on Linux, macOS, or Windows as well as serverless platforms like Vercel and AWS Lambda. Precompiled versions of the library’s native code will be automatically downloaded in the appropriate architecture (`arm64` or `x64`) when you install it via npm.
|
|
86
|
-
|
|
87
|
-
The underlying Rust library uses [N-API][node_napi] v8 which allows it to run on all [currently supported](https://nodejs.org/en/about/previous-releases) Node.js releases, and it is backward compatible with versions going back to v12.22+, v14.17+, v15.12+, and v16+.
|
|
88
|
-
|
|
89
|
-
### Linux
|
|
90
|
-
|
|
91
|
-
The library is compatible with Linux systems using [glibc](https://www.gnu.org/software/libc/) 2.28 or later as well as Alpine Linux and the [musl](https://musl.libc.org) C library it favors. It will make use of the system’s `fontconfig` settings in `/etc/fonts` if they exist but will otherwise fall back to using a [placeholder configuration](https://github.com/samizdatco/skia-canvas/blob/main/lib/fonts/fonts.conf), looking for installed fonts at commonly used Linux paths.
|
|
92
|
-
|
|
93
|
-
### Docker
|
|
94
|
-
|
|
95
|
-
If you are setting up a [Dockerfile](https://nodejs.org/en/docs/guides/nodejs-docker-webapp/) that uses [`node`](https://hub.docker.com/_/node) as its basis, the simplest approach is to set your `FROM` image to one of the (Debian-derived) defaults like `node:lts`, `node:22`, `node:24-bookworm`, or simply:
|
|
96
|
-
```dockerfile
|
|
97
|
-
FROM node
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
If you wish to use Alpine as the underlying distribution, you can start with something along the lines of:
|
|
101
|
-
|
|
102
|
-
```dockerfile
|
|
103
|
-
FROM node:alpine
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### AWS Lambda
|
|
107
|
-
|
|
108
|
-
Skia Canvas depends on libraries that aren't present in the standard Lambda [runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). You can add these to your function by uploading a ‘[layer](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html)’ (a zip file containing the required libraries and `node_modules` directory) and configuring your function to use it.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<details><summary>
|
|
112
|
-
|
|
113
|
-
**Detailed AWS instructions**
|
|
114
|
-
|
|
115
|
-
</summary>
|
|
116
|
-
|
|
117
|
-
#### Adding the Skia Canvas layer to your AWS account
|
|
118
|
-
|
|
119
|
-
1. Look in the **Assets** section of Skia Canvas’s [current release](https://github.com/samizdatco/skia-canvas/releases/latest) and download the `aws-lambda-x64.zip` or `aws-lambda-arm64.zip` file (depending on your architecture) but don’t decompress it
|
|
120
|
-
2. Go to the AWS Lambda [Layers console](https://console.aws.amazon.com/lambda/home/#/layers) and click the **Create Layer** button, then fill in the fields:
|
|
121
|
-
- **Name**: `skia-canvas` (or whatever you want)
|
|
122
|
-
- **Description**: you might want to note the Skia Canvas version here
|
|
123
|
-
- **Compatible architectures**: select **x86_64** or **arm64** depending on which zip you chose
|
|
124
|
-
- **Compatible runtimes**: select **Node.js 22.x** (and/or 20.x)
|
|
125
|
-
3. Click the **Choose file** button and select the zip file you downloaded in Step 1, then click **Create**
|
|
126
|
-
|
|
127
|
-
Alternatively, you can use the [`aws` command line tool](https://github.com/aws/aws-cli) to create the layer. This bash script will fetch the skia-canvas version of your choice and make it available to your Lambda functions.
|
|
128
|
-
```sh
|
|
129
|
-
#!/usr/bin/env bash
|
|
130
|
-
VERSION=3.0 # the skia-canvas version to include
|
|
131
|
-
PLATFORM=arm64 # arm64 or x64
|
|
132
|
-
|
|
133
|
-
curl -sLO https://github.com/samizdatco/skia-canvas/releases/download/v${VERSION}/aws-lambda-${PLATFORM}.zip
|
|
134
|
-
aws lambda publish-layer-version \
|
|
135
|
-
--layer-name "skia-canvas" \
|
|
136
|
-
--description "Skia Canvas ${VERSION} layer" \
|
|
137
|
-
--zip-file "fileb://aws-lambda-${PLATFORM}.zip" \
|
|
138
|
-
--compatible-runtimes "nodejs20.x" "nodejs22.x" \
|
|
139
|
-
--compatible-architectures "${X/#x/x86_}"
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### Using the layer in a Lambda function
|
|
143
62
|
|
|
144
|
-
You can now use this layer in any function you create in the [Functions console](https://console.aws.amazon.com/lambda/home/#/functions). After creating a new function, click the **Add a Layer** button and you can select your newly created Skia Canvas layer from the **Custom Layers** layer source.
|
|
145
|
-
|
|
146
|
-
Note that the layer only includes Skia Canvas and its dependencies—any other npm modules you want to use will need to be bundled into your function. To prevent the `skia-canvas` module from being doubly-included, make sure you add it to the `devDependencies` section (**not** the regular `dependencies` section) of your package.json file.
|
|
147
|
-
|
|
148
|
-
</details>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
### Next.js / Webpack
|
|
152
|
-
|
|
153
|
-
If you are using a framework like Next.js that bundles your server-side code with Webpack, you'll need to mark `skia-canvas` as an ‘external’, otherwise its platform-native binary file will be excluded from the final build. Try adding these options to your `next.config.ts` file:
|
|
154
|
-
|
|
155
|
-
```js
|
|
156
|
-
const nextConfig: NextConfig = {
|
|
157
|
-
serverExternalPackages: ['skia-canvas'],
|
|
158
|
-
webpack: (config, options) => {
|
|
159
|
-
if (options.isServer){
|
|
160
|
-
config.externals = [
|
|
161
|
-
...config.externals,
|
|
162
|
-
{'skia-canvas': 'commonjs skia-canvas'},
|
|
163
|
-
]
|
|
164
|
-
}
|
|
165
|
-
return config
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
## Compiling from Source
|
|
172
|
-
|
|
173
|
-
If prebuilt binaries aren’t available for your system you’ll need to compile the portions of this library that directly interface with Skia.
|
|
174
|
-
|
|
175
|
-
Start by installing:
|
|
176
|
-
|
|
177
|
-
1. A recent version of `git` (older versions have difficulties with Skia's submodules)
|
|
178
|
-
2. The [Rust compiler](https://www.rust-lang.org/tools/install) and cargo package manager using [`rustup`](https://rust-lang.github.io/rustup/)
|
|
179
|
-
3. A C compiler toolchain (either LLVM/Clang or MSVC)
|
|
180
|
-
4. Python 3 (used by Skia's [build process](https://skia.org/docs/user/build/))
|
|
181
|
-
5. The [Ninja](https://ninja-build.org) build system
|
|
182
|
-
6. On Linux: Fontconfig and OpenSSL
|
|
183
|
-
|
|
184
|
-
[Detailed instructions](https://github.com/rust-skia/rust-skia#building) for setting up these dependencies on different operating systems can be found in the ‘Building’ section of the Rust Skia documentation. The Dockerfiles in the [containers](https://github.com/samizdatco/skia-canvas/tree/main/containers) directory may also be useful for identifying needed dependencies. Once all the necessary compilers and libraries are present, running `npm run build` will give you a usable library (after a fairly lengthy compilation process).
|
|
185
|
-
|
|
186
|
-
## Global Settings
|
|
187
|
-
|
|
188
|
-
> There are a handful of settings that can only be configured at launch and will apply to all the canvases you create in your script. The sections below describe the different [environment variables][node_env] you can set to make global changes. You can either set them as part of your command line invocation, or place them in a `.env` file in your project directory and use Node 20's [`--env-file` argument][node_env_arg] to load them all at once.
|
|
189
|
-
|
|
190
|
-
### Multithreading
|
|
191
|
-
|
|
192
|
-
When rendering canvases in the background (e.g., by using the asynchronous [toFile][toFile] or [toBuffer][toBuffer] methods), tasks are spawned in a thread pool managed by the [rayon][rayon] library. By default it will create up to as many threads as your CPU has cores. You can see this default value by inspecting any [Canvas][canvas] object's [`engine.threads`][engine] property. If you wish to override this default, you can set the `SKIA_CANVAS_THREADS` environment variable to your preferred value.
|
|
193
|
-
|
|
194
|
-
For example, you can limit your asynchronous processing to two simultaneous tasks by running your script with:
|
|
195
63
|
```bash
|
|
196
|
-
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### Argument Validation
|
|
200
|
-
|
|
201
|
-
There are a number of situations where the browser API will react to invalid arguments by silently ignoring the method call rather than throwing an error. For example, these lines will simply have no effect:
|
|
202
|
-
|
|
203
|
-
```js
|
|
204
|
-
ctx.fillRect(0, 0, 100, "october")
|
|
205
|
-
ctx.lineTo(NaN, 0)
|
|
64
|
+
npm install skia-canvas
|
|
206
65
|
```
|
|
207
66
|
|
|
208
|
-
|
|
209
|
-
Skia Canvas does its best to emulate these quirks, but allows you to opt into a stricter mode in which it will throw TypeErrors in these situations (which can be useful for debugging).
|
|
210
|
-
|
|
211
|
-
Set the `SKIA_CANVAS_STRICT` environment variable to `1` or `true` to enable this mode.
|
|
67
|
+
For detailed [installation][installation] instructions and runtime [configuration][global_settings] options, take a look at the [Getting Started][getting_started] page.
|
|
212
68
|
|
|
213
69
|
## Example Usage
|
|
214
70
|
|
|
71
|
+
Skia Canvas's classes and extensions to the standard are extensively covered in the [API Documentation][api_docs]. But to give you a sense of some of things you can achieve with it, here are some real-world examples:
|
|
72
|
+
|
|
215
73
|
### Generating image files
|
|
216
74
|
|
|
217
75
|
```js
|
|
@@ -221,6 +79,7 @@ let canvas = new Canvas(400, 400),
|
|
|
221
79
|
ctx = canvas.getContext("2d"),
|
|
222
80
|
{width, height} = canvas;
|
|
223
81
|
|
|
82
|
+
// draw an empty box with a gradient at its edges
|
|
224
83
|
let sweep = ctx.createConicGradient(Math.PI * 1.2, width/2, height/2)
|
|
225
84
|
sweep.addColorStop(0, "red")
|
|
226
85
|
sweep.addColorStop(0.25, "orange")
|
|
@@ -231,32 +90,27 @@ ctx.strokeStyle = sweep
|
|
|
231
90
|
ctx.lineWidth = 100
|
|
232
91
|
ctx.strokeRect(100,100, 200,200)
|
|
233
92
|
|
|
234
|
-
// render to multiple destinations using a background thread
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// ...or use a shorthand for canvas.toBuffer("png")
|
|
239
|
-
let pngData = await canvas.png
|
|
240
|
-
// ...or embed it in a string
|
|
241
|
-
let pngEmbed = `<img src="${await canvas.toDataURL("png")}">`
|
|
242
|
-
}
|
|
243
|
-
render()
|
|
93
|
+
// render to multiple destinations using a background thread...
|
|
94
|
+
await canvas.toFile("rainbox.png", {density:2}) // save a ‘retina’ image
|
|
95
|
+
let pngData = await canvas.png // use a shorthand for canvas.toBuffer("png")
|
|
96
|
+
let pngEmbed = `<img src="${await canvas.toURL("png")}">` // embed it in a string
|
|
244
97
|
|
|
245
98
|
// ...or save the file synchronously from the main thread
|
|
246
|
-
canvas.
|
|
99
|
+
canvas.toFileSync("rainbox.pdf")
|
|
247
100
|
```
|
|
101
|
+
[*rainbox.png* 👁️](docs/assets/examples/generating-image-files@2x.png)
|
|
248
102
|
|
|
249
103
|
### Multi-page sequences
|
|
250
104
|
|
|
251
105
|
```js
|
|
252
|
-
import {Canvas} from 'skia-canvas'
|
|
106
|
+
import {Canvas, loadCanvas} from 'skia-canvas'
|
|
253
107
|
|
|
254
108
|
let canvas = new Canvas(400, 400),
|
|
255
|
-
ctx = canvas.getContext("2d"),
|
|
109
|
+
ctx = canvas.getContext("2d"), // leave first page blank
|
|
256
110
|
{width, height} = canvas
|
|
257
111
|
|
|
258
112
|
for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
|
|
259
|
-
ctx = canvas.newPage()
|
|
113
|
+
ctx = canvas.newPage() // add pages 2–6
|
|
260
114
|
ctx.fillStyle = color
|
|
261
115
|
ctx.fillRect(0,0, width, height)
|
|
262
116
|
ctx.fillStyle = 'white'
|
|
@@ -264,15 +118,20 @@ for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
|
|
|
264
118
|
ctx.fill()
|
|
265
119
|
}
|
|
266
120
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
await canvas.saveAs("all-pages.pdf")
|
|
121
|
+
await canvas.toFile("page-{2}.png") // save to files named `page-01.png`, `page-02.png`, etc.
|
|
122
|
+
await canvas.toFile("all-pages.pdf") // save to a single multi-page PDF file
|
|
270
123
|
|
|
271
|
-
|
|
272
|
-
|
|
124
|
+
// the multi-page PDF can be read back in and even drawn upon
|
|
125
|
+
let multipage = await loadCanvas("all-pages.pdf")
|
|
126
|
+
for (let [i, pg] of multipage.pages.entries()){
|
|
127
|
+
pg.font = 'italic 12px serif'
|
|
128
|
+
pg.textAlign = 'center'
|
|
129
|
+
pg.textBaseline = 'middle'
|
|
130
|
+
pg.fillText(`p. ${i+1}`, multipage.width/2, multipage.height/2)
|
|
273
131
|
}
|
|
274
|
-
|
|
132
|
+
await multipage.toFile("all-pages-labeled.pdf")
|
|
275
133
|
```
|
|
134
|
+
[*all-pages-labeled.pdf* 👁️](docs/assets/examples/multi-page-sequences.pdf)
|
|
276
135
|
|
|
277
136
|
### Rendering to a window
|
|
278
137
|
|
|
@@ -294,6 +153,51 @@ win.on("draw", e => {
|
|
|
294
153
|
ctx.fill()
|
|
295
154
|
})
|
|
296
155
|
```
|
|
156
|
+
[*screenshot* 👁️](docs/assets/examples/rendering-to-a-window@2x.png)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
### Wide-gamut colors
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import {Canvas} from 'skia-canvas'
|
|
163
|
+
|
|
164
|
+
let pad = 16, size = 64, width = 4*size + 3*pad,
|
|
165
|
+
canvas = new Canvas(336, 240),
|
|
166
|
+
ctx = canvas.getContext("2d", {colorSpace:"display-p3"})
|
|
167
|
+
|
|
168
|
+
// CSS Color 4 syntax is supported everywhere (and colors can exceed the sRGB gamut)
|
|
169
|
+
for (let [p3, srgb] of [
|
|
170
|
+
["color(display-p3 1 0 0)", "#ff0000"], ["lch(75% 100 150)", "#00dc51"],
|
|
171
|
+
["lch(85% 80 170)", "#00f8b6"], ["color(display-p3 0 1 1)", "#00ffff"]
|
|
172
|
+
]){
|
|
173
|
+
ctx.fillStyle = p3 // wide gamut color
|
|
174
|
+
ctx.fillRect(pad, pad, size, size/2)
|
|
175
|
+
ctx.fillStyle = srgb // nearest sRGB equivalent
|
|
176
|
+
ctx.fillRect(pad, pad + size/2, size, size/2)
|
|
177
|
+
ctx.translate(size + pad, 0)
|
|
178
|
+
}
|
|
179
|
+
ctx.translate(-width, pad + size)
|
|
180
|
+
|
|
181
|
+
// gradients can select the color space used for interpolation
|
|
182
|
+
for (let {space, from, to, hue} of [
|
|
183
|
+
{space:"srgb", from:"navy", to:"gold"}, // perceptual midpoint is off-center
|
|
184
|
+
{space:"oklab", from:"navy", to:"gold"}, // Oklab stays perceptually uniform
|
|
185
|
+
{space:"oklch", from:"red", to:"red", hue:"longer"}, // full 360° from a single hue
|
|
186
|
+
]){
|
|
187
|
+
let ramp = ctx.createLinearGradient(0, pad, width, pad)
|
|
188
|
+
if (hue) ramp.hueInterpolationMethod = hue // only applies to angle-based spaces
|
|
189
|
+
ramp.colorInterpolationMethod = space
|
|
190
|
+
ramp.addColorStop(0, from)
|
|
191
|
+
ramp.addColorStop(1, to)
|
|
192
|
+
ctx.fillStyle = ramp
|
|
193
|
+
ctx.fillRect(0, pad, width, size/2)
|
|
194
|
+
ctx.translate(0, pad + size/2)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await canvas.toFile("test-pattern.png")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
[*test-pattern.png* 👁️](docs/assets/examples/wide-gamut-colors@2x.png)
|
|
297
201
|
|
|
298
202
|
### Integrating with [Sharp.js][sharp]
|
|
299
203
|
|
|
@@ -322,58 +226,59 @@ await imgData.toSharp().grayscale().png().toFile("black-and-white.png")
|
|
|
322
226
|
let sharpImage = sharp({create:{ width:x, height:y, channels:4, background:"skyblue" }})
|
|
323
227
|
let canvasImage = await loadImage(sharpImage)
|
|
324
228
|
ctx.drawImage(canvasImage, x, 0)
|
|
325
|
-
await canvas.
|
|
229
|
+
await canvas.toFile('mosaic.png')
|
|
326
230
|
```
|
|
327
231
|
|
|
232
|
+
[*sharp exports* 👁️](docs/assets/examples/integrating-with-sharp@2x.png)
|
|
233
|
+
|
|
328
234
|
## Benchmarks
|
|
329
235
|
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.
|
|
330
236
|
|
|
331
|
-
[See full results here…](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
237
|
+
[See full results here…](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/index.md)
|
|
332
238
|
|
|
333
239
|
### [Startup latency](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/cold-start.js)
|
|
334
240
|
| Library | Per Run | Total Time (100 iterations) |
|
|
335
241
|
| -------------------- | --------- | --------------------------------------------- |
|
|
336
|
-
| *canvaskit-wasm* | ` 25 ms` | ` 2.
|
|
337
|
-
| *canvas* | ` 88 ms` | ` 8.
|
|
338
|
-
| *@napi-rs/canvas* | `
|
|
242
|
+
| *canvaskit-wasm* | ` 25 ms` | ` 2.47 s`  |
|
|
243
|
+
| *canvas* | ` 88 ms` | ` 8.77 s`  |
|
|
244
|
+
| *@napi-rs/canvas* | ` 69 ms` | ` 6.87 s`  |
|
|
339
245
|
| *skia-canvas* | ` <1 ms` | ` 33 ms`  |
|
|
340
246
|
|
|
341
247
|
### [Bezier curves](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/beziers.js)
|
|
342
248
|
| Library | Per Run | Total Time (20 iterations) |
|
|
343
249
|
| ------------------------------------------------------------- | --------- | ------------------------------------------- |
|
|
344
|
-
| *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
345
|
-
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
346
|
-
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
347
|
-
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
348
|
-
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
250
|
+
| *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_wasm.png) | ` 790 ms` | `15.81 s`  |
|
|
251
|
+
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_canvas.png) | ` 486 ms` | ` 9.72 s`  |
|
|
252
|
+
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_napi.png) | ` 230 ms` | ` 4.60 s`  |
|
|
253
|
+
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_skia-sync.png) | ` 137 ms` | ` 2.74 s`  |
|
|
254
|
+
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/beziers_skia-async.png) | ` 28 ms` | ` 558 ms`  |
|
|
349
255
|
|
|
350
256
|
### [SVG to PNG](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/from-svg.js)
|
|
351
257
|
| Library | Per Run | Total Time (100 iterations) |
|
|
352
258
|
| -------------------------------------------------------------- | --------- | -------------------------------------------- |
|
|
353
259
|
| canvaskit-wasm | ` ————— ` | ` ————— ` *not supported* |
|
|
354
|
-
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
355
|
-
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
356
|
-
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
357
|
-
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
260
|
+
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_canvas.png) | ` 122 ms` | `12.16 s`  |
|
|
261
|
+
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_napi.png) | ` 84 ms` | ` 8.42 s`  |
|
|
262
|
+
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_skia-sync.png) | ` 58 ms` | ` 5.83 s`  |
|
|
263
|
+
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/from-svg_skia-async.png) | ` 11 ms` | ` 1.08 s`  |
|
|
358
264
|
|
|
359
265
|
### [Scale/rotate images](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/image-blit.js)
|
|
360
266
|
| Library | Per Run | Total Time (50 iterations) |
|
|
361
267
|
| ---------------------------------------------------------------- | --------- | ---------------------------------------------- |
|
|
362
|
-
| *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
363
|
-
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
364
|
-
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
365
|
-
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
366
|
-
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
268
|
+
| *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_wasm.png) | ` 274 ms` | `13.72 s`  |
|
|
269
|
+
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_canvas.png) | ` 283 ms` | `14.13 s`  |
|
|
270
|
+
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_napi.png) | ` 112 ms` | ` 5.60 s`  |
|
|
271
|
+
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_skia-sync.png) | ` 100 ms` | ` 5.00 s`  |
|
|
272
|
+
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/image-blit_skia-async.png) | ` 19 ms` | ` 935 ms`  |
|
|
367
273
|
|
|
368
274
|
### [Basic text](https://github.com/samizdatco/canvas-benchmarks/tree/main/tests/text.js)
|
|
369
275
|
| Library | Per Run | Total Time (200 iterations) |
|
|
370
276
|
| ---------------------------------------------------------- | --------- | ---------------------------------------- |
|
|
371
|
-
| *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
372
|
-
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
373
|
-
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
374
|
-
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
375
|
-
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-
|
|
376
|
-
|
|
277
|
+
| *canvaskit-wasm* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_wasm.png) | ` 24 ms` | ` 4.75 s`  |
|
|
278
|
+
| *canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_canvas.png) | ` 24 ms` | ` 4.88 s`  |
|
|
279
|
+
| *@napi-rs/canvas* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_napi.png) | ` 19 ms` | ` 3.83 s`  |
|
|
280
|
+
| *skia-canvas (serial)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_skia-sync.png) | ` 21 ms` | ` 4.26 s`  |
|
|
281
|
+
| *skia-canvas (async)* [👁️](https://github.com/samizdatco/canvas-benchmarks/blob/main/results/darwin-arm64/2025-09-26/snapshots/text_skia-async.png) | ` 4 ms` | ` 819 ms`  |
|
|
377
282
|
|
|
378
283
|
## Acknowledgements
|
|
379
284
|
|
|
@@ -389,35 +294,38 @@ This project is deeply indebted to the work of the [Rust Skia project](https://g
|
|
|
389
294
|
- [@meihuanyu](https://github.com/meihuanyu) contributed filter & path rendering fixes
|
|
390
295
|
|
|
391
296
|
## Copyright
|
|
392
|
-
© 2020–
|
|
297
|
+
© 2020–2026 [Samizdat Drafting Co.](https://samizdat.co)
|
|
393
298
|
|
|
394
|
-
[bool-ops]: https://skia-canvas.org/api/path2d#complement-difference-intersect-union-
|
|
299
|
+
[bool-ops]: https://skia-canvas.org/api/path2d#complement-difference-intersect-union-xor
|
|
395
300
|
[c2d_font]: https://skia-canvas.org/api/context#font
|
|
396
301
|
[c2d_measuretext]: https://skia-canvas.org/api/context#measuretext
|
|
397
|
-
[canvas]: https://skia-canvas.org/api/canvas
|
|
398
302
|
[createProjection()]: https://skia-canvas.org/api/context#createprojection
|
|
399
303
|
[createTexture()]: https://skia-canvas.org/api/context#createtexture
|
|
400
|
-
[
|
|
304
|
+
[edges]: https://skia-canvas.org/api/path2d#edges
|
|
401
305
|
[fontlibrary-use]: https://skia-canvas.org/api/font-library#use
|
|
402
306
|
[fontvariant]: https://skia-canvas.org/api/context#fontvariant
|
|
307
|
+
[fontfeatures]: https://skia-canvas.org/api/context#fontfeaturesettings
|
|
403
308
|
[lineDashMarker]: https://skia-canvas.org/api/context#linedashmarker
|
|
309
|
+
[loadimage]: https://skia-canvas.org/api/image#loadimage
|
|
404
310
|
[newPage]: https://skia-canvas.org/api/canvas#newpage
|
|
405
|
-
[
|
|
311
|
+
[p2d_contours]: https://skia-canvas.org/api/path2d#contours
|
|
406
312
|
[p2d_points]: https://skia-canvas.org/api/path2d#points
|
|
407
|
-
[
|
|
408
|
-
[
|
|
409
|
-
[p2d_trim]: https://skia-canvas.org/api/path2d#trim
|
|
313
|
+
[p2d_positionAt]: https://skia-canvas.org/api/path2d#positionat
|
|
314
|
+
[p2d_slice]: https://skia-canvas.org/api/path2d#slice
|
|
410
315
|
[toFile]: https://skia-canvas.org/api/canvas#tofile
|
|
411
316
|
[textwrap]: https://skia-canvas.org/api/context#textwrap
|
|
412
317
|
[toBuffer]: https://skia-canvas.org/api/canvas#tobuffer
|
|
413
318
|
[toURL]: https://skia-canvas.org/api/canvas#tourl
|
|
414
|
-
[
|
|
319
|
+
[win_events]: https://skia-canvas.org/api/window#events
|
|
320
|
+
[win_animation]: https://skia-canvas.org/api/window#events-for-animation
|
|
415
321
|
[window]: https://skia-canvas.org/api/window
|
|
322
|
+
[loadcanvas]: https://skia-canvas.org/api/canvas#loadcanvas
|
|
323
|
+
[ctx_colors]: https://skia-canvas.org/api/context#choosing-colors
|
|
324
|
+
[api_docs]: https://skia-canvas.org/api
|
|
325
|
+
[getting_started]: https://skia-canvas.org/getting-started
|
|
326
|
+
[installation]: https://skia-canvas.org/getting-started#installation
|
|
327
|
+
[global_settings]: https://skia-canvas.org/getting-started#global-settings
|
|
416
328
|
[multithreading]: https://skia-canvas.org/getting-started#multithreading
|
|
417
|
-
[node_napi]: https://nodejs.org/api/n-api.html#node-api-version-matrix
|
|
418
|
-
[node_env]: https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs
|
|
419
|
-
[node_env_arg]: https://nodejs.org/dist/latest-v22.x/docs/api/cli.html#--env-fileconfig
|
|
420
|
-
[rayon]: https://crates.io/crates/rayon
|
|
421
329
|
[sharp]: https://sharp.pixelplumbing.com
|
|
422
330
|
[VariableFonts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
|
423
331
|
[filter]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
|