@awflow/avatar-export 1.0.0 → 1.1.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/README.md CHANGED
@@ -14,7 +14,14 @@ Every function returns a `Promise<Uint8Array>`. In Node, Bun and Deno the
14
14
  wasm loads on first use; nothing to set up.
15
15
 
16
16
  ```ts
17
- import { toApng, toGif, toPng, toSprite } from '@awflow/avatar-export'
17
+ import {
18
+ svgToPng,
19
+ toApng,
20
+ toGif,
21
+ toPng,
22
+ toSprite,
23
+ } from '@awflow/avatar-export'
24
+ import { frames } from '@awflow/avatar'
18
25
 
19
26
  // a still PNG, 256 px square by default
20
27
  const png = await toPng('Linh', { expression: 'happy' }, { size: 512 })
@@ -25,6 +32,20 @@ const apng = await toApng('Linh', { reaction: 'hop' }, { fps: 30, seconds: 3 })
25
32
  // a GIF with one 256-colour palette, transparent where nothing is drawn
26
33
  const gif = await toGif('Linh', { frame: 'none', activity: 'done' })
27
34
 
35
+ // a script: idle, then working from 1 s, a cheer at 3 s, idle again at 4 s
36
+ const story = await toGif('Linh', {
37
+ timeline: [
38
+ { at: 1, activity: 'working' },
39
+ { at: 3, reaction: 'celebrate' },
40
+ { at: 4, activity: 'idle' },
41
+ ],
42
+ }, { fps: 25, seconds: 5 })
43
+
44
+ // any SVG string, such as one frame of frames(), at a given width
45
+ const still = await svgToPng(frames('Linh', {}, { fps: 1, seconds: 1 })[0], {
46
+ size: 128,
47
+ })
48
+
28
49
  // many avatars on one sheet: 3 columns of 64 px tiles, 4 px apart
29
50
  const sheet = await toSprite(['Ada', 'Linh', 'Sam'], {}, {
30
51
  size: 64,
@@ -40,7 +61,14 @@ Write the bytes anywhere: `await Bun.write('linh.png', png)`,
40
61
  ## Browsers
41
62
 
42
63
  Load the wasm once with `init` before the first export. It takes a
43
- `fetch()` response (or its promise), an `ArrayBuffer`, typed array or a `URL`:
64
+ `fetch()` response (or its promise), an `ArrayBuffer`, typed array or a `URL`.
65
+ The wasm file comes from `@resvg/resvg-wasm`, which this package depends on.
66
+ Importing it by path from your own code needs it as a direct dependency under
67
+ strict package managers (pnpm), at the exact version this package uses:
68
+
69
+ ```sh
70
+ npm install @resvg/resvg-wasm@2.6.2
71
+ ```
44
72
 
45
73
  ```ts
46
74
  import { init, toPng } from '@awflow/avatar-export'
@@ -58,13 +86,21 @@ return at once.
58
86
  `LIMITS` is
59
87
  `{ maxFrames: 600, maxSize: 1024, maxSprite: 400, maxSheet: 16777216 }`, with
60
88
  `maxFrames` taken from the core's `FRAME_LIMITS` (1 to 120 fps, up to 60
61
- seconds). `maxSheet` caps a sprite sheet's area at 4096 by 4096 pixels. Frames are `round(fps * seconds)`, checked before anything is
89
+ seconds). `maxSheet` caps a sprite sheet's area, and an `svgToPng` image's,
90
+ at 4096 by 4096 pixels. A `timeline` holds at most `maxFrames` cues. Frames are `round(fps * seconds)`, checked before anything is
62
91
  drawn. A value out of range, or not a finite number, throws a `RangeError`
63
92
  that names the limit.
64
93
 
65
94
  GIF counts time in hundredths of a second and browsers slow down frames
66
95
  shorter than 2/100 s, so keep GIFs at 50 fps or less. APNG timing is exact.
67
96
 
97
+ Rasterising is synchronous CPU work on the calling thread. On a recent laptop
98
+ a 1024 px, 60-frame export takes about 13 s as a GIF and 17 s as an APNG, and
99
+ a sheet of 256 tiles at 256 px about 11 s. The worst case the limits allow,
100
+ 600 frames at 1024 px, is minutes of CPU. On a server, cap `size`, `fps` and
101
+ `seconds` well below `LIMITS` for untrusted input, and run exports in a worker
102
+ or a queue rather than on the request path.
103
+
68
104
  ## Docs
69
105
 
70
106
  Full documentation: <https://github.com/awflow/agent-avatar#readme>
@@ -72,7 +108,9 @@ Full documentation: <https://github.com/awflow/agent-avatar#readme>
72
108
  ## Credits
73
109
 
74
110
  Built on Peek by Doan Labs (MIT). Rasterising by
