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 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/v0.9.30...HEAD
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
- # Skia Canvas
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 as a result produces very similar results to Chrome’s `<canvas>` element.
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 that are more relevant to the generation of static graphics files rather than ‘live’ display in a browser.
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 generate output in both raster (JPEG & PNG) and vector (PDF & SVG) image formats
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
- - can apply [3D perspective][createProjection()] transformations in addition to [scaling][scale()], [rotation][rotate()], and [translation][translate()]
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
- - fully supports the [CSS filter effects][filter] image processing operators
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.24 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.
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, loadImage} = require('skia-canvas'),
92
- rand = n => Math.floor(n * Math.random()),
93
- fs = require('fs')
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
- let canvas = new Canvas(600, 600),
96
- ctx = canvas.getContext("2d"),
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
- // draw a sea of blurred dots filling the canvas
100
- ctx.filter = 'blur(12px) hue-rotate(20deg)'
101
- for (let i=0; i<800; i++){
102
- ctx.fillStyle = `hsl(${rand(40)}deg, 80%, 50%)`
103
- ctx.beginPath()
104
- ctx.arc(rand(width), rand(height), rand(20)+5, 0, 2*Math.PI)
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 the graphic...
124
- await canvas.saveAs("pilcrow.png")
125
- // ...or use a shorthand for canvas.toBuffer("png")
126
- let pngData = await canvas.png
127
- // ...or embed it in a string
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] | [**pages**][canvas_pages] ⚡ | ~~[**async**][canvas_async]~~ ⚡ |
167
- | [**height**][canvas_height] | [getContext()][getContext] | [**pdf**, **png**, **svg**, **jpg**][shorthands] ⚡ |
168
- | | [newPage()][newPage] ⚡ | [saveAs()][saveAs] / [saveAsSync()][saveAs] ⚡ |
169
- | | | [toBuffer()][toBuffer] / [toBufferSync()][toBuffer] ⚡ |
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
- | [isPointInPath()][isPointInPath()] | [strokeRect()][strokeRect()] | [createConicGradient()][createConicGradient()] | [**lineDashMarker** ⚡][lineDashMarker] | [getTransform()][getTransform()] | [arcTo()][arcTo()] | [**fontVariant** ⚡](#fontvariant) | [createImageData()][createImageData()] | [**globalCompositeOperation**][globalCompositeOperation] |
295
- | [isPointInStroke()][isPointInStroke()] | [fillText()][fillText()] ⧸[⚡][drawText] | [createLinearGradient()][createLinearGradient()] | [**lineDashOffset**][lineDashOffset] | [setTransform()][setTransform()] | [bezierCurveTo()][bezierCurveTo()] | [**textAlign**][textAlign] | [getImageData()][getImageData()] | [**shadowBlur**][shadowBlur] |
296
- | [save()][save()] | [strokeText()][strokeText()] ⧸[⚡][drawText] | [createRadialGradient()][createRadialGradient()] | [**lineJoin**][lineJoin] | [resetTransform()][resetTransform()] | [conicCurveTo() ⚡][conicCurveTo] | [**textBaseline**][textBaseline] | [putImageData()][putImageData()] | [**shadowColor**][shadowColor] |
297
- | [restore()][restore()] | [fill()][fill()] | [createPattern()][createPattern()] | [**lineWidth**][lineWidth] | [transform()][transform()] | [quadraticCurveTo()][quadraticCurveTo()] | [**textTracking** ⚡](#texttracking) | [drawCanvas() ⚡](#drawcanvascanvas-x-y-) | [**shadowOffsetX**][shadowOffsetX] |
298
- | [clip()][clip()] | [stroke()][stroke()] | [createTexture() ⚡][createTexture()] | [**miterLimit**][miterLimit] | [translate()][translate()] | [closePath()][closePath()] | [**textWrap** ⚡](#textwrap) | [drawImage()][drawImage()] | [**shadowOffsetY**][shadowOffsetY] |
299
- | | | | [getLineDash()][getLineDash()] | [rotate()][rotate()] | [arc()][arc()] | [measureText()][measureText()] ⧸[⚡](#measuretextstr-width) | | |
300
- | | | | [setLineDash()][setLineDash()] | [scale()][scale()] | [ellipse()][ellipse()] | [outlineText() ⚡][outlineText()] | | |
301
- | | | | | | [rect()][rect()] | | |
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 | Boolean Ops ⚡ | Filters ⚡ | Geometry ⚡ |
544
- | -- | -- | -- | -- | -- |
545
- | [**d** ⚡](#d) | [addPath()][p2d_addPath] | [complement()][bool-ops] | [interpolate()][p2d_interpolate] | [**bounds**](#bounds) |
546
- | [moveTo()][p2d_moveTo] | [arc()][p2d_arc] | [difference()][bool-ops] | [jitter()][p2d_jitter] | [**edges**](#edges) |
547
- | [lineTo()][p2d_lineTo] | [arcTo()][p2d_arcTo] | [intersect()][bool-ops] | [round()][p2d_round] | [contains()][p2d_contains] |
548
- | [bezierCurveTo()][p2d_bezierCurveTo] | [ellipse()][p2d_ellipse] | [union()][bool-ops] | [simplify()][p2d_simplify] | [points()][p2d_points] |
549
- | [conicCurveTo() ⚡][conicCurveTo] | [rect()][p2d_rect] | [xor()][bool-ops] | [trim()][p2d_trim] | [offset()][p2d_offset] |
550
- | [quadraticCurveTo()][p2d_quadraticCurveTo] | | | [unwind()][p2d_unwind] | [transform()][p2d_transform] |
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
  ![convert winding rule subpaths](/test/assets/path/effect-unwind@2x.png)
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, val){
48
+ prop(attr, ...vals){
49
49
  let getset = arguments.length > 1 ? 'set' : 'get'
50
- return this.native[attr][getset](this[ø], val)
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
- static parent = new WeakMap()
110
- static contexts = new WeakMap()
392
+ #contexts
111
393
 
112
394
  constructor(width, height){
113
395
  super(Canvas).alloc()
114
- Canvas.contexts.set(this, [])
396
+ this.#contexts = []
115
397
  Object.assign(this, {width, height})
116
398
  }
117
399
 
118
400
  getContext(kind){
119
- return (kind=="2d") ? Canvas.contexts.get(this)[0] || this.newPage() : null
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 (Canvas.contexts.get(this)[0]) this.getContext("2d").ƒ('resetSize', core(this))
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 (Canvas.contexts.get(this)[0]) this.getContext("2d").ƒ('resetSize', core(this))
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(core(this))
136
- Canvas.parent.set(ctx, this)
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 Canvas.contexts.get(this).slice().reverse()
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 Canvas.parent.get(this) }
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.9.30",
4
- "description": "A canvas environment for Node",
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": "^27.5.1",
37
- "@types/node": "^17.0.38",
38
- "aws-sdk": "^2.1146.0",
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.0",
40
+ "jest": "^28.1.3",
41
41
  "lodash": "^4.17.21",
42
- "nodemon": "^2.0.16",
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"