skia-canvas 0.9.30 → 1.0.0
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/CHANGELOG.md +29 -1
- package/README.md +427 -65
- package/lib/css.js +40 -0
- package/lib/index.d.ts +79 -3
- package/lib/index.js +327 -19
- package/package.json +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- ## 🥚 ⟩ [Unreleased] -->
|
|
4
4
|
|
|
5
|
+
## 📦 ⟩ [v1.0.0] ⟩ Aug 5, 2022
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
- The new [Window][window] class can display a **Canvas** on screen, respond to mouse and keyboard input, and fluidly [animate][window_anim] by calling user-defined [event handlers][window_events].
|
|
9
|
+
- Bitmap rendering now occurs on the GPU by default and can be configured using the **Canvas**'s [`.gpu`][canvas_gpu] property. If the platform supports hardware-accelerated rendering (using Metal on macOS and Vulkan on Linux & Windows), the property will be `true` by default and can be set to `false` to use the software renderer.
|
|
10
|
+
- Added support for recent Chrome features:
|
|
11
|
+
- the [`reset()`][chrome_reset] context method which erases the canvas, resets the transformation state, and clears the current path
|
|
12
|
+
- the [`roundRect()`][chrome_rrect] method on contexts and **Path2D** objects which adds a rounded rectangle using 1–4 corner radii (provided as a single value or an array of numbers and/or **DOMPoint** objects)
|
|
13
|
+
|
|
14
|
+
### Bugfixes
|
|
15
|
+
- The `FontLibrary.reset()` method didn't actually remove previously installed fonts that had already been drawn with (and thus cached). It now clears those caches, which also means previously used fonts can now be replaced by calling `.use()` again with the same family name.
|
|
16
|
+
- The [`.drawCanvas()`][drawCanvas] routine now applies filter effects and shadows consistent with the current resolution and transformation state.
|
|
17
|
+
|
|
18
|
+
### Misc. Improvements
|
|
19
|
+
- The [`.filter`][filter] property's `"blur(…)"` and `"drop-shadow(…)"` effects now match browser behavior much more closely and scale appropriately with the `density` export option.
|
|
20
|
+
- Antialiasing is smoother, particularly when down-scaling images, thanks to the use of mipmaps rather than Skia's (apparently buggy?) implementation of bucubic interpolation.
|
|
21
|
+
- Calling `clearRect()` with dimensions that fully enclose the canvas will now discard all the vector objects that have been drawn so far (rather than simply covering them up).
|
|
22
|
+
- Upgraded Skia to milestone 103
|
|
23
|
+
|
|
24
|
+
[window]: https://github.com/samizdatco/skia-canvas#window
|
|
25
|
+
[window_anim]: https://github.com/samizdatco/skia-canvas#events--animation
|
|
26
|
+
[window_events]: https://github.com/samizdatco/skia-canvas#on--off--once
|
|
27
|
+
[canvas_gpu]: https://github.com/samizdatco/skia-canvas#gpu
|
|
28
|
+
[filter]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
|
|
29
|
+
[chrome_reset]: https://developer.chrome.com/blog/canvas2d/#context-reset
|
|
30
|
+
[chrome_rrect]: https://developer.chrome.com/blog/canvas2d/#round-rect
|
|
31
|
+
|
|
5
32
|
## 📦 ⟩ [v0.9.30] ⟩ Jun 7, 2022
|
|
6
33
|
|
|
7
34
|
### New Features
|
|
@@ -228,7 +255,8 @@
|
|
|
228
255
|
|
|
229
256
|
**Initial public release** 🎉
|
|
230
257
|
|
|
231
|
-
[unreleased]: https://github.com/samizdatco/skia-canvas/compare/
|
|
258
|
+
[unreleased]: https://github.com/samizdatco/skia-canvas/compare/v1.0.0...HEAD
|
|
259
|
+
[v1.0.0]: https://github.com/samizdatco/skia-canvas/compare/v0.9.30...v1.0.0
|
|
232
260
|
[v0.9.30]: https://github.com/samizdatco/skia-canvas/compare/v0.9.29...v0.9.30
|
|
233
261
|
[v0.9.29]: https://github.com/samizdatco/skia-canvas/compare/v0.9.28...v0.9.29
|
|
234
262
|
[v0.9.28]: https://github.com/samizdatco/skia-canvas/compare/v0.9.27...v0.9.28
|
package/README.md
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="test/assets/readme-header-dark@2x.png">
|
|
3
|
+
<img alt="Skia Canvas" src="test/assets/readme-header@2x.png">
|
|
4
|
+
</picture>
|
|
2
5
|
|
|
3
|
-
Skia Canvas is a browser-less implementation of the HTML Canvas drawing API for Node.js. It is based on Google’s [Skia](https://skia.org) graphics engine and
|
|
6
|
+
Skia Canvas is a browser-less implementation of the HTML Canvas drawing API for Node.js. It is based on Google’s [Skia](https://skia.org) graphics engine and, accordingly, produces very similar results to Chrome’s `<canvas>` element. The library is well suited for use on desktop machines where you can render hardware-accelerated graphics to a window and on the server where it can output a variety of image formats.
|
|
4
7
|
|
|
5
|
-
While the primary goal of this project is to provide a reliable emulation of the [standard API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) according to the [spec](https://html.spec.whatwg.org/multipage/canvas.html), it also extends it in a number of areas
|
|
8
|
+
While the primary goal of this project is to provide a reliable emulation of the [standard API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) according to the [spec](https://html.spec.whatwg.org/multipage/canvas.html), it also extends it in a number of areas to take greater advantage of Skia's advanced graphical features and provide a more expressive coding environment.
|
|
6
9
|
|
|
7
10
|
In particular, Skia Canvas:
|
|
8
11
|
|
|
9
|
-
- is fast and compact since all the heavy lifting is done by native code written in Rust and C++
|
|
10
|
-
- can
|
|
12
|
+
- is fast and compact since rendering takes place on the GPU and all the heavy lifting is done by native code written in Rust and C++
|
|
13
|
+
- can render to [windows](#window) using an OS-native graphics pipeline and provides a browser-like [UI event][win_bind] framework
|
|
14
|
+
- generates output in both raster (JPEG & PNG) and vector (PDF & SVG) image formats
|
|
11
15
|
- can save images to [files][saveAs], return them as [Buffers][toBuffer], or encode [dataURL][toDataURL_ext] strings
|
|
12
16
|
- uses native threads and the Node [worker pool](https://github.com/neon-bindings/rfcs/pull/35) for asynchronous rendering and file I/O
|
|
13
17
|
- can create [multiple ‘pages’][newPage] on a given canvas and then [output][saveAs] them as a single, multi-page PDF or an image-sequence saved to multiple files
|
|
14
18
|
- can [simplify][p2d_simplify], [blunt][p2d_round], [combine][bool-ops], [excerpt][p2d_trim], and [atomize][p2d_points] bézier paths using [efficient](https://www.youtube.com/watch?v=OmfliNQsk88) boolean operations or point-by-point [interpolation][p2d_interpolate]
|
|
15
|
-
-
|
|
19
|
+
- provides [3D perspective][createProjection()] transformations in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
|
|
16
20
|
- can fill shapes with vector-based [Textures][createTexture()] in addition to bitmap-based [Patterns][createPattern()] and supports line-drawing with custom [markers][lineDashMarker]
|
|
17
|
-
-
|
|
21
|
+
- supports the full set of [CSS filter][filter] image processing operators
|
|
18
22
|
- offers rich typographic control including:
|
|
19
23
|
|
|
20
24
|
- multi-line, [word-wrapped](#textwrap) text
|
|
@@ -24,7 +28,6 @@ In particular, Skia Canvas:
|
|
|
24
28
|
- support for [variable fonts][VariableFonts] and transparent mapping of weight values
|
|
25
29
|
- use of non-system fonts [loaded](#usefamilyname-fontpaths) from local files
|
|
26
30
|
|
|
27
|
-
|
|
28
31
|
## Installation
|
|
29
32
|
|
|
30
33
|
If you’re running on a supported platform, installation should be as simple as:
|
|
@@ -52,9 +55,9 @@ Nearly everything you need is statically linked into the library. A notable exce
|
|
|
52
55
|
|
|
53
56
|
### Running in Docker
|
|
54
57
|
|
|
55
|
-
The library is compatible with Linux systems using [glibc](https://www.gnu.org/software/libc/) 2.
|
|
58
|
+
The library is compatible with Linux systems using [glibc](https://www.gnu.org/software/libc/) 2.28 or later as well as Alpine Linux (x64 & arm64) and the [musl](https://musl.libc.org) C library it favors. In both cases, Fontconfig must be installed on the system for `skia-canvas` to operate correctly.
|
|
56
59
|
|
|
57
|
-
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:16`, `node:14`, `node:12`, `node:bullseye`, `node:buster`, or simply:
|
|
60
|
+
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:18`, `node:16`, `node:14-buster`, `node:12-buster`, `node:bullseye`, `node:buster`, or simply:
|
|
58
61
|
```dockerfile
|
|
59
62
|
FROM node
|
|
60
63
|
```
|
|
@@ -87,52 +90,89 @@ Start by installing:
|
|
|
87
90
|
[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. 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).
|
|
88
91
|
|
|
89
92
|
## Example Usage
|
|
93
|
+
|
|
94
|
+
#### Generating image files
|
|
95
|
+
|
|
90
96
|
```js
|
|
91
|
-
const {Canvas
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
const {Canvas} = require('skia-canvas')
|
|
98
|
+
|
|
99
|
+
let canvas = new Canvas(400, 400),
|
|
100
|
+
{width, height} = canvas,
|
|
101
|
+
ctx = canvas.getContext("2d");
|
|
102
|
+
|
|
103
|
+
let sweep = ctx.createConicGradient(Math.PI * 1.2, width/2, height/2)
|
|
104
|
+
sweep.addColorStop(0, "red")
|
|
105
|
+
sweep.addColorStop(0.25, "orange")
|
|
106
|
+
sweep.addColorStop(0.5, "yellow")
|
|
107
|
+
sweep.addColorStop(0.75, "green")
|
|
108
|
+
sweep.addColorStop(1, "red")
|
|
109
|
+
ctx.strokeStyle = sweep
|
|
110
|
+
ctx.lineWidth = 100
|
|
111
|
+
ctx.strokeRect(100,100, 200,200)
|
|
112
|
+
|
|
113
|
+
// render to multiple destinations using a background thread
|
|
114
|
+
async function render(){
|
|
115
|
+
// save a ‘retina’ image...
|
|
116
|
+
await canvas.saveAs("rainbox.png", {density:2})
|
|
117
|
+
// ...or use a shorthand for canvas.toBuffer("png")
|
|
118
|
+
let pngData = await canvas.png
|
|
119
|
+
// ...or embed it in a string
|
|
120
|
+
let pngEmbed = `<img src="${await canvas.toDataURL("png")}">`
|
|
121
|
+
}
|
|
122
|
+
render()
|
|
94
123
|
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
// ...or save the file synchronously from the main thread
|
|
125
|
+
canvas.saveAsSync("rainbox.pdf")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Multi-page sequences
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
const {Canvas} = require('skia-canvas')
|
|
132
|
+
|
|
133
|
+
let canvas = new Canvas(400, 400),
|
|
97
134
|
{width, height} = canvas;
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
ctx
|
|
101
|
-
|
|
102
|
-
ctx.
|
|
103
|
-
ctx.
|
|
104
|
-
ctx.arc(
|
|
136
|
+
for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
|
|
137
|
+
ctx = canvas.newPage()
|
|
138
|
+
ctx.fillStyle = color
|
|
139
|
+
ctx.fillRect(0,0, width, height)
|
|
140
|
+
ctx.fillStyle = 'white'
|
|
141
|
+
ctx.arc(width/2, height/2, 40, 0, 2 * Math.PI)
|
|
105
142
|
ctx.fill()
|
|
106
143
|
}
|
|
107
144
|
|
|
108
|
-
// mask all of the dots that don't overlap with the text
|
|
109
|
-
ctx.filter = 'none'
|
|
110
|
-
ctx.globalCompositeOperation = 'destination-in'
|
|
111
|
-
ctx.font='italic 480px Times, DejaVu Serif'
|
|
112
|
-
ctx.textAlign = 'center'
|
|
113
|
-
ctx.textBaseline = 'top'
|
|
114
|
-
ctx.fillText('¶', width/2, 0)
|
|
115
|
-
|
|
116
|
-
// draw a background behind the clipped text
|
|
117
|
-
ctx.globalCompositeOperation = 'destination-over'
|
|
118
|
-
ctx.fillStyle = '#182927'
|
|
119
|
-
ctx.fillRect(0,0, width,height)
|
|
120
|
-
|
|
121
|
-
// render to files using a background thread
|
|
122
145
|
async function render(){
|
|
123
|
-
// save
|
|
124
|
-
await canvas.saveAs("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
console.log(`<img src="${await canvas.toDataURL("png")}">`)
|
|
146
|
+
// save to a multi-page PDF file
|
|
147
|
+
await canvas.saveAs("all-pages.pdf")
|
|
148
|
+
|
|
149
|
+
// save to files named `page-01.png`, `page-02.png`, etc.
|
|
150
|
+
await canvas.saveAs("page-{2}.png")
|
|
129
151
|
}
|
|
130
152
|
render()
|
|
131
153
|
|
|
132
|
-
// ...or save the file synchronously from the main thread
|
|
133
|
-
canvas.saveAsSync("pilcrow.png")
|
|
134
154
|
```
|
|
135
155
|
|
|
156
|
+
#### Rendering to a window
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
const {Window} = require('skia-canvas')
|
|
160
|
+
|
|
161
|
+
let win = new Window(300, 300)
|
|
162
|
+
win.title = "Canvas Window"
|
|
163
|
+
win.on("draw", e => {
|
|
164
|
+
let ctx = e.target.canvas.getContext("2d")
|
|
165
|
+
ctx.lineWidth = 25 + 25 * Math.cos(e.frame / 10)
|
|
166
|
+
ctx.beginPath()
|
|
167
|
+
ctx.arc(150, 150, 50, 0, 2 * Math.PI)
|
|
168
|
+
ctx.stroke()
|
|
169
|
+
|
|
170
|
+
ctx.beginPath()
|
|
171
|
+
ctx.arc(150, 150, 10, 0, 2 * Math.PI)
|
|
172
|
+
ctx.stroke()
|
|
173
|
+
ctx.fill()
|
|
174
|
+
})
|
|
175
|
+
```
|
|
136
176
|
|
|
137
177
|
|
|
138
178
|
# API Documentation
|
|
@@ -152,6 +192,8 @@ The library exports a number of classes emulating familiar browser objects inclu
|
|
|
152
192
|
|
|
153
193
|
In addition, the module contains:
|
|
154
194
|
|
|
195
|
+
- [Window](#window) a class allowing you to display your canvas interactively in an on-screen window
|
|
196
|
+
- [App](#app) a helper class for coordinating multiple windows in a single script
|
|
155
197
|
- [loadImage()](#loadimage) a utility function for loading `Image` objects asynchronously
|
|
156
198
|
- [FontLibrary](#fontlibrary) a class allowing you to inspect the system’s installed fonts and load additional ones
|
|
157
199
|
|
|
@@ -163,15 +205,16 @@ The Canvas object is a stand-in for the HTML `<canvas>` element. It defines imag
|
|
|
163
205
|
|
|
164
206
|
| Image Dimensions | Rendering Contexts | Output |
|
|
165
207
|
| -- | -- | -- |
|
|
166
|
-
| [**width**][canvas_width] | [**
|
|
167
|
-
| [**height**][canvas_height] | [
|
|
168
|
-
| | [
|
|
169
|
-
| |
|
|
208
|
+
| [**width**][canvas_width] | [**gpu**][canvas_gpu] ⚡ | ~~[**async**][canvas_async]~~ ⚡ |
|
|
209
|
+
| [**height**][canvas_height] | [**pages**][canvas_pages] ⚡ | [**pdf**, **png**, **svg**, **jpg**][shorthands] ⚡ |
|
|
210
|
+
| | [getContext()][getContext] | [saveAs()][saveAs] / [saveAsSync()][saveAs] ⚡ |
|
|
211
|
+
| | [newPage()][newPage] ⚡ | [toBuffer()][toBuffer] / [toBufferSync()][toBuffer] ⚡ |
|
|
170
212
|
| | | [toDataURL()][toDataURL_ext] / [toDataURLSync()][toDataURL_ext] ⚡ |
|
|
171
213
|
|
|
172
214
|
[canvas_width]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/width
|
|
173
215
|
[canvas_height]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/height
|
|
174
216
|
[canvas_async]: #async
|
|
217
|
+
[canvas_gpu]: #gpu
|
|
175
218
|
[canvas_pages]: #pages
|
|
176
219
|
[getContext]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
|
|
177
220
|
[saveAs]: #saveasfilename-page-format-matte-density1-quality092-outlinefalse
|
|
@@ -226,13 +269,17 @@ function synchronous(){
|
|
|
226
269
|
|
|
227
270
|
**The async property has been deprecated** and will be removed in a future release. Use the [`saveAsSync()`][saveAs], [`toBufferSync()`][toBuffer], and [`toDataURLSync()`][toDataURL_ext] methods if the default, asynchronous versions aren't to your liking.
|
|
228
271
|
|
|
272
|
+
#### `.gpu`
|
|
273
|
+
|
|
274
|
+
The `.gpu` attribute allows you to control whether rendering occurs on the graphics card or uses the CPU. Rendering is hardware accelerated by default, using [Metal](https://developer.apple.com/metal/) on macOS and [Vulkan](https://www.vulkan.org) on Linux and Windows. To use software-based rendering, set the `.gpu` property to `false`. If the current platform doesn't support GPU-based rendering, the property will be `false` by default (see [this article](https://linuxconfig.org/install-and-test-vulkan-on-linux) for some tips on getting Vulkan working on Linux).
|
|
275
|
+
|
|
229
276
|
#### `.pages`
|
|
230
277
|
|
|
231
278
|
The canvas’s `.pages` attribute is an array of [`CanvasRenderingContext2D`][CanvasRenderingContext2D] objects corresponding to each ‘page’ that has been created. The first page is added when the canvas is initialized and additional ones can be added by calling the `newPage()` method. Note that all the pages remain drawable persistently, so you don’t have to constrain yourself to modifying the ‘current’ page as you render your document or image sequence.
|
|
232
279
|
|
|
233
280
|
#### `.pdf`, `.svg`, `.jpg`, and `.png`
|
|
234
281
|
|
|
235
|
-
These properties are syntactic sugar for calling the `toBuffer()` method. Each returns a Node [`Buffer`][Buffer] object with the contents of the canvas in the given format. If more than one page has been added to the canvas, only the most recent one will be included unless you’ve accessed the `.pdf` property in which case the buffer will contain a multi-page PDF.
|
|
282
|
+
These properties are syntactic sugar for calling the `toBuffer()` method. Each returns a [Promise][Promise] that resolves to a Node [`Buffer`][Buffer] object with the contents of the canvas in the given format. If more than one page has been added to the canvas, only the most recent one will be included unless you’ve accessed the `.pdf` property in which case the buffer will contain a multi-page PDF.
|
|
236
283
|
|
|
237
284
|
##### METHODS
|
|
238
285
|
|
|
@@ -291,14 +338,14 @@ Most of your interaction with the canvas will actually be directed toward its
|
|
|
291
338
|
|-----------------------------------------------|---------------------------------------------------|---------------------------------------------------|----------------------------------------------|--------------------------------------------------|------------------------------------------|------------------------------------------------------------------|----------------------------------------------------|----------------------------------------------------------|
|
|
292
339
|
| [**canvas**][canvas_attr] ⧸[⚡](#canvas) | [clearRect()][clearRect()] | [**fillStyle**][fillStyle] | [**lineCap**][lineCap] | [**currentTransform**][currentTransform] | [moveTo()][moveTo()] | [**direction**][direction] | [**imageSmoothingEnabled**][imageSmoothingEnabled] | [**filter**][filter] |
|
|
293
340
|
| [beginPath()][beginPath()] | [fillRect()][fillRect()] | [**strokeStyle**][strokeStyle] | [**lineDashFit** ⚡][lineDashFit] | [createProjection() ⚡][createProjection()] | [lineTo()][lineTo()] | [**font**][font] ⧸[⚡](#font) | [**imageSmoothingQuality**][imageSmoothingQuality] | [**globalAlpha**][globalAlpha] |
|
|
294
|
-
| [
|
|
295
|
-
| [
|
|
296
|
-
| [
|
|
297
|
-
| [
|
|
298
|
-
| [
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
| | | | | | [
|
|
341
|
+
| [closePath()][closePath()] | [strokeRect()][strokeRect()] | [createConicGradient()][createConicGradient()] | [**lineDashMarker** ⚡][lineDashMarker] | [getTransform()][getTransform()] | [arcTo()][arcTo()] | [**fontVariant** ⚡](#fontvariant) | [createImageData()][createImageData()] | [**globalCompositeOperation**][globalCompositeOperation] |
|
|
342
|
+
| [isPointInPath()][isPointInPath()] | [fillText()][fillText()] ⧸[⚡][drawText] | [createLinearGradient()][createLinearGradient()] | [**lineDashOffset**][lineDashOffset] | [setTransform()][setTransform()] | [bezierCurveTo()][bezierCurveTo()] | [**textAlign**][textAlign] | [getImageData()][getImageData()] | [**shadowBlur**][shadowBlur] |
|
|
343
|
+
| [isPointInStroke()][isPointInStroke()] | [strokeText()][strokeText()] ⧸[⚡][drawText] | [createRadialGradient()][createRadialGradient()] | [**lineJoin**][lineJoin] | [resetTransform()][resetTransform()] | [conicCurveTo() ⚡][conicCurveTo] | [**textBaseline**][textBaseline] | [putImageData()][putImageData()] | [**shadowColor**][shadowColor] |
|
|
344
|
+
| [save()][save()] | [fill()][fill()] | [createPattern()][createPattern()] | [**lineWidth**][lineWidth] | [transform()][transform()] | [quadraticCurveTo()][quadraticCurveTo()] | [**textTracking** ⚡](#texttracking) | [drawCanvas() ⚡](#drawcanvascanvas-x-y-) | [**shadowOffsetX**][shadowOffsetX] |
|
|
345
|
+
| [restore()][restore()] | [stroke()][stroke()] | [createTexture() ⚡][createTexture()] | [**miterLimit**][miterLimit] | [translate()][translate()] | [arc()][arc()] | [**textWrap** ⚡](#textwrap) | [drawImage()][drawImage()] | [**shadowOffsetY**][shadowOffsetY] |
|
|
346
|
+
| [reset()][reset()] | | | [getLineDash()][getLineDash()] | [rotate()][rotate()] | [ellipse()][ellipse()] | [measureText()][measureText()] ⧸[⚡](#measuretextstr-width) | | |
|
|
347
|
+
| [clip()][clip()] | | | [setLineDash()][setLineDash()] | [scale()][scale()] | [rect()][rect()] | [outlineText() ⚡][outlineText()] | | |
|
|
348
|
+
| | | | | | [roundRect()][roundRect()] | | |
|
|
302
349
|
|
|
303
350
|
##### PROPERTIES
|
|
304
351
|
|
|
@@ -540,14 +587,14 @@ for (let i=0; i<8000; i++){
|
|
|
540
587
|
The `Path2D` class allows you to create paths independent of a given [Canvas](#canvas) or [graphics context](#canvasrenderingcontext2d). These paths can be modified over time and drawn repeatedly (potentially on multiple canvases). `Path2D` objects can also be used as [lineDashMarker][lineDashMarker]s or as the repeating pattern in a [CanvasTexture][createTexture()].
|
|
541
588
|
|
|
542
589
|
|
|
543
|
-
| Line Segments | Shapes
|
|
544
|
-
| -- | --
|
|
545
|
-
| [**d** ⚡](#d) | [addPath()][p2d_addPath]
|
|
546
|
-
| [moveTo()][p2d_moveTo] | [arc()][p2d_arc]
|
|
547
|
-
| [lineTo()][p2d_lineTo] | [arcTo()][p2d_arcTo]
|
|
548
|
-
| [bezierCurveTo()][p2d_bezierCurveTo] | [ellipse()][p2d_ellipse]
|
|
549
|
-
| [conicCurveTo() ⚡][conicCurveTo] | [rect()][p2d_rect]
|
|
550
|
-
| [quadraticCurveTo()][p2d_quadraticCurveTo] |
|
|
590
|
+
| Line Segments | Shapes | Boolean Ops ⚡ | Filters ⚡ | Geometry ⚡ |
|
|
591
|
+
| -- | -- | -- | -- | -- |
|
|
592
|
+
| [**d** ⚡](#d) | [addPath()][p2d_addPath] | [complement()][bool-ops] | [interpolate()][p2d_interpolate] | [**bounds**](#bounds) |
|
|
593
|
+
| [moveTo()][p2d_moveTo] | [arc()][p2d_arc] | [difference()][bool-ops] | [jitter()][p2d_jitter] | [**edges**](#edges) |
|
|
594
|
+
| [lineTo()][p2d_lineTo] | [arcTo()][p2d_arcTo] | [intersect()][bool-ops] | [round()][p2d_round] | [contains()][p2d_contains] |
|
|
595
|
+
| [bezierCurveTo()][p2d_bezierCurveTo] | [ellipse()][p2d_ellipse] | [union()][bool-ops] | [simplify()][p2d_simplify] | [points()][p2d_points] |
|
|
596
|
+
| [conicCurveTo() ⚡][conicCurveTo] | [rect()][p2d_rect] | [xor()][bool-ops] | [trim()][p2d_trim] | [offset()][p2d_offset] |
|
|
597
|
+
| [quadraticCurveTo()][p2d_quadraticCurveTo] | [roundRect()][roundRect()] | | [unwind()][p2d_unwind] | [transform()][p2d_transform] |
|
|
551
598
|
| [closePath()][p2d_closePath] |
|
|
552
599
|
|
|
553
600
|
#### Creating `Path2D` objects
|
|
@@ -774,6 +821,319 @@ let unwound = orig.unwind()
|
|
|
774
821
|
```
|
|
775
822
|

|
|
776
823
|
|
|
824
|
+
|
|
825
|
+
## Window
|
|
826
|
+
|
|
827
|
+
The `Window` class allows you to open a native OS window and draw within its frame. You can create multiple windows (each with their own event-handling and rendering routines) and update them in response to user input.
|
|
828
|
+
|
|
829
|
+
Its attributes and methods include:
|
|
830
|
+
|
|
831
|
+
| Dimensions | Content | Interface | Mode | Methods |
|
|
832
|
+
| -- | -- | -- | -- | -- |
|
|
833
|
+
| [**left**][win_layout] | [**background**][win_background] | [**title**][title] | [**visible**][visible] | [on()][win_bind] / [once()][win_bind] |
|
|
834
|
+
| [**top**][win_layout] | [**canvas**][win_canvas] | [**cursor**][cursor] | [**fullscreen**][fullscreen] | [off()][win_bind] |
|
|
835
|
+
| [**width**][win_layout] | [**ctx**][win_ctx] | [**fit**][fit] | | [close()][close] |
|
|
836
|
+
| [**height**][win_layout] | [**page**][win_page] | | | |
|
|
837
|
+
|
|
838
|
+
[win_background]: #background
|
|
839
|
+
[win_canvas]: #canvas-1
|
|
840
|
+
[win_ctx]: #ctx
|
|
841
|
+
[win_page]: #page-1
|
|
842
|
+
[win_layout]: #left--top--width--height
|
|
843
|
+
[title]: #title
|
|
844
|
+
[cursor]: #cursor
|
|
845
|
+
[fit]: #fit
|
|
846
|
+
[visible]: #visible
|
|
847
|
+
[fullscreen]: #fullscreen
|
|
848
|
+
[win_bind]: #on--off--once
|
|
849
|
+
[close]: #close
|
|
850
|
+
|
|
851
|
+
#### Creating new `Window` objects
|
|
852
|
+
|
|
853
|
+
When called with no arguments, the `Window` constructor will return a 512 × 512 pt window with a white background and automatically create a `Canvas` of the same size that you can access through its `.canvas` property:
|
|
854
|
+
|
|
855
|
+
```js
|
|
856
|
+
let win = new Window()
|
|
857
|
+
console.log(win.canvas)
|
|
858
|
+
// Canvas {width:512, height:512, gpu:true, pages:[CanvasRenderingContext2D{}]}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
You can specify a size (to be shared by the window and canvas) by passing width & height arguments:
|
|
862
|
+
```js
|
|
863
|
+
let smaller = new Window(256, 128)
|
|
864
|
+
````
|
|
865
|
+
|
|
866
|
+
All of the other window properties can be customized by passing an options object, either in addition to the width & height or all by itself:
|
|
867
|
+
|
|
868
|
+
```js
|
|
869
|
+
let orange = new Window(1024, 768, {background:"orange"})
|
|
870
|
+
let titled = new Window({title:"Canvas Window"}) // use default 512×512 size
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
After creating the window, you can modify these properties through simple assignment:
|
|
874
|
+
|
|
875
|
+
```js
|
|
876
|
+
let win = new Window(800, 600, {title="Multi-step Window"})
|
|
877
|
+
win.background = "skyblue"
|
|
878
|
+
win.top = 40
|
|
879
|
+
win.left = 40
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
The object accessible through the window’s `.canvas` attribute is no different than any other `Canvas` you create. You can even create a `Window` after setting up a canvas and tell the window to use it instead of automatically creating one. If you pass it to the constructor without specifying a window size, the window will match the dimensions of the canvas:
|
|
883
|
+
|
|
884
|
+
```js
|
|
885
|
+
let bigCanvas = new Canvas(1024, 1024)
|
|
886
|
+
let win = new Window({canvas:bigCanvas})
|
|
887
|
+
console.log([win.width, win.height])
|
|
888
|
+
// [1024, 1024]
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Likewise, assigning a new `.canvas` will replace the contents of the window (though it won’t affect the window’s size):
|
|
892
|
+
|
|
893
|
+
```js
|
|
894
|
+
let win = new Window()
|
|
895
|
+
win.canvas = new Canvas(1024, 32)
|
|
896
|
+
|
|
897
|
+
console.log([win.width, win.height])
|
|
898
|
+
// [512, 512]
|
|
899
|
+
console.log([win.canvas.width, win.canvas.height])
|
|
900
|
+
// [1024, 32]
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
> When the window and canvas sizes don’t perfectly match, the canvas will be scaled using the approach selected via the window’s [`fit`][fit] property.
|
|
904
|
+
|
|
905
|
+
#### Drawing to a Window
|
|
906
|
+
|
|
907
|
+
To draw to the window’s canvas, you can either use the reference to its `.canvas` property to create a context, or use the shortcut `.ctx` property which skips that step:
|
|
908
|
+
|
|
909
|
+
```js
|
|
910
|
+
let win = new Window({background:"olive", fit:"contain-y"})
|
|
911
|
+
console.log(win.ctx === win.canvas.getContext("2d"))
|
|
912
|
+
// true
|
|
913
|
+
|
|
914
|
+
let {canvas, ctx} = win
|
|
915
|
+
ctx.fillStyle = 'lightskyblue'
|
|
916
|
+
ctx.fillRect(10, 10, canvas.width-20, canvas.height-20)
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
If you create multiple pages in your canvas using [newPage()][newPage], you can select which one is currently displayed by setting the window’s [`.page`][win_page]. By default, the most recently created page will be visible, but if you create a new page after the window is on screen, you’ll need to update the `.page` attribute manually to display it. The window’s `.ctx` shortcut will always point to the context for the currently visible page.
|
|
920
|
+
|
|
921
|
+
```js
|
|
922
|
+
let canvas = new Canvas(32, 32),
|
|
923
|
+
colors = ['orange', 'yellow', 'green', 'skyblue', 'purple']
|
|
924
|
+
|
|
925
|
+
for (var c of colors){
|
|
926
|
+
ctx = canvas.newPage(canvas.width * 2, canvas.height * 2)
|
|
927
|
+
ctx.fillStyle = c
|
|
928
|
+
ctx.fillRect(0,0, canvas.width, canvas.height)
|
|
929
|
+
ctx.fillStyle = 'white'
|
|
930
|
+
ctx.arc(canvas.width/2, canvas.height/2, 40, 0, 2 * Math.PI)
|
|
931
|
+
ctx.fill()
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
let win = new Window({canvas, page:-2})
|
|
935
|
+
win.on('keydown', e => {
|
|
936
|
+
if (e.key=='Left') win.page--
|
|
937
|
+
if (e.key=='Right') win.page++
|
|
938
|
+
console.log(`page ${win.page}/${canvas.pages.length}: ${canvas.width} × ${canvas.height}`)
|
|
939
|
+
})
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
#### Responding to Events
|
|
943
|
+
|
|
944
|
+
Once you've created a `Window` object, Node will wait for your current function to end and then switch over to an OS-controlled event loop for the rest of your program’s runtime. This means it can actively redraw your canvas when you resize the window or update its contents, but also means the Node interpreter will be frozen for the duration.
|
|
945
|
+
|
|
946
|
+
As a result, you cannot rely upon Node's traditional asynchrononous behavior for structuring your program. In particular, the usual methods for scheduling callbacks like `setTimeout`, `setImmediate`, and `setInterval` **will not work**.
|
|
947
|
+
|
|
948
|
+
Instead, you must use event handlers attached to the `Window` object. By calling the window’s `.on()`, `.off()`, and `.once()` methods, you can respond to [user interface events][win_bind] like mouse and keyboard input, the window being dragged or resized, a new window becoming active, etc.
|
|
949
|
+
|
|
950
|
+
Any changes you make in an event handler (whether to the window's canvas or its attributes) will become visible in the next pass through the event loop. For example, you can let the user scribble to the canvas with the mouse and clear it via the escape key with:
|
|
951
|
+
|
|
952
|
+
```js
|
|
953
|
+
let win = new Window(400, 300, {background:'rgba(16, 16, 16, 0.35)'}),
|
|
954
|
+
{canvas, ctx} = win // use the canvas & context created by the window
|
|
955
|
+
|
|
956
|
+
win.on('mousemove', ({button, x, y}) => {
|
|
957
|
+
if (button == 0){ // a left click
|
|
958
|
+
ctx.fillStyle = `rgb(${Math.floor(255 * Math.random())},0,0)`
|
|
959
|
+
ctx.beginPath()
|
|
960
|
+
ctx.arc(x, y, 10 + 30 * Math.random(), 0, 2 * Math.PI)
|
|
961
|
+
ctx.fill()
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
win.cursor = button === 0 ? 'none' : 'crosshair'
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
win.on('keydown', ({key}) => {
|
|
968
|
+
if (key == 'Escape'){
|
|
969
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
970
|
+
}
|
|
971
|
+
})
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
In the previous example, we used references to the window’s `ctx` and `canvas` that were created outside the event handler, but this makes the function less general since it's tied to a single window. We can get a reference to the specific window associated with an event through its `.target` attribute, allowing us to write an event handler that doesn't contain a reference to the `win` variable it's attached to:
|
|
975
|
+
```js
|
|
976
|
+
const closeWindow = (e) => {
|
|
977
|
+
console.log("now closing window:", e.target)
|
|
978
|
+
e.target.close()
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
let win1 = new Window(),
|
|
982
|
+
win2 = new Window();
|
|
983
|
+
win1.on('mousedown', closeWindow)
|
|
984
|
+
win2.on('mousedown', closeWindow)
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
Alternatively, we could have created our event handler using a `function(e){…}` defintion (rather than an `(e) => {…}` arrow expression) in which case the `this` variable will point to the window:
|
|
988
|
+
```js
|
|
989
|
+
function closeWindow(e){
|
|
990
|
+
console.log("now closing window:", this)
|
|
991
|
+
this.close()
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
#### Events for Animation
|
|
997
|
+
|
|
998
|
+
In the previous example you may have noticed that the canvas’s contents were preserved in between events and the screen was only being updated in response to user interaction. In general, this is the behavior you want for UI-driven graphics.
|
|
999
|
+
|
|
1000
|
+
But another common case is creating animations in which you redraw the canvas at regular intervals (quite possibly from scratch rather than layering atop the previous contents). In these situations you’ll want to use a set of events that are driven by *timing* rather than interaction:
|
|
1001
|
+
- [`setup`][setup] fires once, just before your window is first drawn to the screen
|
|
1002
|
+
- [`frame`][frame] fires [60 times per second][fps] and provides a frame counter in its event object
|
|
1003
|
+
- [`draw`][draw] fires immediately after `frame` and **clears the canvas** of any window that has event handlers for it
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
To create a ‘flipbook’ animation (in which the screen is fully redrawn in each pass), your best choice is set up an event handler for the `draw` event. Since `draw` automatically erases the canvas before your code begins to run, you can presume a clean slate each time. The event object passed as an argument to your handler contains a propery called `frame` which will increment by one each time you draw (making it handy for advancing the ‘state’ of your animation):
|
|
1007
|
+
|
|
1008
|
+
```js
|
|
1009
|
+
let win = new Window(300, 300, {background:'red'}),
|
|
1010
|
+
{ctx} = win
|
|
1011
|
+
|
|
1012
|
+
win.on("draw", e => {
|
|
1013
|
+
ctx.strokeStyle = 'white'
|
|
1014
|
+
ctx.lineWidth = 60 + 80 * Math.sin(e.frame/20)
|
|
1015
|
+
ctx.beginPath()
|
|
1016
|
+
ctx.moveTo(100,100)
|
|
1017
|
+
ctx.lineTo(200,200)
|
|
1018
|
+
ctx.moveTo(100,200)
|
|
1019
|
+
ctx.lineTo(200,100)
|
|
1020
|
+
ctx.stroke()
|
|
1021
|
+
})
|
|
1022
|
+
````
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
##### PROPERTIES
|
|
1026
|
+
|
|
1027
|
+
#### `background`
|
|
1028
|
+
This specifies the color of the window's background which is drawn behind your canvas content. It supports all the same CSS color formats as the `fillStyle` and `strokeStyle` properties. Defaults to white.
|
|
1029
|
+
|
|
1030
|
+
#### `canvas`
|
|
1031
|
+
The `Canvas` object associated with the window. By default the window will create a canvas with the same size as the window dimensions, but the canvas can also be replaced at any time by assigning a new one to this property.
|
|
1032
|
+
|
|
1033
|
+
#### `ctx`
|
|
1034
|
+
The rendering context of the window's canvas. This is a shortcut to calling `win.canvas.getContext("2d")`. If the canvas has multiple pages, this will point to the most recent (i.e., the ‘topmost’ page in the stack).
|
|
1035
|
+
|
|
1036
|
+
#### `page`
|
|
1037
|
+
A 1-based index into the canvas's pages array. If the canvas has multiple pages, this property allows you to select which one to display (potentially allowing for pre-rendering a canvas then animating it as a flip-book). Page `1` is the earliest (or ‘bottommost’) page created. Negative page numbers also work, counting backward from `-1` (the ‘topmost’ page).
|
|
1038
|
+
|
|
1039
|
+
#### `left` / `top` / `width` / `height`
|
|
1040
|
+
The current location and size of the window as specified in resolution-independent ‘points’. Defaults to a 512 × 512 pt window in the center of the screen. Note that the window and the canvas have independent sizes: the window will scale the canvas's content to fit its current dimensions (using the `fit` property to determine how to deal with differences in aspect ratio).
|
|
1041
|
+
|
|
1042
|
+
#### `title`
|
|
1043
|
+
The string that is displayed in the window's title bar.
|
|
1044
|
+
|
|
1045
|
+
#### `cursor`
|
|
1046
|
+
The icon used for the mouse pointer. By default an arrow cursor is used, but other styles can be selected by setting the property to one of the standard [CSS cursor][mdn_cursor] values.
|
|
1047
|
+
|
|
1048
|
+
#### `fit`
|
|
1049
|
+
When the window is resized, it is likely that it will not perfectly match the aspect ratio of the underlying canvas. This property selects how the layout should adapt—whether it should add margins, allow portions of the canvas to be cropped, or stretch the image to fit. It supports the standard [CSS modes][mdn_object_fit] (`"none"`, `"contain"`, `"cover"`, `"fill"`, and `"scale-down"`) plus some additions:
|
|
1050
|
+
- `contain-x` and `contain-y` extend the `contain` mode to choose which axis to use when fitting the canvas
|
|
1051
|
+
- `resize` will modify the window's canvas to match the new window size (you'll probably also want to define an `.on("resize")` handler to update the contents)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
#### `visible`
|
|
1055
|
+
When set to `false`, the window will become invisible but will not be permanently ‘closed’. It can be made visible again by setting the property back to `true`.
|
|
1056
|
+
|
|
1057
|
+
#### `fullscreen`
|
|
1058
|
+
A boolean flag determining whether the window should expand to fill the screen.
|
|
1059
|
+
|
|
1060
|
+
##### METHODS
|
|
1061
|
+
|
|
1062
|
+
#### `close()`
|
|
1063
|
+
Removes the window from the screen permanently. References to the `Window` object will remain valid however, and its canvas can still be used to export images to file, be inserted into other windows, etc.
|
|
1064
|
+
|
|
1065
|
+
#### `on()` / `off()` / `once()`
|
|
1066
|
+
The `Window` object is an [Event Emitter][event_emitter] subclass and supports all the standard methods for adding and removing event listeners. The supported events are mostly consistent with browser-based DOM events, but include some non-standard additions (⚡) specific to Skia Canvas:
|
|
1067
|
+
|
|
1068
|
+
| Mouse | Keyboard | Window | Focus | Animation |
|
|
1069
|
+
| -- | -- | -- | -- | -- |
|
|
1070
|
+
| [`mousedown`][mousedown] | [`keydown`][keydown] | [`move`](#move-event) ⚡ | [`blur`][blur] | [`setup`][setup] ⚡|
|
|
1071
|
+
| [`mouseup`][mouseup] | [`keyup`][keyup] | [`resize`][resize] | [`focus`][focus] | [`frame`][frame] ⚡|
|
|
1072
|
+
| [`mousemove`][mousemove] | [`input`][input] | | | [`draw`][draw] ⚡ |
|
|
1073
|
+
| [`wheel`][wheel] |
|
|
1074
|
+
|
|
1075
|
+
##### `move` event
|
|
1076
|
+
Emitted when the user drags the window to a new position. The event object includes `top` and `left` properties expressed in resolution-independent points.
|
|
1077
|
+
|
|
1078
|
+
##### `setup` event
|
|
1079
|
+
The `setup` event is emitted just before a newly created window is displayed on screen. This can be a good place to collect the data you'll need for an animation. Immediately after `setup`, the `frame` and `draw` events will fire.
|
|
1080
|
+
|
|
1081
|
+
##### `frame` event
|
|
1082
|
+
Similar to the `requestAnimationFrame` callback system in browsers, the `frame` event allows you to schedule redrawing your canvas to maintain a constant frame rate. The event object provides a window-specific frame counter that begins ticking upward from zero as soon as the window appears.
|
|
1083
|
+
|
|
1084
|
+
##### `draw` event
|
|
1085
|
+
The `draw` event fires immediately after `frame` and has the potentially convenient side effect of automatically erasing the window's canvas before calling your event handler.
|
|
1086
|
+
|
|
1087
|
+
> Note that this canvas-clearing behavior depends upon your having set up an event handler using `.on("draw", …)` and will continue until (and unless) you delete the window's `draw` event handlers using `.off()` or [`removeAllListeners()`][remove_all].
|
|
1088
|
+
|
|
1089
|
+
[event_emitter]: https://nodejs.org/api/events.html#class-eventemitter
|
|
1090
|
+
[event_on]: https://nodejs.org/api/events.html#emitteroneventname-listener
|
|
1091
|
+
[event_off]: https://nodejs.org/api/events.html#emitteroffeventname-listener
|
|
1092
|
+
[event_once]: https://nodejs.org/api/events.html#emitteronceeventname-listener
|
|
1093
|
+
[remove_all]: https://nodejs.org/api/events.html#emitterremovealllistenerseventname
|
|
1094
|
+
[mdn_cursor]: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
|
|
1095
|
+
[mdn_object_fit]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
|
|
1096
|
+
[mousedown]: https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event
|
|
1097
|
+
[mouseup]: https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseup_event
|
|
1098
|
+
[mousemove]: https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event
|
|
1099
|
+
[wheel]: https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
|
|
1100
|
+
[keydown]: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
|
|
1101
|
+
[keyup]: https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event
|
|
1102
|
+
[input]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event
|
|
1103
|
+
[resize]: https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event
|
|
1104
|
+
[focus]: https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event
|
|
1105
|
+
[blur]: https://developer.mozilla.org/en-US/docs/Web/API/Window/blur_event
|
|
1106
|
+
[setup]: #setup-event
|
|
1107
|
+
[frame]: #frame-event
|
|
1108
|
+
[draw]: #draw-event
|
|
1109
|
+
|
|
1110
|
+
## App
|
|
1111
|
+
|
|
1112
|
+
The `App` global variable is a static class which does not need to be instantiated with `new`. Instead you can directly access its properties and methods on the `App` you import from the module. It allows you to access all the windows that are currently on screen, choose a frame rate for the `frame` and `draw` events, and control when the GUI event loop begins and terminates.
|
|
1113
|
+
|
|
1114
|
+
[fps]: #fps
|
|
1115
|
+
|
|
1116
|
+
##### PROPERTIES
|
|
1117
|
+
|
|
1118
|
+
#### `fps`
|
|
1119
|
+
By default, each window will attempt to update its display 60 times per second. You can reduce this by setting `App.fps` to a smaller integer value. You can raise it as well but on the majority of LCD monitors you won't see any benefit and are likely to get worse performance as you begin to swamp the CPU with your rendering code.
|
|
1120
|
+
> This setting is only relevant if you are listening for `frame` or `draw` events on your windows. Otherwise the canvas will only be updated when responding to UI interactions like keyboard and mouse events.
|
|
1121
|
+
|
|
1122
|
+
#### `running`
|
|
1123
|
+
A read-only boolean flagging whether the GUI event loop has taken control away from Node in order to display your windows.
|
|
1124
|
+
|
|
1125
|
+
#### `windows`
|
|
1126
|
+
An array of references to all of the `Window` objects that have been created and not yet [closed][close].
|
|
1127
|
+
|
|
1128
|
+
##### METHODS
|
|
1129
|
+
|
|
1130
|
+
#### `launch()`
|
|
1131
|
+
Any `Window` you create will schedule the `App` to begin running as soon as the current function returns. You can make this happen sooner by calling `App.launch` within your code. The `launch()` method will not return until the last window is closed so you may find it handy to place ‘clean up’ code after the `launch()` invocation.
|
|
1132
|
+
>Note, however, that the `App` **cannot be launched a second time** once it terminates due to limitiations in the underlying platform libraries.
|
|
1133
|
+
|
|
1134
|
+
#### `quit()`
|
|
1135
|
+
By default your process will terminate once the final window has closed. If you wish to bring things to a swifter conclusion from code, call the `App.quit()` method from one of your event handlers instead.
|
|
1136
|
+
|
|
777
1137
|
## Utilities
|
|
778
1138
|
|
|
779
1139
|
### loadImage()
|
|
@@ -1001,8 +1361,10 @@ Many thanks to the [`node-canvas`](https://github.com/Automattic/node-canvas) de
|
|
|
1001
1361
|
[strokeText()]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeText
|
|
1002
1362
|
[transform()]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform
|
|
1003
1363
|
[translate()]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/translate
|
|
1364
|
+
[reset()]: https://developer.chrome.com/blog/canvas2d/#context-reset
|
|
1365
|
+
[roundRect()]: https://developer.chrome.com/blog/canvas2d/#round-rect
|
|
1004
1366
|
|
|
1005
1367
|
[nonzero]: https://en.wikipedia.org/wiki/Nonzero-rule
|
|
1006
1368
|
[evenodd]: https://en.wikipedia.org/wiki/Even–odd_rule
|
|
1007
1369
|
|
|
1008
|
-
[glob]: https://github.com/isaacs/node-glob/blob/main/changelog.md#80
|
|
1370
|
+
[glob]: https://github.com/isaacs/node-glob/blob/main/changelog.md#80
|
package/lib/css.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// https://www.w3.org/TR/css-fonts-3/#font-size-prop
|
|
10
10
|
|
|
11
11
|
var splitBy = require('string-split-by'),
|
|
12
|
+
{DOMPoint} = require('./geometry'),
|
|
12
13
|
m, cache = {font:{}, variant:{}};
|
|
13
14
|
|
|
14
15
|
const styleRE = /^(normal|italic|oblique)$/,
|
|
@@ -158,6 +159,42 @@ function parseVariant(str){
|
|
|
158
159
|
return cache.variant[str];
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
// -- Window Types -----------------------------------------------------------------------
|
|
163
|
+
let cursorTypes = [
|
|
164
|
+
"default", "crosshair", "hand", "arrow", "move", "text", "wait", "help", "progress", "not-allowed", "context-menu",
|
|
165
|
+
"cell", "vertical-text", "alias", "copy", "no-drop", "grab", "grabbing", "all-scroll", "zoom-in", "zoom-out",
|
|
166
|
+
"e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize",
|
|
167
|
+
"ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "none"
|
|
168
|
+
]
|
|
169
|
+
function parseCursor(str){
|
|
170
|
+
return cursorTypes.includes(str)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseFit(mode){
|
|
174
|
+
return ["none", "contain-x", "contain-y", "contain", "cover", "fill", "scale-down", "resize"].includes(mode)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// -- Corner Rounding
|
|
178
|
+
// https://github.com/fserb/canvas2D/blob/master/spec/roundrect.md
|
|
179
|
+
|
|
180
|
+
function parseCornerRadii(r){
|
|
181
|
+
r = [r].flat()
|
|
182
|
+
.map(n => n instanceof DOMPoint ? n : new DOMPoint(n, n))
|
|
183
|
+
.slice(0, 4)
|
|
184
|
+
|
|
185
|
+
if (r.some(pt => !Number.isFinite(pt.x) || !Number.isFinite(pt.y))){
|
|
186
|
+
return null // silently abort
|
|
187
|
+
}else if (r.some(pt => pt.x < 0 || pt.y < 0)){
|
|
188
|
+
throw new Error("Corner radius cannot be negative")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return r.length == 1 ? [r[0], r[0], r[0], r[0]]
|
|
192
|
+
: r.length == 2 ? [r[0], r[1], r[0], r[1]]
|
|
193
|
+
: r.length == 3 ? [r[0], r[1], r[2], r[1]]
|
|
194
|
+
: r.length == 4 ? [r[0], r[1], r[2], r[3]]
|
|
195
|
+
: [0, 0, 0, 0].map(n => new DOMPoint(n, n))
|
|
196
|
+
}
|
|
197
|
+
|
|
161
198
|
// -- Image Filters -----------------------------------------------------------------------
|
|
162
199
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/filter
|
|
163
200
|
|
|
@@ -301,4 +338,7 @@ module.exports = {
|
|
|
301
338
|
variant:parseVariant,
|
|
302
339
|
size:parseSize,
|
|
303
340
|
filter:parseFilter,
|
|
341
|
+
cursor:parseCursor,
|
|
342
|
+
fit:parseFit,
|
|
343
|
+
radii:parseCornerRadii,
|
|
304
344
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/// <reference lib="dom"/>
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
|
|
4
|
-
export function loadImage(src: string | Buffer): Promise<Image>
|
|
5
4
|
export class DOMMatrix extends globalThis.DOMMatrix {}
|
|
6
5
|
export class DOMPoint extends globalThis.DOMPoint {}
|
|
7
6
|
export class DOMRect extends globalThis.DOMRect {}
|
|
8
|
-
export class Image extends globalThis.Image {}
|
|
9
|
-
export class ImageData extends globalThis.ImageData {}
|
|
10
7
|
export class CanvasGradient extends globalThis.CanvasGradient {}
|
|
11
8
|
export class CanvasPattern extends globalThis.CanvasPattern {}
|
|
12
9
|
export class CanvasTexture {}
|
|
13
10
|
|
|
11
|
+
//
|
|
12
|
+
// Images
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
export function loadImage(src: string | Buffer): Promise<Image>
|
|
16
|
+
export class ImageData extends globalThis.ImageData {}
|
|
17
|
+
export class Image extends globalThis.Image {
|
|
18
|
+
get src(): string
|
|
19
|
+
set src(src: string | Buffer)
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
//
|
|
15
23
|
// Canvas
|
|
16
24
|
//
|
|
@@ -56,6 +64,9 @@ export class Canvas {
|
|
|
56
64
|
newPage(width?: number, height?: number): CanvasRenderingContext2D
|
|
57
65
|
readonly pages: CanvasRenderingContext2D[]
|
|
58
66
|
|
|
67
|
+
get gpu(): boolean
|
|
68
|
+
set gpu(enabled: boolean)
|
|
69
|
+
|
|
59
70
|
saveAs(filename: string, options?: SaveOptions): Promise<void>
|
|
60
71
|
toBuffer(format: ExportFormat, options?: RenderOptions): Promise<Buffer>
|
|
61
72
|
toDataURL(format: ExportFormat, options?: RenderOptions): Promise<string>
|
|
@@ -117,6 +128,8 @@ interface CanvasFillStrokeStyles {
|
|
|
117
128
|
type QuadOrRect = [x1:number, y1:number, x2:number, y2:number, x3:number, y3:number, x4:number, y4:number] |
|
|
118
129
|
[left:number, top:number, right:number, bottom:number] | [width:number, height:number]
|
|
119
130
|
|
|
131
|
+
type CornerRadius = number | DOMPoint
|
|
132
|
+
|
|
120
133
|
export interface CanvasRenderingContext2D extends CanvasCompositing, CanvasDrawImage, CanvasDrawPath, CanvasFillStrokeStyles, CanvasFilters, CanvasImageData, CanvasImageSmoothing, CanvasPath, CanvasPathDrawingStyles, CanvasRect, CanvasShadowStyles, CanvasState, CanvasText, CanvasTextDrawingStyles, CanvasTransform, CanvasUserInterface {
|
|
121
134
|
readonly canvas: Canvas;
|
|
122
135
|
fontVariant: string;
|
|
@@ -130,12 +143,15 @@ export interface CanvasRenderingContext2D extends CanvasCompositing, CanvasDrawI
|
|
|
130
143
|
createProjection(quad: QuadOrRect, basis?: QuadOrRect): DOMMatrix
|
|
131
144
|
|
|
132
145
|
conicCurveTo(cpx: number, cpy: number, x: number, y: number, weight: number): void
|
|
146
|
+
roundRect(x: number, y: number, width: number, height: number, radii: number | CornerRadius[])
|
|
133
147
|
// getContextAttributes(): CanvasRenderingContext2DSettings;
|
|
134
148
|
|
|
135
149
|
fillText(text: string, x: number, y:number, maxWidth?: number): void
|
|
136
150
|
strokeText(text: string, x: number, y:number, maxWidth?: number): void
|
|
137
151
|
measureText(text: string, maxWidth?: number): TextMetrics
|
|
138
152
|
outlineText(text: string): Path2D
|
|
153
|
+
|
|
154
|
+
reset(): void
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
//
|
|
@@ -167,6 +183,8 @@ export class Path2D extends globalThis.Path2D {
|
|
|
167
183
|
weight: number
|
|
168
184
|
): void
|
|
169
185
|
|
|
186
|
+
roundRect(x: number, y: number, width: number, height: number, radii: number | CornerRadius[])
|
|
187
|
+
|
|
170
188
|
complement(otherPath: Path2D): Path2D
|
|
171
189
|
difference(otherPath: Path2D): Path2D
|
|
172
190
|
intersect(otherPath: Path2D): Path2D
|
|
@@ -234,3 +252,61 @@ export interface FontLibrary {
|
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
export const FontLibrary: FontLibrary
|
|
255
|
+
|
|
256
|
+
//
|
|
257
|
+
// Window & App
|
|
258
|
+
//
|
|
259
|
+
|
|
260
|
+
import { EventEmitter } from "stream";
|
|
261
|
+
export type FitStyle = "none" | "contain-x" | "contain-y" | "contain" | "cover" | "fill" | "scale-down" | "resize"
|
|
262
|
+
export type CursorStyle = "default" | "crosshair" | "hand" | "arrow" | "move" | "text" | "wait" | "help" | "progress" | "not-allowed" | "context-menu" |
|
|
263
|
+
"cell" | "vertical-text" | "alias" | "copy" | "no-drop" | "grab" | "grabbing" | "all-scroll" | "zoom-in" | "zoom-out" |
|
|
264
|
+
"e-resize" | "n-resize" | "ne-resize" | "nw-resize" | "s-resize" | "se-resize" | "sw-resize" | "w-resize" | "ew-resize" |
|
|
265
|
+
"ns-resize" | "nesw-resize" | "nwse-resize" | "col-resize" | "row-resize" | "none"
|
|
266
|
+
|
|
267
|
+
export type WindowOptions = {
|
|
268
|
+
title?: string
|
|
269
|
+
left?: number
|
|
270
|
+
top?: number
|
|
271
|
+
width?: number
|
|
272
|
+
height?: number
|
|
273
|
+
fit?: FitStyle
|
|
274
|
+
page?: number
|
|
275
|
+
background?: string
|
|
276
|
+
fullscreen?: boolean
|
|
277
|
+
visible?: boolean
|
|
278
|
+
cursor?: CursorStyle
|
|
279
|
+
canvas?: Canvas
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export class Window extends EventEmitter{
|
|
283
|
+
constructor(width: number, height: number, options?: WindowOptions)
|
|
284
|
+
constructor(options?: WindowOptions)
|
|
285
|
+
|
|
286
|
+
readonly ctx: CanvasRenderingContext2D
|
|
287
|
+
canvas: Canvas
|
|
288
|
+
visible: boolean
|
|
289
|
+
fullscreen: boolean
|
|
290
|
+
title: string
|
|
291
|
+
cursor: CursorStyle
|
|
292
|
+
fit: FitStyle
|
|
293
|
+
left: number
|
|
294
|
+
top: number
|
|
295
|
+
width: number
|
|
296
|
+
height: number
|
|
297
|
+
page: number
|
|
298
|
+
background: string
|
|
299
|
+
|
|
300
|
+
close(): void
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface App{
|
|
304
|
+
readonly windows: Window[]
|
|
305
|
+
readonly running: boolean
|
|
306
|
+
fps: number
|
|
307
|
+
|
|
308
|
+
launch(): void
|
|
309
|
+
quit(): void
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const App: App
|
package/lib/index.js
CHANGED
|
@@ -45,9 +45,9 @@ class RustClass{
|
|
|
45
45
|
return arguments.length > 1 ? this[Symbol.for(key)] = val : this[Symbol.for(key)]
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
prop(attr,
|
|
48
|
+
prop(attr, ...vals){
|
|
49
49
|
let getset = arguments.length > 1 ? 'set' : 'get'
|
|
50
|
-
return this.native[attr][getset](this[ø],
|
|
50
|
+
return this.native[attr][getset](this[ø], ...vals)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
ƒ(fn, ...args){
|
|
@@ -97,44 +97,328 @@ function fromSkMatrix(skMatrix){
|
|
|
97
97
|
0, 0, 1, 0,
|
|
98
98
|
c, f, 0, p2
|
|
99
99
|
])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//
|
|
103
|
+
// Windows & Event Handling
|
|
104
|
+
//
|
|
105
|
+
|
|
106
|
+
class App extends RustClass{
|
|
107
|
+
#running
|
|
108
|
+
#fps
|
|
109
|
+
|
|
110
|
+
constructor(){
|
|
111
|
+
super(App)
|
|
112
|
+
this.#running = false
|
|
113
|
+
this.#fps = 60
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get windows(){ return [...GUI.windows] }
|
|
117
|
+
get running(){ return this.#running }
|
|
118
|
+
get fps(){ return this.#fps }
|
|
119
|
+
set fps(rate){
|
|
120
|
+
if (rate >= 1 && rate != this.#fps){
|
|
121
|
+
this.#fps = this.ƒ('setRate', rate)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
launch(){
|
|
126
|
+
if (this.#running) return console.error('Application is already running')
|
|
127
|
+
this.#running = true
|
|
128
|
+
clearTimeout(GUI.launcher)
|
|
129
|
+
|
|
130
|
+
// begin event loop (and never return)
|
|
131
|
+
this.ƒ("launch", args => {
|
|
132
|
+
let {ui, state, geom} = JSON.parse(args)
|
|
133
|
+
|
|
134
|
+
// in the initial roundtrip only, merge the autogenerated window locations with the specs
|
|
135
|
+
for (const [id, {top, left}] of Object.entries(geom || {})){
|
|
136
|
+
GUI.getWindow(id, win => {
|
|
137
|
+
win.left = win.left || left
|
|
138
|
+
win.top = win.top || top
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// update local state based on ui modifications (and evict GUI.windows that have been closed)
|
|
143
|
+
if (state) GUI.windows = GUI.windows.filter(win => {
|
|
144
|
+
return win.state.id in (state || {}) && Object.assign(win, state[win.state.id])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// deliver ui events to corresponding windows
|
|
148
|
+
for (const [id, events] of Object.entries(ui || {})){
|
|
149
|
+
GUI.getWindow(id, (win, frame) => {
|
|
150
|
+
let modifiers = {}
|
|
151
|
+
for (const [[type, e]] of events.map(o => Object.entries(o))){
|
|
152
|
+
switch(type){
|
|
153
|
+
case 'modifiers':
|
|
154
|
+
let {ctrl:ctrlKey, alt:altKey, logo:metaKey, shift:shiftKey} = e
|
|
155
|
+
modifiers = {ctrlKey, altKey, metaKey, shiftKey}
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
case 'mouse':
|
|
159
|
+
let {button, x, y, pageX, pageY} = e
|
|
160
|
+
e.events.forEach(type => win.emit(type, {x, y, pageX, pageY, button, ...modifiers}))
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
case 'input':
|
|
164
|
+
win.emit(type, {value:e, code:e.charCodeAt(), ...modifiers})
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
case 'keyboard':
|
|
168
|
+
let {event, key, code, repeat} = e,
|
|
169
|
+
defaults = true;
|
|
170
|
+
|
|
171
|
+
win.emit(event, {key, code, repeat, ...modifiers,
|
|
172
|
+
preventDefault:() => defaults = false
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// apply default keybindings unless e.preventDefault() was run
|
|
176
|
+
if (defaults && event=='keydown' && !repeat){
|
|
177
|
+
let {ctrlKey, altKey, metaKey} = modifiers
|
|
178
|
+
if ( (metaKey && key=='W') || (ctrlKey && key=='C') || (altKey && key=='F4') ){
|
|
179
|
+
win.close()
|
|
180
|
+
}else if ( (metaKey && key=='F') || (altKey && key=='F8') ){
|
|
181
|
+
win.fullscreen = !win.fullscreen
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
case 'focus':
|
|
187
|
+
if (e) win.emit('focus')
|
|
188
|
+
else win.emit('blur')
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
case 'resize':
|
|
192
|
+
if (win.fit == 'resize'){
|
|
193
|
+
win.ctx.prop('size', e.width, e.height)
|
|
194
|
+
win.canvas.prop('width', e.width)
|
|
195
|
+
win.canvas.prop('height', e.height)
|
|
196
|
+
}
|
|
197
|
+
win.emit(type, e)
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
case 'move':
|
|
201
|
+
case 'wheel':
|
|
202
|
+
win.emit(type, e)
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
console.log(type, e);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// provide frame updates to prompt redraws
|
|
213
|
+
GUI.nextFrame((win, frame) => {
|
|
214
|
+
if (frame==0) win.emit("setup")
|
|
215
|
+
win.emit("frame", {frame})
|
|
216
|
+
if (win.listenerCount('draw')){
|
|
217
|
+
win.canvas.width = win.canvas.width
|
|
218
|
+
win.emit("draw", {frame})
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// refresh lazily if not doing a flipbook animation
|
|
223
|
+
this.ƒ('setRate', GUI.needsFrameUpdates() ? this.#fps : 0)
|
|
224
|
+
|
|
225
|
+
// update the display
|
|
226
|
+
return [
|
|
227
|
+
JSON.stringify( GUI.windows.map(win => win.state) ),
|
|
228
|
+
GUI.windows.map(win => core(win.canvas.pages[win.page-1]) )
|
|
229
|
+
]
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
GUI.windows = [] // if the launch call exited, the last window was closed
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
quit(){
|
|
236
|
+
this.ƒ("quit")
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
class Window extends EventEmitter{
|
|
241
|
+
static #kwargs = "left,top,width,height,title,page,background,fullscreen,cursor,fit,visible".split(/,/)
|
|
242
|
+
#canvas
|
|
243
|
+
#state
|
|
244
|
+
|
|
245
|
+
// accept either ƒ(width, height, {…}) or ƒ({…})
|
|
246
|
+
constructor(width=512, height=512, opts={}){
|
|
247
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)){
|
|
248
|
+
opts = [...arguments].slice(-1)[0] || {}
|
|
249
|
+
width = opts.width || (opts.canvas || {}).width || 512
|
|
250
|
+
height = opts.height || (opts.canvas || {}).height || 512
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let canvas = (opts.canvas instanceof Canvas) ? opts.canvas : new Canvas(width, height)
|
|
254
|
+
|
|
255
|
+
super(Window)
|
|
256
|
+
this.#state = {
|
|
257
|
+
title: "",
|
|
258
|
+
visible: true,
|
|
259
|
+
background: "white",
|
|
260
|
+
fullscreen: false,
|
|
261
|
+
page: canvas.pages.length,
|
|
262
|
+
left: undefined,
|
|
263
|
+
top: undefined,
|
|
264
|
+
width,
|
|
265
|
+
height,
|
|
266
|
+
cursor: "default",
|
|
267
|
+
cursorHidden: false,
|
|
268
|
+
fit: "contain",
|
|
269
|
+
id: Math.random().toString(16)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Object.assign(this, {canvas}, Object.fromEntries(
|
|
273
|
+
Object.entries(opts).filter(([k, v]) => Window.#kwargs.includes(k) && v!==undefined)
|
|
274
|
+
))
|
|
275
|
+
|
|
276
|
+
GUI.openWindow(this)
|
|
277
|
+
}
|
|
100
278
|
|
|
279
|
+
get state(){ return this.#state }
|
|
280
|
+
get ctx(){ return this.#canvas.pages[this.page-1] }
|
|
281
|
+
|
|
282
|
+
get canvas(){ return this.#canvas }
|
|
283
|
+
set canvas(canvas){
|
|
284
|
+
if (canvas instanceof Canvas){
|
|
285
|
+
canvas.getContext("2d") // ensure it has at least one page
|
|
286
|
+
this.#canvas = canvas
|
|
287
|
+
this.#state.page = canvas.pages.length
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get visible(){ return this.#state.visible }
|
|
292
|
+
set visible(flag){ this.#state.visible = !!flag }
|
|
293
|
+
|
|
294
|
+
get fullscreen(){ return this.#state.fullscreen }
|
|
295
|
+
set fullscreen(flag){ this.#state.fullscreen = !!flag }
|
|
296
|
+
|
|
297
|
+
get title(){ return this.#state.title }
|
|
298
|
+
set title(txt){ this.#state.title = (txt != null ? txt : '').toString() }
|
|
299
|
+
|
|
300
|
+
get cursor(){ return this.#state.cursorHidden ? 'none' : this.#state.cursor }
|
|
301
|
+
set cursor(icon){
|
|
302
|
+
if (css.cursor(icon)){
|
|
303
|
+
this.#state.cursorHidden = icon == 'none'
|
|
304
|
+
if (icon != 'none') this.#state.cursor = icon
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
get fit(){ return this.#state.fit }
|
|
309
|
+
set fit(mode){ if (css.fit(mode)) this.#state.fit = mode }
|
|
310
|
+
|
|
311
|
+
get left(){ return this.#state.left }
|
|
312
|
+
set left(val){ if (Number.isFinite(val)) this.#state.left = val }
|
|
313
|
+
|
|
314
|
+
get top(){ return this.#state.top }
|
|
315
|
+
set top(val){ if (Number.isFinite(val)) this.#state.top = val }
|
|
316
|
+
|
|
317
|
+
get width(){ return this.#state.width }
|
|
318
|
+
set width(val){ if (Number.isFinite(val)) this.#state.width = val }
|
|
319
|
+
|
|
320
|
+
get height(){ return this.#state.height }
|
|
321
|
+
set height(val){ if (Number.isFinite(val)) this.#state.height = val }
|
|
322
|
+
|
|
323
|
+
get page(){ return this.#state.page }
|
|
324
|
+
set page(val){
|
|
325
|
+
if (val < 0) val += this.#canvas.pages.length + 1
|
|
326
|
+
let page = this.#canvas.pages[val-1]
|
|
327
|
+
if (page && this.#state.page != val){
|
|
328
|
+
let [width, height] = page.prop('size')
|
|
329
|
+
this.#canvas.prop('width', width)
|
|
330
|
+
this.#canvas.prop('height', height)
|
|
331
|
+
this.#state.page = val
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
get background(){ return this.#state.background }
|
|
336
|
+
set background(c){ this.#state.background = (c != null ? c : '').toString() }
|
|
337
|
+
|
|
338
|
+
emit(type, e){
|
|
339
|
+
// report errors in event-handlers but don't crash
|
|
340
|
+
try{ super.emit(type, Object.assign({target:this, type}, e)) }
|
|
341
|
+
catch(err){ console.error(err) }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
close(){ GUI.closeWindow(this) }
|
|
345
|
+
|
|
346
|
+
[REPR](depth, options) {
|
|
347
|
+
let info = Object.fromEntries(Window.#kwargs.map(k => [k, this.#state[k]]))
|
|
348
|
+
return `Window ${inspect(info, options)}`
|
|
349
|
+
}
|
|
101
350
|
}
|
|
102
351
|
|
|
352
|
+
const GUI = {
|
|
353
|
+
App: new App(),
|
|
354
|
+
windows: [],
|
|
355
|
+
frames: new WeakMap(),
|
|
356
|
+
launcher: null,
|
|
357
|
+
|
|
358
|
+
nextFrame(callback){
|
|
359
|
+
GUI.windows.forEach(win => {
|
|
360
|
+
let frame = GUI.frames.get(win) || 0
|
|
361
|
+
GUI.frames.set(win, frame + 1)
|
|
362
|
+
callback(win, frame)
|
|
363
|
+
})
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
needsFrameUpdates(){
|
|
367
|
+
let names = GUI.windows.map(win => win.eventNames()).flat()
|
|
368
|
+
return (names.includes('frame') || names.includes('draw'))
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
getWindow(id, callback){
|
|
372
|
+
GUI.windows.filter(w => w.state.id==id).forEach(win => callback(win))
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
openWindow(win){
|
|
376
|
+
GUI.windows.push(win)
|
|
377
|
+
if (!GUI.launcher) GUI.launcher = setTimeout( () => GUI.App.launch() )
|
|
378
|
+
neon.App.openWindow(JSON.stringify(win.state), core(win.canvas.pages[win.state.page-1]))
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
closeWindow(win){
|
|
382
|
+
GUI.windows = GUI.windows.filter(w => w !== win)
|
|
383
|
+
neon.App.closeWindow(win.state.id)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
103
386
|
|
|
104
387
|
//
|
|
105
388
|
// The Canvas API
|
|
106
389
|
//
|
|
107
390
|
|
|
108
391
|
class Canvas extends RustClass{
|
|
109
|
-
|
|
110
|
-
static contexts = new WeakMap()
|
|
392
|
+
#contexts
|
|
111
393
|
|
|
112
394
|
constructor(width, height){
|
|
113
395
|
super(Canvas).alloc()
|
|
114
|
-
|
|
396
|
+
this.#contexts = []
|
|
115
397
|
Object.assign(this, {width, height})
|
|
116
398
|
}
|
|
117
399
|
|
|
118
400
|
getContext(kind){
|
|
119
|
-
return (kind=="2d") ?
|
|
401
|
+
return (kind=="2d") ? this.#contexts[0] || this.newPage() : null
|
|
120
402
|
}
|
|
121
403
|
|
|
404
|
+
get gpu(){ return this.prop('engine')=='gpu' }
|
|
405
|
+
set gpu(mode){ this.prop('engine', !!mode ? 'gpu' : 'cpu') }
|
|
406
|
+
|
|
122
407
|
get width(){ return this.prop('width') }
|
|
123
408
|
set width(w){
|
|
124
409
|
this.prop('width', (typeof w=='number' && !Number.isNaN(w) && w>=0) ? w : 300)
|
|
125
|
-
if (
|
|
410
|
+
if (this.#contexts[0]) this.getContext("2d").ƒ('resetSize', core(this))
|
|
126
411
|
}
|
|
127
412
|
|
|
128
413
|
get height(){ return this.prop('height') }
|
|
129
414
|
set height(h){
|
|
130
415
|
this.prop('height', h = (typeof h=='number' && !Number.isNaN(h) && h>=0) ? h : 150)
|
|
131
|
-
if (
|
|
416
|
+
if (this.#contexts[0]) this.getContext("2d").ƒ('resetSize', core(this))
|
|
132
417
|
}
|
|
133
418
|
|
|
134
419
|
newPage(width, height){
|
|
135
|
-
let ctx = new CanvasRenderingContext2D(
|
|
136
|
-
|
|
137
|
-
Canvas.contexts.get(this).unshift(ctx)
|
|
420
|
+
let ctx = new CanvasRenderingContext2D(this)
|
|
421
|
+
this.#contexts.unshift(ctx)
|
|
138
422
|
if (arguments.length==2){
|
|
139
423
|
Object.assign(this, {width, height})
|
|
140
424
|
}
|
|
@@ -142,7 +426,7 @@ class Canvas extends RustClass{
|
|
|
142
426
|
}
|
|
143
427
|
|
|
144
428
|
get pages(){
|
|
145
|
-
return
|
|
429
|
+
return this.#contexts.slice().reverse()
|
|
146
430
|
}
|
|
147
431
|
|
|
148
432
|
get png(){ return this.toBuffer("png") }
|
|
@@ -206,8 +490,8 @@ class Canvas extends RustClass{
|
|
|
206
490
|
|
|
207
491
|
|
|
208
492
|
[REPR](depth, options) {
|
|
209
|
-
let {width, height, async, pages} = this
|
|
210
|
-
return `Canvas ${inspect({width, height, async, pages}, options)}`
|
|
493
|
+
let {width, height, async, gpu, pages} = this
|
|
494
|
+
return `Canvas ${inspect({width, height, async, gpu, pages}, options)}`
|
|
211
495
|
}
|
|
212
496
|
}
|
|
213
497
|
|
|
@@ -268,17 +552,23 @@ class CanvasTexture extends RustClass{
|
|
|
268
552
|
}
|
|
269
553
|
}
|
|
270
554
|
|
|
555
|
+
|
|
271
556
|
class CanvasRenderingContext2D extends RustClass{
|
|
557
|
+
#canvas
|
|
558
|
+
|
|
272
559
|
constructor(canvas){
|
|
273
560
|
try{
|
|
274
|
-
super(CanvasRenderingContext2D).alloc(canvas)
|
|
561
|
+
super(CanvasRenderingContext2D).alloc(core(canvas))
|
|
562
|
+
this.#canvas = new WeakRef(canvas)
|
|
275
563
|
}catch(e){
|
|
276
564
|
throw new TypeError(`Function is not a constructor (use Canvas's "getContext" method instead)`)
|
|
277
565
|
}
|
|
278
566
|
}
|
|
279
567
|
|
|
280
|
-
get canvas(){ return
|
|
568
|
+
get canvas(){ return this.#canvas.deref() }
|
|
281
569
|
|
|
570
|
+
// -- global state & content reset ------------------------------------------
|
|
571
|
+
reset(){ this.ƒ('reset') }
|
|
282
572
|
|
|
283
573
|
// -- grid state ------------------------------------------------------------
|
|
284
574
|
save(){ this.ƒ('save') }
|
|
@@ -317,6 +607,16 @@ class CanvasRenderingContext2D extends RustClass{
|
|
|
317
607
|
isPointInPath(x, y){ return this.ƒ('isPointInPath', ...arguments) }
|
|
318
608
|
isPointInStroke(x, y){ return this.ƒ('isPointInStroke', ...arguments) }
|
|
319
609
|
|
|
610
|
+
roundRect(x, y, w, h, r){
|
|
611
|
+
let radii = css.radii(r)
|
|
612
|
+
if (radii){
|
|
613
|
+
if (w < 0) radii = [radii[1], radii[0], radii[3], radii[2]]
|
|
614
|
+
if (h < 0) radii = [radii[3], radii[2], radii[1], radii[0]]
|
|
615
|
+
this.ƒ("roundRect", x, y, w, h, ...radii.map(({x, y}) => [x, y]).flat())
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
|
|
320
620
|
// -- using paths -----------------------------------------------------------
|
|
321
621
|
fill(path, rule){
|
|
322
622
|
if (path instanceof Path2D) this.ƒ('fill', core(path), rule)
|
|
@@ -685,6 +985,14 @@ class Path2D extends RustClass{
|
|
|
685
985
|
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, isCCW){ this.ƒ("ellipse", ...arguments) }
|
|
686
986
|
rect(x, y, width, height){this.ƒ("rect", ...arguments) }
|
|
687
987
|
arc(x, y, radius, startAngle, endAngle){ this.ƒ("arc", ...arguments) }
|
|
988
|
+
roundRect(x, y, w, h, r){
|
|
989
|
+
let radii = css.radii(r)
|
|
990
|
+
if (radii){
|
|
991
|
+
if (w < 0) radii = [radii[1], radii[0], radii[3], radii[2]]
|
|
992
|
+
if (h < 0) radii = [radii[3], radii[2], radii[1], radii[0]]
|
|
993
|
+
this.ƒ("roundRect", x, y, w, h, ...radii.map(({x, y}) => [x, y]).flat())
|
|
994
|
+
}
|
|
995
|
+
}
|
|
688
996
|
|
|
689
997
|
// tween similar paths
|
|
690
998
|
interpolate(path, weight){ return Path2D.interpolate(this, path, weight) }
|
|
@@ -752,6 +1060,6 @@ const loadImage = src => Object.assign(new Image(), {src}).decode()
|
|
|
752
1060
|
|
|
753
1061
|
module.exports = {
|
|
754
1062
|
Canvas, CanvasGradient, CanvasPattern, CanvasRenderingContext2D, CanvasTexture,
|
|
755
|
-
TextMetrics, Image, ImageData, Path2D, loadImage, ...geometry,
|
|
756
|
-
FontLibrary:new FontLibrary()
|
|
757
|
-
}
|
|
1063
|
+
TextMetrics, Image, ImageData, Path2D, Window, loadImage, ...geometry,
|
|
1064
|
+
FontLibrary: new FontLibrary(), App: GUI.App
|
|
1065
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skia-canvas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A GPU-accelerated Canvas Graphics API for Node",
|
|
5
5
|
"author": "Christian Swinehart <drafting@samizdat.co>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"string-split-by": "^1.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@types/jest": "^
|
|
37
|
-
"@types/node": "^
|
|
38
|
-
"aws-sdk": "^2.
|
|
36
|
+
"@types/jest": "^28.1.6",
|
|
37
|
+
"@types/node": "^18.6.1",
|
|
38
|
+
"aws-sdk": "^2.1181.0",
|
|
39
39
|
"express": "^4.18.1",
|
|
40
|
-
"jest": "^28.1.
|
|
40
|
+
"jest": "^28.1.3",
|
|
41
41
|
"lodash": "^4.17.21",
|
|
42
|
-
"nodemon": "^2.0.
|
|
42
|
+
"nodemon": "^2.0.19",
|
|
43
43
|
"tmp": "^0.2.1"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
@@ -56,8 +56,9 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
|
-
"skia",
|
|
60
59
|
"canvas",
|
|
60
|
+
"gpu",
|
|
61
|
+
"skia",
|
|
61
62
|
"offscreen",
|
|
62
63
|
"headless",
|
|
63
64
|
"graphic",
|
|
@@ -66,6 +67,8 @@
|
|
|
66
67
|
"images",
|
|
67
68
|
"compositing",
|
|
68
69
|
"render",
|
|
70
|
+
"vulkan",
|
|
71
|
+
"metal",
|
|
69
72
|
"pdf",
|
|
70
73
|
"svg",
|
|
71
74
|
"rust"
|