75
- [resvg](https://github.com/linebender/resvg), GIF encoding by
111
+ [resvg](https://github.com/linebender/resvg) through
112
+ [`@resvg/resvg-wasm`](https://www.npmjs.com/package/@resvg/resvg-wasm)
113
+ (MPL-2.0), GIF encoding by
76
114
  [gifenc](https://github.com/mattdesl/gifenc), compression by
77
115
  [fflate](https://github.com/101arrowz/fflate).
78
116
 
@@ -1,6 +1,12 @@
1
- import { type AvatarOptions, type Reaction } from '@awflow/avatar';
1
+ import { type AvatarOptions, type Cue, type Reaction } from '@awflow/avatar';
2
+ /**
3
+ * What to animate: any avatar options (a registered `character` included),
4
+ * a `reaction` from the first frame, and a `timeline` of later cues: an
5
+ * activity, an expression or a reaction at a time in seconds.
6
+ */
2
7
  export type AnimatedOptions = AvatarOptions & {
3
8
  reaction?: Reaction;
9
+ timeline?: readonly Cue[];
4
10
  };
5
11
  export type AnimationSettings = {
6
12
  /** Side in pixels, 1 to LIMITS.maxSize. Default 256. */
package/dist/index.d.ts CHANGED
@@ -6,6 +6,14 @@ export type { WasmSource } from './raster.js';
6
6
  export { init } from './raster.js';
7
7
  export type { SpriteSettings } from './sprite.js';
8
8
  export { toSprite } from './sprite.js';
9
+ /**
10
+ * Any avatar SVG (one frame of `frames()`, or a sheet of them) as a PNG,
11
+ * `size` pixels wide (default 256, at most LIMITS.maxSize). The height
12
+ * follows the SVG's aspect ratio.
13
+ */
14
+ export declare function svgToPng(svg: string, o?: {
15
+ size?: number;
16
+ }): Promise<Uint8Array>;
9
17
  /** The avatar as a still PNG, `size` pixels square (default 256). */
10
18
  export declare function toPng(name: string, opts?: AvatarOptions, o?: {
11
19
  size?: number;
package/dist/index.js CHANGED
@@ -101,8 +101,13 @@ var options = (size) => ({
101
101
  fitTo: { mode: "width", value: size },
102
102
  font: { loadSystemFonts: false }
103
103
  });
104
- function png(svg, size) {
104
+ function png(svg, size, maxPixels) {
105
105
  const resvg = new Resvg(svg, options(size));
106
+ const area = resvg.width > 0 ? size * size * (resvg.height / resvg.width) : 0;
107
+ if (maxPixels !== undefined && area > maxPixels) {
108
+ resvg.free();
109
+ throw new RangeError(`svgToPng(): ${Math.round(area)} pixels is above maxSheet ${maxPixels}`);
110
+ }
106
111
  const image = resvg.render();
107
112
  try {
108
113
  return image.asPng();
@@ -134,7 +139,9 @@ function rgba(svg, size) {
134
139
  }
135
140
 
136
141
  // src/animated.ts
137
- import { frames } from "@awflow/avatar";
142
+ import {
143
+ frames
144
+ } from "@awflow/avatar";
138
145
  import * as gifenc from "gifenc";
139
146
 
140
147
  // src/png.ts
@@ -392,6 +399,13 @@ async function toSprite(names, opts = {}, o = {}) {
392
399
  }
393
400
 
394
401
  // src/index.ts
402
+ async function svgToPng(svg, o = {}) {
403
+ const size = checkSize("svgToPng()", o.size);
404
+ if (typeof svg !== "string" || !svg.trimStart().startsWith("<svg"))
405
+ throw new TypeError("svgToPng(): svg must be an SVG string");
406
+ await ensure();
407
+ return png(svg, size, LIMITS.maxSheet);
408
+ }
395
409
  async function toPng(name, opts = {}, o = {}) {
396
410
  const size = checkSize("toPng()", o.size);
397
411
  await ensure();
@@ -402,6 +416,7 @@ export {
402
416
  toPng,
403
417
  toGif,
404
418
  toApng,
419
+ svgToPng,
405
420
  init,
406
421
  LIMITS,
407
422
  DEFAULTS
package/dist/raster.d.ts CHANGED
@@ -9,8 +9,12 @@ export type WasmSource = ArrayBuffer | ArrayBufferView | Response | URL | WebAss
9
9
  export declare function init(wasm?: WasmSource | PromiseLike<WasmSource>): Promise<void>;
10
10
  /** The export functions wait on this: init() with no argument when needed. */
11
11
  export declare const ensure: () => Promise<void>;
12
- /** One SVG as a PNG, size pixels wide, encoded by resvg. */
13
- export declare function png(svg: string, size: number): Uint8Array;
12
+ /**
13
+ * One SVG as a PNG, size pixels wide, encoded by resvg. With `maxPixels`,
14
+ * an SVG whose height would take the image past that area is refused before
15
+ * any pixel is drawn.
16
+ */
17
+ export declare function png(svg: string, size: number, maxPixels?: number): Uint8Array;
14
18
  /**
15
19
  * One SVG as straight (not premultiplied) RGBA pixels, size by size. resvg
16
20
  * hands premultiplied pixels back; they are demultiplied the way its own PNG
package/dist/sprite.d.ts CHANGED
@@ -12,4 +12,4 @@ export type SpriteSettings = {
12
12
  * (columns - 1) * gap, height likewise for the rows used. The same options
13
13
  * apply to every tile. The sheet's area is at most LIMITS.maxSheet pixels. Name i sits at column i % columns, row i / columns.
14
14
  */
15
- export declare function toSprite(names: string[], opts?: AvatarOptions, o?: SpriteSettings): Promise<Uint8Array>;
15
+ export declare function toSprite(names: readonly string[], opts?: AvatarOptions, o?: SpriteSettings): Promise<Uint8Array>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awflow/avatar-export",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "bin": {
@@ -21,13 +21,16 @@
21
21
  "LICENSE"
22
22
  ],
23
23
  "dependencies": {
24
- "@awflow/avatar": "^1.0.0",
24
+ "@awflow/avatar": "^1.1.0",
25
25
  "@resvg/resvg-wasm": "2.6.2",
26
26
  "fflate": "0.8.3",
27
27
  "gifenc": "1.0.3"
28
28
  },
29
29
  "description": "Rasterise AWFlow avatars to PNG, APNG, GIF and sprite sheets, in Node and the browser.",
30
30
  "license": "MIT",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
31
34
  "author": "AWFlow",
32
35
  "homepage": "https://github.com/awflow/agent-avatar",
33
36
  "repository": {