@effing/skia 1.0.9-effing.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 lynweklm@gmail.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # `@effing/skia`
2
+
3
+ [![CI](https://github.com/builtbyfew/effing-skia/actions/workflows/CI.yaml/badge.svg)](https://github.com/builtbyfew/effing-skia/actions/workflows/CI.yaml)
4
+
5
+ Effing's fork of [`@napi-rs/canvas`](https://github.com/napi-rs/canvas), the Skia-backed Canvas for Node.js.
6
+
7
+ - The main entry is a drop-in for `@napi-rs/canvas`: same classes, same context, same types.
8
+ - `@effing/skia/extensions` adds a native `Paragraph` primitive, painted with `fillParagraph` /
9
+ `strokeParagraph`, and compositing groups with `beginGroup` / `endGroup`.
10
+ - Under `textRendering = 'geometricPrecision'`, text is laid out unhinted and painted unsnapped, so it
11
+ lands in the same place at any raster scale.
12
+
13
+ ```bash
14
+ npm install @effing/skia
15
+ ```
16
+
17
+ Prebuilt binaries are published for Linux x64 and arm64 (glibc and musl), macOS x64 and arm64, and
18
+ Windows x64.
19
+
20
+ [docs/effing.md](./docs/effing.md) documents the extensions, where the fork's code lives, how it hooks
21
+ into upstream, and how releases work.
22
+
23
+ All credit for the canvas itself goes to [`@napi-rs/canvas`](https://github.com/napi-rs/canvas) and its
24
+ author. Upstream's README follows unchanged; wherever it says `@napi-rs/canvas`, `@effing/skia` works
25
+ the same way, on the platforms listed above.
26
+
27
+ ---
28
+
29
+ # `skr canvas`
30
+
31
+ [![CI](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml/badge.svg)](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml)
32
+ ![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm154-hotpink)
33
+ [![install size](https://packagephobia.com/badge?p=@napi-rs/canvas)](https://packagephobia.com/result?p=@napi-rs/canvas)
34
+ [![Downloads](https://img.shields.io/npm/dm/@napi-rs/canvas.svg?sanitize=true)](https://npmcharts.com/compare/@napi-rs/canvas?minimal=true)
35
+
36
+ > 🚀 Help me to become a full-time open-source developer by [sponsoring me on Github](https://github.com/sponsors/Brooooooklyn)
37
+
38
+ Google Skia binding to Node.js via [Node-API](https://napi.rs), **0 System dependencies!**
39
+
40
+ [中文文档](./README-zh.md)
41
+
42
+ # Install
43
+
44
+ ```bash
45
+ yarn add @napi-rs/canvas
46
+ npm install @napi-rs/canvas
47
+ ```
48
+
49
+ # Support matrix
50
+
51
+ ## System requirement
52
+
53
+ ### `arm64`
54
+
55
+ [**_cortex-a57_**](https://en.wikipedia.org/wiki/ARM_Cortex-A57) or newer CPU architecture on **Linux**.
56
+
57
+ All Apple M chips on **macOS**.
58
+
59
+ ### `armv7`
60
+
61
+ [**_cortex-a7_**](https://en.wikipedia.org/wiki/ARM_Cortex-A7) or newer CPU architecture.
62
+
63
+ ### glibc
64
+
65
+ Since Skia relies on the [glibc](https://www.gnu.org/software/libc/) 2.18 API, you need to have at least glibc version >= 2.18 on your system.
66
+
67
+ ## AWS Lambda usage
68
+
69
+ To use this library on Lambda you will need to use a Lambda layer.
70
+
71
+ You can simply attach a lambda layer by getting an ARN from [Canvas-Lambda-Layer](https://github.com/ShivamJoker/Canvas-Lambda-Layer)
72
+
73
+ > Make sure to exclude `@napi-rs/canvas` while bundling your Lambda.
74
+
75
+ # Usage
76
+
77
+ ```js
78
+ const { promises } = require('node:fs')
79
+ const { join } = require('node:path')
80
+ const { createCanvas, loadImage } = require('@napi-rs/canvas')
81
+
82
+ const canvas = createCanvas(300, 320)
83
+ const ctx = canvas.getContext('2d')
84
+
85
+ ctx.lineWidth = 10
86
+ ctx.strokeStyle = '#03a9f4'
87
+ ctx.fillStyle = '#03a9f4'
88
+
89
+ // Wall
90
+ ctx.strokeRect(75, 140, 150, 110)
91
+
92
+ // Door
93
+ ctx.fillRect(130, 190, 40, 60)
94
+
95
+ // Roof
96
+ ctx.beginPath()
97
+ ctx.moveTo(50, 140)
98
+ ctx.lineTo(150, 60)
99
+ ctx.lineTo(250, 140)
100
+ ctx.closePath()
101
+ ctx.stroke()
102
+
103
+ async function main() {
104
+ // load images from disk or from a URL
105
+ const catImage = await loadImage('path/to/cat.png')
106
+ const dogImage = await loadImage('https://example.com/path/to/dog.jpg')
107
+
108
+ ctx.drawImage(catImage, 0, 0, catImage.width, catImage.height)
109
+
110
+ ctx.drawImage(dogImage, canvas.width / 2, canvas.height / 2, dogImage.width, dogImage.height)
111
+
112
+ // export canvas as image
113
+ const pngData = await canvas.encode('png') // JPEG, AVIF and WebP are also supported
114
+ // encoding in libuv thread pool, non-blocking
115
+ await promises.writeFile(join(__dirname, 'simple.png'), pngData)
116
+ }
117
+
118
+ main()
119
+ ```
120
+
121
+ ![](./example/simple.png)
122
+
123
+ ## Emoji text
124
+
125
+ ```js
126
+ const { writeFileSync } = require('fs')
127
+ const { join } = require('path')
128
+
129
+ const { createCanvas, GlobalFonts } = require('@napi-rs/canvas')
130
+
131
+ GlobalFonts.registerFromPath(join(__dirname, '..', 'fonts', 'AppleColorEmoji@2x.ttf'), 'Apple Emoji')
132
+ GlobalFonts.registerFromPath(join(__dirname, '..', '__test__', 'fonts', 'COLRv1.ttf'), 'COLRv1')
133
+
134
+ console.info(GlobalFonts.families)
135
+
136
+ const canvas = createCanvas(760, 360)
137
+ const ctx = canvas.getContext('2d')
138
+
139
+ ctx.font = '50px Apple Emoji'
140
+ ctx.strokeText('😀😃😄😁😆😅😂🤣☺️😊😊😇', 50, 150)
141
+
142
+ ctx.font = '100px COLRv1'
143
+ ctx.fillText('abc', 50, 300)
144
+
145
+ const b = canvas.toBuffer('image/png')
146
+
147
+ writeFileSync(join(__dirname, 'draw-emoji.png'), b)
148
+ ```
149
+
150
+ ![](./example/draw-emoji.png)
151
+
152
+ # Performance
153
+
154
+ See [benchmark](./benchmark) for benchmark code.
155
+
156
+ Hardware info:
157
+
158
+ ```
159
+ ,MMMM. Host - xxxxxxxxxxxxxxxxxxxxxxx
160
+ .MMMMMM Machine - Mac15,9
161
+ MMMMM, Kernel - 24.0.0
162
+ .;MMMMM:' MMMMMMMMMM;. OS - macOS 15.0.1 Sequoia
163
+ MMMMMMMMMMMMNWMMMMMMMMMMM: DE - Aqua
164
+ .MMMMMMMMMMMMMMMMMMMMMMMMWM. WM - Quartz Compositor
165
+ MMMMMMMMMMMMMMMMMMMMMMMMM. Packages - 194 (Homebrew), 32 (cargo)
166
+ ;MMMMMMMMMMMMMMMMMMMMMMMM: Shell - zsh
167
+ :MMMMMMMMMMMMMMMMMMMMMMMM: Terminal - warpterminal (Version v0.2024.10.23.14.49.stable_00)
168
+ .MMMMMMMMMMMMMMMMMMMMMMMMM. Resolution - 5120x2880@160fps (as 2560x1440)
169
+ MMMMMMMMMMMMMMMMMMMMMMMMMMM. 2992x1934@120fps (as 1496x967)
170
+ .MMMMMMMMMMMMMMMMMMMMMMMMMM. 2232x1512@60fps (as 1116x756)
171
+ MMMMMMMMMMMMMMMMMMMMMMMM Uptime - 1d 2h 32m
172
+ ;MMMMMMMMMMMMMMMMMMMM. CPU - Apple M3 Max (16)
173
+ .MMMM,. .MMMM,. CPU Load - 16%
174
+ Memory - 50.1 GB / 134.2 GB
175
+ Battery - 78% & Discharging
176
+ Disk Space - 624.0 GB / 994.7 GB
177
+ ```
178
+
179
+ ```
180
+ ❯ yarn bench
181
+ Draw a House and export to PNG
182
+ ┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
183
+ │ (index) │ Task name │ Latency average (ns) │ Latency median (ns) │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
184
+ ├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
185
+ │ 0 │ '@napi-rs/skia' │ '14676992.14 ± 0.68%' │ '14602333.00' │ '68 ± 0.59%' │ '68' │ 69 │
186
+ │ 1 │ 'skia-canvas' │ '21167809.17 ± 2.05%' │ '20960021.00 ± 13646.00' │ '47 ± 1.31%' │ '48' │ 64 │
187
+ │ 2 │ 'node-canvas' │ '16552027.42 ± 0.70%' │ '16451291.50 ± 2208.50' │ '60 ± 0.62%' │ '61' │ 64 │
188
+ └─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
189
+ Draw Gradient and export to PNG
190
+ ┌─────────┬─────────────────┬───────────────────────┬─────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
191
+ │ (index) │ Task name │ Latency average (ns) │ Latency median (ns) │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
192
+ ├─────────┼─────────────────┼───────────────────────┼─────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
193
+ │ 0 │ '@napi-rs/skia' │ '15228495.58 ± 0.53%' │ '15146312.50 ± 1187.50' │ '66 ± 0.48%' │ '66' │ 66 │
194
+ │ 1 │ 'skia-canvas' │ '21725564.41 ± 2.20%' │ '21412520.50 ± 2104.50' │ '46 ± 1.39%' │ '47' │ 64 │
195
+ │ 2 │ 'node-canvas' │ '17976022.14 ± 1.53%' │ '17563479.50 ± 5104.50' │ '56 ± 1.38%' │ '57' │ 64 │
196
+ └─────────┴─────────────────┴───────────────────────┴─────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
197
+ ```
198
+
199
+ # Features
200
+
201
+ ## Path2D
202
+
203
+ ```typescript
204
+ new Path2D()
205
+ new Path2D(path: Path2D)
206
+ // new Path2D('M108.956,403.826c0,0,0.178,3.344-1.276,3.311 c-1.455-0.033-30.507-84.917-66.752-80.957C40.928,326.18,72.326,313.197,108.956,403.826z')
207
+ new Path2D(path: string)
208
+ ```
209
+
210
+ ```typescript
211
+ export interface DOMMatrix2DInit {
212
+ a: number
213
+ b: number
214
+ c: number
215
+ d: number
216
+ e: number
217
+ f: number
218
+ }
219
+
220
+ export class Path2D {
221
+ constructor(path?: Path2D | string)
222
+
223
+ addPath(path: Path2D, transform?: DOMMatrix2DInit): void
224
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
225
+ arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
226
+ bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
227
+ closePath(): void
228
+ ellipse(
229
+ x: number,
230
+ y: number,
231
+ radiusX: number,
232
+ radiusY: number,
233
+ rotation: number,
234
+ startAngle: number,
235
+ endAngle: number,
236
+ anticlockwise?: boolean,
237
+ ): void
238
+ lineTo(x: number, y: number): void
239
+ moveTo(x: number, y: number): void
240
+ quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
241
+ rect(x: number, y: number, w: number, h: number): void
242
+
243
+ // PathKit methods
244
+ op(path: Path2D, operation: PathOp): Path2D
245
+ toSVGString(): string
246
+ getFillType(): FillType
247
+ getFillTypeString(): string
248
+ setFillType(type: FillType): void
249
+ simplify(): Path2D
250
+ asWinding(): Path2D
251
+ stroke(stroke?: StrokeOptions): Path2D
252
+ transform(transform: DOMMatrix2DInit): Path2D
253
+ getBounds(): [left: number, top: number, right: number, bottom: number]
254
+ computeTightBounds(): [left: number, top: number, right: number, bottom: number]
255
+ trim(start: number, end: number, isComplement?: boolean): Path2D
256
+ round(radius: number): Path2D
257
+ equals(path: Path2D): boolean
258
+ }
259
+ ```
260
+
261
+ ## PathKit
262
+
263
+ `PathKit` is a toolset for manipulating Path in `Skia`, supporting **_quadratic beziers_**, **_cubic beziers_** and **_conics_**.
264
+ The main features are.
265
+
266
+ ### Path Operation
267
+
268
+ `.op(path, PathOp)`
269
+
270
+ ```js
271
+ const pathOne = new Path2D(
272
+ 'M8 50H92C96.4183 50 100 53.5817 100 58V142C100 146.418 96.4183 150 92 150H8C3.58172 150 0 146.418 0 142V58C0 53.5817 3.58172 50 8 50Z',
273
+ )
274
+ const pathTwo = new Path2D(
275
+ '"M58 0H142C146.418 0 150 3.58172 150 8V92C150 96.4183 146.418 100 142 100H58C53.5817 100 50 96.4183 50 92V8C50 3.58172 53.5817 0 58 0Z',
276
+ )
277
+
278
+ pathOne.op(pathTwo, PathOp.Intersect).toSVGString()
279
+ // => "M100 100L58 100C53.5817 100 50 96.4183 50 92L50 50L92 50C96.4183 50 100 53.5817 100 58L100 100Z"
280
+ ```
281
+
282
+ - **Union**, subtract the op path from the first path
283
+ - **Difference**, intersect the two paths
284
+ - **ReverseDifference**, union (inclusive-or) the two paths
285
+ - **Intersect**, exclusive-or the two paths
286
+ - **XOR**, subtract the first path from the op path
287
+
288
+ ![boolean-operations](./docs/imgs/boolean-operations.svg)
289
+
290
+ ### Covert `FillType` in **_Path_**
291
+
292
+ `.asWinding()`
293
+
294
+ You can convert `fill-rule="evenodd"` to `fill-rule="nonzero"` in SVG.
295
+ This is useful for **OpenType** font-related tools, as `fill-rule="nonzero"` is only supported in **OpenType** fonts.
296
+
297
+ ![SVG fill-rule](./docs/imgs/asWinding@2x.png)
298
+
299
+ ```js
300
+ const pathCircle = new Path2D(
301
+ 'M24.2979 13.6364H129.394V40.9091H24.2979L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455H70.4673V68.1818H16.073C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727V113.636C7.00728 118.657 11.0661 122.727 16.073 122.727H70.4673V150H84.0658V122.727H128.041C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818H84.0658V54.5455H133.927C138.934 54.5455 142.993 50.4755 142.993 45.4545V9.09091C142.993 4.07014 138.934 0 133.927 0H21.9592ZM125.702 109.091H20.6058V81.8182H125.702L135.372 95.4545L125.702 109.091Z',
302
+ )
303
+ pathCircle.setFillType(FillType.EvenOdd)
304
+ pathCircle.asWinding().toSVGString()
305
+ // => "M24.2979 13.6364L129.394 13.6364L129.394 40.9091L24.2979 40.9091L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455L70.4673 54.5455L70.4673 68.1818L16.073 68.1818C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727L7.00728 113.636C7.00728 118.657 11.0661 122.727 16.073 122.727L70.4673 122.727L70.4673 150L84.0658 150L84.0658 122.727L128.041 122.727C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818L84.0658 68.1818L84.0658 54.5455L133.927 54.5455C138.934 54.5455 142.993 50.4755 142.993 45.4545L142.993 9.09091C142.993 4.07014 138.934 0 133.927 0L21.9592 0ZM125.702 109.091L20.6058 109.091L20.6058 81.8182L125.702 81.8182L135.372 95.4545L125.702 109.091Z"
306
+ ```
307
+
308
+ ### Simplify **_Path_**
309
+
310
+ `.simplify()`
311
+
312
+ Set the path to the same non-overlapping contour as the original path area, which means that it can also remove overlapping paths.
313
+
314
+ <img width="800" src="./docs/imgs/simplify.png" >
315
+
316
+ [SVG with overlapping paths](./docs/imgs/overlapping-path.svg) (Left)
317
+
318
+ ```js
319
+ const path =
320
+ 'M2.933,89.89 L89.005,3.818 Q90.412,2.411 92.249,1.65 Q94.087,0.889 96.076,0.889 Q98.065,0.889 99.903,1.65 Q101.741,2.411 103.147,3.818 L189.22,89.89 Q190.626,91.296 191.387,93.134 Q192.148,94.972 192.148,96.961 Q192.148,98.95 191.387,100.788 Q190.626,102.625 189.219,104.032 Q187.813,105.439 185.975,106.2 Q184.138,106.961 182.148,106.961 Q180.159,106.961 178.322,106.2 Q176.484,105.439 175.077,104.032 L89.005,17.96 L96.076,10.889 L103.147,17.96 L17.075,104.032 Q15.668,105.439 13.831,106.2 Q11.993,106.961 10.004,106.961 Q8.015,106.961 6.177,106.2 Q4.339,105.439 2.933,104.032 Q1.526,102.625 0.765,100.788 Q0.004,98.95 0.004,96.961 Q0.004,94.972 0.765,93.134 Q1.526,91.296 2.933,89.89 Z'
321
+
322
+ path.simplify().toSVGString()
323
+ // => "M89.005 3.818L2.933 89.89Q1.526 91.296 0.765 93.134Q0.004 94.972 0.004 96.961Q0.004 98.95 0.765 100.788Q1.526 102.625 2.933 104.032Q4.339 105.439 6.177 106.2Q8.015 106.961 10.004 106.961Q11.993 106.961 13.831 106.2Q15.668 105.439 17.075 104.032L96.076 25.031L175.077 104.032Q176.484 105.439 178.322 106.2Q180.159 106.961 182.148 106.961Q184.138 106.961 185.975 106.2Q187.813 105.439 189.219 104.032Q190.626 102.625 191.387 100.788Q192.148 98.95 192.148 96.961Q192.148 94.972 191.387 93.134Q190.626 91.296 189.22 89.89L103.147 3.818Q101.741 2.411 99.903 1.65Q98.065 0.889 96.076 0.889Q94.087 0.889 92.249 1.65Q90.412 2.411 89.005 3.818Z"
324
+ ```
325
+
326
+ ## Lottie Animation
327
+
328
+ Render [Lottie](https://airbnb.io/lottie/) animations using Skia's [Skottie](https://skia.org/docs/user/modules/skottie/) module.
329
+
330
+ ### Load Animation
331
+
332
+ ```js
333
+ const { LottieAnimation } = require('@napi-rs/canvas')
334
+
335
+ // Load from file
336
+ const animation = LottieAnimation.loadFromFile('animation.json')
337
+
338
+ // Load from JSON string with resource path for external assets
339
+ const animation = LottieAnimation.loadFromData(jsonString, {
340
+ resourcePath: '/path/to/assets',
341
+ })
342
+ ```
343
+
344
+ ### Animation Properties
345
+
346
+ ```js
347
+ animation.duration // Total duration in seconds
348
+ animation.fps // Frames per second
349
+ animation.frames // Total frame count
350
+ animation.width // Animation width
351
+ animation.height // Animation height
352
+ animation.version // Lottie format version
353
+ ```
354
+
355
+ ### Playback Control
356
+
357
+ ```js
358
+ animation.seekFrame(30) // Seek to frame 30
359
+ animation.seek(1.5) // Seek to 1.5 seconds
360
+ ```
361
+
362
+ ### Render to Canvas
363
+
364
+ ```js
365
+ const { createCanvas, LottieAnimation } = require('@napi-rs/canvas')
366
+
367
+ const animation = LottieAnimation.loadFromFile('animation.json')
368
+ const canvas = createCanvas(animation.width, animation.height)
369
+ const ctx = canvas.getContext('2d')
370
+
371
+ // Render at original size
372
+ animation.render(ctx)
373
+
374
+ // Render with custom destination rect
375
+ animation.render(ctx, { x: 0, y: 0, width: 800, height: 600 })
376
+ ```
377
+
378
+ ### Supported Features
379
+
380
+ - **Embedded images** - Base64-encoded images (`data:image/png;base64,...`)
381
+ - **Embedded fonts** - Vector glyph paths for text rendering without system fonts
382
+ - **External assets** - Load images from `resourcePath` directory
383
+ - **dotLottie format** - Extract `.lottie` ZIP files at runtime (see example)
384
+
385
+ ### Example: Encode Lottie to Video
386
+
387
+ See [`example/lottie-to-video.ts`](./example/lottie-to-video.ts) for encoding Lottie animations to MP4 using [`@napi-rs/webcodecs`](https://github.com/Brooooooklyn/webcodecs-node).
388
+
389
+ ```js
390
+ import { createCanvas, LottieAnimation } from '@napi-rs/canvas'
391
+ import {
392
+ VideoEncoder,
393
+ VideoFrame,
394
+ Mp4Muxer,
395
+ type EncodedVideoChunk,
396
+ type EncodedVideoChunkMetadata,
397
+ } from '@napi-rs/webcodecs'
398
+
399
+ const animation = LottieAnimation.loadFromFile('animation.json')
400
+ const canvas = createCanvas(animation.width, animation.height)
401
+ const ctx = canvas.getContext('2d')
402
+
403
+ for (let frame = 0; frame < animation.frames; frame++) {
404
+ animation.seekFrame(frame)
405
+ ctx.fillStyle = '#ffffff'
406
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
407
+ animation.render(ctx)
408
+ // Encode frame to video...
409
+ }
410
+ ```
411
+
412
+ # [Example](./example/tiger.js)
413
+
414
+ > The tiger.json was serialized from [gojs/samples/tiger](https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/tiger.html)
415
+
416
+ <img width="500" src="example/tiger.png">
417
+
418
+ ```shell
419
+ node example/anime-girl.js
420
+ ```
421
+
422
+ | SVG | PNG |
423
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
424
+ | <img width="500" src="example/anime-girl.svg"><br/>[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) | <img width="500" src="example/anime-girl.png"><br/>[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) |
425
+
426
+ # Building
427
+
428
+ ## Build skia from source
429
+
430
+ You can build this project from source, the system requirements are here: https://skia.org/docs/user/build
431
+
432
+ ```sh
433
+ # Clone the code:
434
+ $ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
435
+ $ cd canvas
436
+
437
+ # Build Skia:
438
+ $ node scripts/build-skia.js
439
+
440
+ # Install NPM packages, build the Node.js addon:
441
+ $ npm install -g yarn
442
+ $ yarn install --mode=skip-build # Here are modules that are used for benchmarking and are hard to install, you can skip it by specifying `--mode=skip-build`
443
+ $ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
444
+ $ yarn build
445
+
446
+ # All done! Run test cases or examples now:
447
+ $ yarn test
448
+ $ node example/tiger.js
449
+ ```
450
+
451
+ ## Pull pre-build skia binary from GitHub
452
+
453
+ You can pull skia pre-build binaries if you just care the `Rust` part:
454
+
455
+ ```sh
456
+ # Clone the code:
457
+ $ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
458
+ $ cd canvas
459
+
460
+ # Download Skia binaries:
461
+ # It will pull the binaries match the git hash in `./skia` submodule
462
+ $ node scripts/release-skia-binary.mjs --download
463
+
464
+ # Install NPM packages, build the Node.js addon:
465
+ $ npm install -g yarn
466
+ $ yarn install --mode=skip-build
467
+ $ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
468
+ $ yarn build
469
+
470
+ # All done! Run test cases or examples now:
471
+ $ yarn test
472
+ $ node example/tiger.js
473
+ ```
@@ -0,0 +1,94 @@
1
+ // Effing's additions to @napi-rs/canvas. They live in this separate entry so
2
+ // the main entry stays a drop-in for upstream. See docs/effing.md.
3
+ import type { SKRSContext2D } from './index'
4
+
5
+ export interface GroupOptions {
6
+ /** Group opacity, 0 to 1 (clamped). Defaults to 1. */
7
+ opacity?: number
8
+ /** A `globalCompositeOperation` value, e.g. `multiply`. Defaults to `source-over`. */
9
+ blendMode?: string
10
+ /** A CSS `filter` value applied to the whole group. */
11
+ filter?: string
12
+ /** A CSS `filter` value applied to the content behind the group, which the group starts from. */
13
+ backdropFilter?: string
14
+ /** `[x, y, width, height]` in the current coordinate space: a size hint that also clips the group's content. */
15
+ bounds?: [number, number, number, number]
16
+ }
17
+
18
+ /**
19
+ * Starts a compositing group on `ctx`: everything drawn until the matching
20
+ * `endGroup(ctx)` is composited as one with the group's opacity, blend mode
21
+ * and filter. Saves the context state like `save()`. Throws on invalid
22
+ * options, and on an SVG canvas for a group that composites (anything but
23
+ * opacity 1, `source-over` and no filters): Skia's SVG device has no layers.
24
+ */
25
+ export function beginGroup(ctx: SKRSContext2D, options?: GroupOptions): void
26
+ /** Ends the innermost group on `ctx`, compositing it. Throws if the innermost save is not a group. */
27
+ export function endGroup(ctx: SKRSContext2D): void
28
+
29
+ export interface ParagraphStyle {
30
+ /** CSS font-family list, e.g. `"Inter", sans-serif`. */
31
+ fontFamily: string
32
+ fontSize: number
33
+ /** Defaults to 400. */
34
+ fontWeight?: number
35
+ fontStyle?: 'normal' | 'italic' | 'oblique'
36
+ letterSpacing?: number
37
+ /** Line box height in px; omitted or 0 for `normal` (hhea ascent + descent). */
38
+ lineHeight?: number
39
+ /** `start` and `end` follow `direction`. Defaults to `left`. */
40
+ textAlign?: 'left' | 'right' | 'center' | 'justify' | 'start' | 'end'
41
+ direction?: 'ltr' | 'rtl'
42
+ /** Break only at hard line breaks. */
43
+ noWrap?: boolean
44
+ /** 0 or omitted for unlimited. */
45
+ maxLines?: number
46
+ /** Appended where text is truncated by `maxLines` or `noWrap`, e.g. `…`. */
47
+ ellipsis?: string
48
+ }
49
+
50
+ export interface ParagraphLine {
51
+ /** Left edge of the line, alignment included. */
52
+ left: number
53
+ /** Advance width without trailing whitespace, letter spacing included. */
54
+ width: number
55
+ /** Baseline, from the top of the paragraph. */
56
+ baseline: number
57
+ /** UTF-16 offsets of the line's text (JS string indices), trailing whitespace excluded. */
58
+ startIndex: number
59
+ endIndex: number
60
+ hardBreak: boolean
61
+ }
62
+
63
+ export interface ParagraphLayout {
64
+ /** `lines.length * lineHeight`. */
65
+ height: number
66
+ longestLine: number
67
+ /** The widest word, or for `noWrap` text the widest line: CSS min-content. */
68
+ minIntrinsicWidth: number
69
+ maxIntrinsicWidth: number
70
+ didExceedMaxLines: boolean
71
+ /** Every line box is exactly this tall. */
72
+ lineHeight: number
73
+ /** The primary font's hhea ascender and descender in px. */
74
+ ascent: number
75
+ descent: number
76
+ lines: ParagraphLine[]
77
+ }
78
+
79
+ /**
80
+ * A single-style paragraph laid out natively. Line boxes follow the CSS
81
+ * model (every line exactly `lineHeight` tall, baseline placed by
82
+ * half-leading) and glyphs are painted unhinted and unsnapped, so the text
83
+ * lands in the same place at any raster scale.
84
+ */
85
+ export class Paragraph {
86
+ constructor(text: string, style: ParagraphStyle)
87
+ /** Lays the paragraph out in `width` px (non-finite or ≤ 0 for unbounded) and reports its lines. */
88
+ layout(width: number): ParagraphLayout
89
+ }
90
+
91
+ /** Fills a laid-out paragraph's glyphs on `ctx` with its current fill style, the paragraph's top-left corner at (x, y). */
92
+ export function fillParagraph(ctx: SKRSContext2D, paragraph: Paragraph, x: number, y: number): void
93
+ /** Strokes a laid-out paragraph's glyph outlines on `ctx` with its current stroke style, the paragraph's top-left corner at (x, y). */
94
+ export function strokeParagraph(ctx: SKRSContext2D, paragraph: Paragraph, x: number, y: number): void
package/extensions.js ADDED
@@ -0,0 +1,11 @@
1
+ // Effing's additions, kept off the main entry so that `@effing/skia` itself
2
+ // stays a drop-in for `@napi-rs/canvas`. See docs/effing.md.
3
+ const { Paragraph, beginGroup, endGroup, fillParagraph, strokeParagraph } = require('./js-binding.js')
4
+
5
+ module.exports = {
6
+ Paragraph,
7
+ beginGroup,
8
+ endGroup,
9
+ fillParagraph,
10
+ strokeParagraph,
11
+ }