@napi-rs/canvas 0.1.83 → 0.1.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -298,6 +298,92 @@ path.simplify().toSVGString()
298
298
  // => "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"
299
299
  ```
300
300
 
301
+ ## Lottie Animation
302
+
303
+ Render [Lottie](https://airbnb.io/lottie/) animations using Skia's [Skottie](https://skia.org/docs/user/modules/skottie/) module.
304
+
305
+ ### Load Animation
306
+
307
+ ```js
308
+ const { LottieAnimation } = require('@napi-rs/canvas')
309
+
310
+ // Load from file
311
+ const animation = LottieAnimation.loadFromFile('animation.json')
312
+
313
+ // Load from JSON string with resource path for external assets
314
+ const animation = LottieAnimation.loadFromData(jsonString, {
315
+ resourcePath: '/path/to/assets',
316
+ })
317
+ ```
318
+
319
+ ### Animation Properties
320
+
321
+ ```js
322
+ animation.duration // Total duration in seconds
323
+ animation.fps // Frames per second
324
+ animation.frames // Total frame count
325
+ animation.width // Animation width
326
+ animation.height // Animation height
327
+ animation.version // Lottie format version
328
+ ```
329
+
330
+ ### Playback Control
331
+
332
+ ```js
333
+ animation.seekFrame(30) // Seek to frame 30
334
+ animation.seek(1.5) // Seek to 1.5 seconds
335
+ ```
336
+
337
+ ### Render to Canvas
338
+
339
+ ```js
340
+ const { createCanvas, LottieAnimation } = require('@napi-rs/canvas')
341
+
342
+ const animation = LottieAnimation.loadFromFile('animation.json')
343
+ const canvas = createCanvas(animation.width, animation.height)
344
+ const ctx = canvas.getContext('2d')
345
+
346
+ // Render at original size
347
+ animation.render(ctx)
348
+
349
+ // Render with custom destination rect
350
+ animation.render(ctx, { x: 0, y: 0, width: 800, height: 600 })
351
+ ```
352
+
353
+ ### Supported Features
354
+
355
+ - **Embedded images** - Base64-encoded images (`data:image/png;base64,...`)
356
+ - **Embedded fonts** - Vector glyph paths for text rendering without system fonts
357
+ - **External assets** - Load images from `resourcePath` directory
358
+ - **dotLottie format** - Extract `.lottie` ZIP files at runtime (see example)
359
+
360
+ ### Example: Encode Lottie to Video
361
+
362
+ 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).
363
+
364
+ ```js
365
+ import { createCanvas, LottieAnimation } from '@napi-rs/canvas'
366
+ import {
367
+ VideoEncoder,
368
+ VideoFrame,
369
+ Mp4Muxer,
370
+ type EncodedVideoChunk,
371
+ type EncodedVideoChunkMetadata,
372
+ } from '@napi-rs/webcodecs'
373
+
374
+ const animation = LottieAnimation.loadFromFile('animation.json')
375
+ const canvas = createCanvas(animation.width, animation.height)
376
+ const ctx = canvas.getContext('2d')
377
+
378
+ for (let frame = 0; frame < animation.frames; frame++) {
379
+ animation.seekFrame(frame)
380
+ ctx.fillStyle = '#ffffff'
381
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
382
+ animation.render(ctx)
383
+ // Encode frame to video...
384
+ }
385
+ ```
386
+
301
387
  # [Example](./example/tiger.js)
302
388
 
303
389
  > The tiger.json was serialized from [gojs/samples/tiger](https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/tiger.html)
package/index.d.ts CHANGED
@@ -619,6 +619,121 @@ export interface AvifConfig {
619
619
  /** set to '4:2:0' to use chroma subsampling, default '4:4:4' */
620
620
  chromaSubsampling?: ChromaSubsampling
621
621
  }
622
+
623
+ /** GIF encoding configuration for single-frame encoding */
624
+ export interface GifConfig {
625
+ /**
626
+ * Quality for NeuQuant color quantization (1-30, lower = slower but better quality)
627
+ * @default 10
628
+ */
629
+ quality?: number
630
+ }
631
+
632
+ /** Configuration for the GIF encoder (animated GIFs) */
633
+ export interface GifEncoderConfig {
634
+ /**
635
+ * Loop count: 0 = infinite loop, positive number = finite loops
636
+ * @default 0 (infinite)
637
+ */
638
+ repeat?: number
639
+ /**
640
+ * Quality for NeuQuant color quantization (1-30, lower = slower but better quality)
641
+ * @default 10
642
+ */
643
+ quality?: number
644
+ }
645
+
646
+ /** Configuration for individual GIF frames */
647
+ export interface GifFrameConfig {
648
+ /**
649
+ * Frame delay in milliseconds
650
+ * @default 100
651
+ */
652
+ delay?: number
653
+ /** Disposal method for this frame */
654
+ disposal?: GifDisposal
655
+ /** Transparent color index (0-255), if the frame has transparency */
656
+ transparent?: number
657
+ /** X offset of this frame within the canvas */
658
+ left?: number
659
+ /** Y offset of this frame within the canvas */
660
+ top?: number
661
+ }
662
+
663
+ /** GIF frame disposal method */
664
+ export enum GifDisposal {
665
+ /** Keep the frame visible (default) */
666
+ Keep = 0,
667
+ /** Clear the frame area to the background color */
668
+ Background = 1,
669
+ /** Restore to the previous frame */
670
+ Previous = 2,
671
+ }
672
+
673
+ /**
674
+ * GIF Encoder for creating animated GIFs.
675
+ * Implements `Disposable` interface for use with the `using` keyword (ES2024).
676
+ *
677
+ * @example
678
+ * ```typescript
679
+ * // Basic usage
680
+ * const encoder = new GifEncoder(100, 100, { repeat: 0, quality: 10 });
681
+ * encoder.addFrame(rgbaData, 100, 100, { delay: 100 });
682
+ * encoder.addFrame(rgbaData2, 100, 100, { delay: 100 });
683
+ * const buffer = encoder.finish();
684
+ *
685
+ * // With `using` keyword (automatic cleanup)
686
+ * {
687
+ * using encoder = new GifEncoder(100, 100);
688
+ * encoder.addFrame(frame1, 100, 100, { delay: 100 });
689
+ * const buffer = encoder.finish();
690
+ * } // encoder automatically disposed
691
+ * ```
692
+ */
693
+ export class GifEncoder implements Disposable {
694
+ /**
695
+ * Create a new GIF encoder with the specified dimensions
696
+ * @param width - Width of the GIF canvas in pixels
697
+ * @param height - Height of the GIF canvas in pixels
698
+ * @param config - Optional encoder configuration
699
+ */
700
+ constructor(width: number, height: number, config?: GifEncoderConfig)
701
+
702
+ /** Width of the GIF canvas */
703
+ readonly width: number
704
+ /** Height of the GIF canvas */
705
+ readonly height: number
706
+ /** Number of frames added so far */
707
+ readonly frameCount: number
708
+
709
+ /**
710
+ * Add a frame from RGBA pixel data
711
+ * @param data - RGBA pixel data (must be width * height * 4 bytes)
712
+ * @param width - Width of the frame in pixels
713
+ * @param height - Height of the frame in pixels
714
+ * @param config - Optional frame configuration
715
+ */
716
+ addFrame(data: Uint8Array, width: number, height: number, config?: GifFrameConfig): void
717
+
718
+ /**
719
+ * Finish encoding and return the GIF data.
720
+ * Clears all accumulated frames after encoding.
721
+ * @throws Error if no frames have been added
722
+ */
723
+ finish(): Buffer
724
+
725
+ /**
726
+ * Dispose of the encoder, clearing all accumulated frames without encoding.
727
+ * Called automatically when using the `using` keyword.
728
+ */
729
+ dispose(): void
730
+
731
+ /**
732
+ * Symbol.dispose implementation for ES2024 Explicit Resource Management.
733
+ * Allows using `using encoder = new GifEncoder(...)` syntax.
734
+ */
735
+ [Symbol.dispose](): void
736
+ }
622
737
  /**
623
738
  * https://en.wikipedia.org/wiki/Chroma_subsampling#Types_of_sampling_and_subsampling
624
739
  * https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts
@@ -676,24 +791,29 @@ export class Canvas {
676
791
  encodeSync(format: 'webp' | 'jpeg', quality?: number): Buffer
677
792
  encodeSync(format: 'png'): Buffer
678
793
  encodeSync(format: 'avif', cfg?: AvifConfig): Buffer
794
+ encodeSync(format: 'gif', quality?: number): Buffer
679
795
  encode(format: 'webp' | 'jpeg', quality?: number): Promise<Buffer>
680
796
  encode(format: 'png'): Promise<Buffer>
681
797
  encode(format: 'avif', cfg?: AvifConfig): Promise<Buffer>
798
+ encode(format: 'gif', quality?: number): Promise<Buffer>
682
799
  encodeStream(format: 'webp' | 'jpeg', quality?: number): ReadableStream<Buffer>
683
800
  encodeStream(format: 'png'): ReadableStream<Buffer>
684
801
  toBuffer(mime: 'image/png'): Buffer
685
802
  toBuffer(mime: 'image/jpeg' | 'image/webp', quality?: number): Buffer
686
803
  toBuffer(mime: 'image/avif', cfg?: AvifConfig): Buffer
804
+ toBuffer(mime: 'image/gif', quality?: number): Buffer
687
805
  // raw pixels
688
806
  data(): Buffer
689
807
  toDataURL(mime?: 'image/png'): string
690
808
  toDataURL(mime: 'image/jpeg' | 'image/webp', quality?: number): string
691
- toDataURL(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): string
809
+ toDataURL(mime: 'image/gif', quality?: number): string
810
+ toDataURL(mime?: 'image/jpeg' | 'image/webp' | 'image/png' | 'image/gif', quality?: number): string
692
811
  toDataURL(mime?: 'image/avif', cfg?: AvifConfig): string
693
812
 
694
813
  toDataURLAsync(mime?: 'image/png'): Promise<string>
695
814
  toDataURLAsync(mime: 'image/jpeg' | 'image/webp', quality?: number): Promise<string>
696
- toDataURLAsync(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): Promise<string>
815
+ toDataURLAsync(mime: 'image/gif', quality?: number): Promise<string>
816
+ toDataURLAsync(mime?: 'image/jpeg' | 'image/webp' | 'image/png' | 'image/gif', quality?: number): Promise<string>
697
817
  toDataURLAsync(mime?: 'image/avif', cfg?: AvifConfig): Promise<string>
698
818
 
699
819
  toBlob(callback: (blob: Blob | null) => void, mime?: string, quality?: number): void
@@ -837,3 +957,99 @@ export declare class PDFDocument {
837
957
  endPage(): void
838
958
  close(): Buffer
839
959
  }
960
+
961
+ export interface LottieAnimationOptions {
962
+ /** Base path for resolving external resources (images, fonts) */
963
+ resourcePath?: string
964
+ }
965
+
966
+ export interface LottieRenderRect {
967
+ x: number
968
+ y: number
969
+ width: number
970
+ height: number
971
+ }
972
+
973
+ /**
974
+ * Lottie animation class for loading and rendering Lottie JSON files.
975
+ *
976
+ * @example
977
+ * ```typescript
978
+ * import { LottieAnimation, createCanvas } from '@napi-rs/canvas'
979
+ *
980
+ * const animation = LottieAnimation.loadFromFile('./animation.json')
981
+ * const canvas = createCanvas(animation.width, animation.height)
982
+ * const ctx = canvas.getContext('2d')
983
+ *
984
+ * // Render frame 0
985
+ * animation.seekFrame(0)
986
+ * animation.render(ctx)
987
+ *
988
+ * // Save to PNG
989
+ * const buffer = canvas.toBuffer('image/png')
990
+ * ```
991
+ */
992
+ export declare class LottieAnimation {
993
+ /**
994
+ * Load animation from JSON string or Buffer
995
+ * @param data - JSON string or Buffer containing Lottie animation data
996
+ * @param options - Optional configuration
997
+ */
998
+ static loadFromData(data: string | Buffer, options?: LottieAnimationOptions): LottieAnimation
999
+
1000
+ /**
1001
+ * Load animation from file path
1002
+ * External assets are resolved relative to the file's directory
1003
+ * @param path - Path to the Lottie JSON file
1004
+ */
1005
+ static loadFromFile(path: string): LottieAnimation
1006
+
1007
+ /** Animation duration in seconds */
1008
+ readonly duration: number
1009
+
1010
+ /** Frame rate (frames per second) */
1011
+ readonly fps: number
1012
+
1013
+ /** Total frame count */
1014
+ readonly frames: number
1015
+
1016
+ /** Animation width in pixels */
1017
+ readonly width: number
1018
+
1019
+ /** Animation height in pixels */
1020
+ readonly height: number
1021
+
1022
+ /** Lottie format version string */
1023
+ readonly version: string
1024
+
1025
+ /** Animation in-point (start frame) */
1026
+ readonly inPoint: number
1027
+
1028
+ /** Animation out-point (end frame) */
1029
+ readonly outPoint: number
1030
+
1031
+ /**
1032
+ * Seek to normalized position
1033
+ * @param t - Position between 0.0 (start) and 1.0 (end)
1034
+ */
1035
+ seek(t: number): void
1036
+
1037
+ /**
1038
+ * Seek to specific frame index
1039
+ * @param frame - Frame index (0 = first frame)
1040
+ */
1041
+ seekFrame(frame: number): void
1042
+
1043
+ /**
1044
+ * Seek to specific time in seconds
1045
+ * @param seconds - Time in seconds from start
1046
+ */
1047
+ seekTime(seconds: number): void
1048
+
1049
+ /**
1050
+ * Render current frame to canvas context
1051
+ * @param ctx - Canvas 2D rendering context
1052
+ * @param dst - Optional destination rectangle for scaling/positioning
1053
+ */
1054
+ render(ctx: CanvasRenderingContext2D, dst?: LottieRenderRect): void
1055
+ }
package/index.js CHANGED
@@ -17,12 +17,22 @@ const {
17
17
  StrokeCap,
18
18
  convertSVGTextToPath,
19
19
  PdfDocument,
20
+ GifEncoder,
21
+ GifDisposal,
22
+ LottieAnimation,
20
23
  } = require('./js-binding')
21
24
 
22
25
  const { DOMPoint, DOMMatrix, DOMRect } = require('./geometry')
23
26
 
24
27
  const loadImage = require('./load-image')
25
28
 
29
+ // Add Symbol.dispose support for GifEncoder (ECMAScript 2024 Explicit Resource Management)
30
+ if (GifEncoder && typeof Symbol.dispose !== 'undefined') {
31
+ GifEncoder.prototype[Symbol.dispose] = function () {
32
+ this.dispose()
33
+ }
34
+ }
35
+
26
36
  const SvgExportFlag = {
27
37
  ConvertTextToPaths: 0x01,
28
38
  NoPrettyXML: 0x02,
@@ -178,4 +188,9 @@ module.exports = {
178
188
  CanvasElement,
179
189
  SVGCanvas,
180
190
  PDFDocument: PdfDocument,
191
+ // GIF encoding
192
+ GifEncoder,
193
+ GifDisposal,
194
+ // Lottie animation
195
+ LottieAnimation,
181
196
  }
package/js-binding.js CHANGED
@@ -460,3 +460,5 @@ module.exports.PathOp = nativeBinding.PathOp
460
460
  module.exports.StrokeCap = nativeBinding.StrokeCap
461
461
  module.exports.StrokeJoin = nativeBinding.StrokeJoin
462
462
  module.exports.SvgExportFlag = nativeBinding.SvgExportFlag
463
+ module.exports.GifEncoder = nativeBinding.GifEncoder
464
+ module.exports.GifDisposal = nativeBinding.GifDisposal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napi-rs/canvas",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Canvas for Node.js with skia backend",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -37,6 +37,7 @@
37
37
  "x86_64-unknown-linux-gnu",
38
38
  "x86_64-apple-darwin",
39
39
  "x86_64-pc-windows-msvc",
40
+ "aarch64-pc-windows-msvc",
40
41
  "armv7-unknown-linux-gnueabihf",
41
42
  "x86_64-unknown-linux-musl",
42
43
  "aarch64-unknown-linux-gnu",
@@ -58,6 +59,7 @@
58
59
  "bench": "node --import @oxc-node/core/register benchmark/bench.ts",
59
60
  "build": "napi build --platform --release --js js-binding.js",
60
61
  "build:debug": "napi build --platform --js js-binding.js",
62
+ "example-lottie": "yarn oxnode ./example/lottie-to-video.ts",
61
63
  "format": "run-p format:source format:rs format:toml",
62
64
  "format:rs": "cargo fmt",
63
65
  "format:source": "prettier . -w",
@@ -76,8 +78,10 @@
76
78
  "@jimp/jpeg": "^0.22.12",
77
79
  "@jimp/png": "^0.22.12",
78
80
  "@napi-rs/cli": "^3.1.1",
81
+ "@napi-rs/webcodecs": "^1.1.0",
79
82
  "@octokit/rest": "^22.0.0",
80
- "@oxc-node/core": "^0.0.34",
83
+ "@oxc-node/cli": "^0.0.35",
84
+ "@oxc-node/core": "^0.0.35",
81
85
  "@taplo/cli": "^0.7.0",
82
86
  "@types/lodash": "^4.17.17",
83
87
  "@types/node": "^24.0.0",
@@ -102,7 +106,7 @@
102
106
  "semver": "^7.7.2",
103
107
  "skia-canvas": "^3.0.0",
104
108
  "table": "^6.9.0",
105
- "tinybench": "^5.0.0",
109
+ "tinybench": "^6.0.0",
106
110
  "typescript": "^5.8.3"
107
111
  },
108
112
  "lint-staged": {
@@ -140,17 +144,22 @@
140
144
  "singleQuote": true,
141
145
  "arrowParens": "always"
142
146
  },
147
+ "funding": {
148
+ "type": "github",
149
+ "url": "https://github.com/sponsors/Brooooooklyn"
150
+ },
143
151
  "packageManager": "yarn@4.12.0",
144
152
  "optionalDependencies": {
145
- "@napi-rs/canvas-linux-x64-gnu": "0.1.83",
146
- "@napi-rs/canvas-darwin-x64": "0.1.83",
147
- "@napi-rs/canvas-win32-x64-msvc": "0.1.83",
148
- "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.83",
149
- "@napi-rs/canvas-linux-x64-musl": "0.1.83",
150
- "@napi-rs/canvas-linux-arm64-gnu": "0.1.83",
151
- "@napi-rs/canvas-linux-arm64-musl": "0.1.83",
152
- "@napi-rs/canvas-darwin-arm64": "0.1.83",
153
- "@napi-rs/canvas-android-arm64": "0.1.83",
154
- "@napi-rs/canvas-linux-riscv64-gnu": "0.1.83"
153
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.85",
154
+ "@napi-rs/canvas-darwin-x64": "0.1.85",
155
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.85",
156
+ "@napi-rs/canvas-win32-arm64-msvc": "0.1.85",
157
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.85",
158
+ "@napi-rs/canvas-linux-x64-musl": "0.1.85",
159
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.85",
160
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.85",
161
+ "@napi-rs/canvas-darwin-arm64": "0.1.85",
162
+ "@napi-rs/canvas-android-arm64": "0.1.85",
163
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.85"
155
164
  }
156
165
  }