@effing/canvas 0.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/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ O'Saasy License
2
+
3
+ Copyright © 2026, Trackuity BV.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ 1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ 2. No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.
10
+
11
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # @effing/canvas
2
+
3
+ **Server-side canvas with JSX and Lottie support.**
4
+
5
+ > Part of the [**Effing**](../../README.md) family — programmatic video creation with TypeScript.
6
+
7
+ Render React JSX elements and Lottie animations directly to a canvas using [@napi-rs/canvas](https://github.com/nicolo-ribaudo/napi-rs-canvas) (Rust-based Skia bindings). Includes Yoga flex layout, emoji support, font management, and frame-level Lottie rendering.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @effing/canvas
13
+ ```
14
+
15
+ Requires the `@napi-rs/canvas` peer dependency (typically installed automatically though):
16
+
17
+ ```bash
18
+ npm install @napi-rs/canvas
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { createCanvas, renderReactElement, type FontData } from "@effing/canvas";
25
+ import fs from "node:fs";
26
+
27
+ const font: FontData = {
28
+ name: "Inter",
29
+ data: await fs.readFile("./fonts/Inter-Regular.ttf"),
30
+ weight: 400,
31
+ style: "normal",
32
+ };
33
+
34
+ const canvas = createCanvas(1080, 1080);
35
+ const ctx = canvas.getContext("2d");
36
+
37
+ await renderReactElement(
38
+ ctx,
39
+ <div
40
+ style={{
41
+ width: 1080,
42
+ height: 1080,
43
+ display: "flex",
44
+ alignItems: "center",
45
+ justifyContent: "center",
46
+ backgroundColor: "#1a1a2e",
47
+ color: "white",
48
+ fontSize: 64,
49
+ }}
50
+ >
51
+ Hello World!
52
+ </div>,
53
+ { fonts: [font] },
54
+ );
55
+
56
+ const png = await canvas.encode("png");
57
+ await fs.writeFile("output.png", png);
58
+ ```
59
+
60
+ ## Concepts
61
+
62
+ ### Rendering Pipeline
63
+
64
+ ```
65
+ JSX → Yoga layout → Skia canvas → PNG
66
+ ```
67
+
68
+ 1. **Yoga** calculates flex layout from your JSX tree (flexbox, positioning, text measurement)
69
+ 2. **Skia** draws each node to a canvas context (backgrounds, borders, text, images, SVG, gradients)
70
+ 3. **Encode** the canvas to PNG or JPEG via `canvas.encode()` or `canvas.encodeSync()`
71
+
72
+ Canvas dimensions are taken from the context itself (`ctx.canvas.width` / `ctx.canvas.height`), so there are no `width`/`height` options — just create a canvas at the size you need.
73
+
74
+ ### Font Loading
75
+
76
+ Fonts must be registered before rendering text. You can pass them via the `fonts` option (registered automatically), or register them manually:
77
+
78
+ ```typescript
79
+ import {
80
+ registerFont,
81
+ registerFontFromPath,
82
+ registeredFamilies,
83
+ } from "@effing/canvas";
84
+
85
+ // From a buffer
86
+ registerFont({ name: "Inter", data: fontBuffer, weight: 400, style: "normal" });
87
+
88
+ // From a file path
89
+ registerFontFromPath("./fonts/Inter-Bold.ttf", "Inter");
90
+
91
+ // Check what's registered
92
+ console.log(registeredFamilies()); // ["Inter", ...]
93
+ ```
94
+
95
+ ### Emoji Support
96
+
97
+ Emoji characters are automatically rendered as images from CDNs. Supported styles:
98
+
99
+ | Style | Source |
100
+ | ------------ | ------------------------------ |
101
+ | `twemoji` | Twitter Emoji (default) |
102
+ | `openmoji` | OpenMoji |
103
+ | `blobmoji` | Google Blob Emoji |
104
+ | `noto` | Google Noto Emoji |
105
+ | `fluent` | Microsoft Fluent Emoji (color) |
106
+ | `fluentFlat` | Microsoft Fluent Emoji (flat) |
107
+
108
+ Pass `emoji: "none"` to disable emoji image rendering.
109
+
110
+ ### Lottie Animations
111
+
112
+ Render individual frames of Lottie animations to a canvas:
113
+
114
+ ```typescript
115
+ import { createCanvas, loadLottie, renderLottieFrame } from "@effing/canvas";
116
+
117
+ const anim = loadLottie(fs.readFileSync("animation.json", "utf-8"));
118
+
119
+ const canvas = createCanvas(1080, 1080);
120
+ const ctx = canvas.getContext("2d");
121
+
122
+ renderLottieFrame(ctx, anim, 0); // render frame 0
123
+ const png = canvas.encodeSync("png");
124
+ ```
125
+
126
+ ## API Overview
127
+
128
+ ### `createCanvas(width, height)`
129
+
130
+ Create a new canvas. Re-exported from `@napi-rs/canvas`.
131
+
132
+ ```typescript
133
+ function createCanvas(width: number, height: number): Canvas;
134
+ ```
135
+
136
+ ### `renderReactElement(ctx, element, options)`
137
+
138
+ Render a React element tree to a canvas context.
139
+
140
+ ```typescript
141
+ function renderReactElement(
142
+ ctx: SKRSContext2D,
143
+ element: ReactNode,
144
+ options: RenderReactElementOptions,
145
+ ): Promise<void>;
146
+ ```
147
+
148
+ ### `loadLottie(data, options?)`
149
+
150
+ Load a Lottie animation from a JSON string or Buffer.
151
+
152
+ ```typescript
153
+ function loadLottie(
154
+ data: string | Buffer,
155
+ options?: { resourcePath?: string },
156
+ ): LottieAnimation;
157
+ ```
158
+
159
+ ### `renderLottieFrame(ctx, animation, frame)`
160
+
161
+ Render a specific frame of a Lottie animation to a canvas context.
162
+
163
+ ```typescript
164
+ function renderLottieFrame(
165
+ ctx: SKRSContext2D,
166
+ animation: LottieAnimation,
167
+ frame: number,
168
+ ): void;
169
+ ```
170
+
171
+ ### Font Helpers
172
+
173
+ - `registerFont(font)` — Register a font from a `FontData` buffer (idempotent)
174
+ - `registerFontFromPath(path, nameAlias?)` — Register a font from a file path
175
+ - `registeredFamilies()` — Get registered font family names
176
+
177
+ ### Options
178
+
179
+ | Option | Type | Required | Description |
180
+ | ------- | ---------------------- | -------- | ---------------------------------------- |
181
+ | `fonts` | `FontData[]` | Yes | Font data for text rendering |
182
+ | `debug` | `boolean` | No | Draw layout bounding boxes for debugging |
183
+ | `emoji` | `EmojiStyle \| "none"` | No | Emoji style (default: `"twemoji"`) |
184
+
185
+ ### Types
186
+
187
+ ```typescript
188
+ type FontData = {
189
+ name: string;
190
+ data: Buffer | ArrayBuffer;
191
+ weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
192
+ style: "normal" | "italic";
193
+ };
194
+
195
+ type EmojiStyle =
196
+ | "twemoji"
197
+ | "openmoji"
198
+ | "blobmoji"
199
+ | "noto"
200
+ | "fluent"
201
+ | "fluentFlat";
202
+ ```
203
+
204
+ Also re-exports from `@napi-rs/canvas`: `Canvas`, `SKRSContext2D`, `GlobalFonts`, `loadImage`, `Image`, `LottieAnimation`.
205
+
206
+ ## Examples
207
+
208
+ ### Animation Frames with Tween
209
+
210
+ ```typescript
211
+ import { createCanvas, renderReactElement } from "@effing/canvas";
212
+ import { tween, easeOutQuad } from "@effing/tween";
213
+ import { annieStream } from "@effing/annie";
214
+
215
+ async function* generateFrames() {
216
+ yield* tween(90, async ({ lower: progress }) => {
217
+ const scale = 1 + 0.3 * easeOutQuad(progress);
218
+
219
+ const canvas = createCanvas(1080, 1920);
220
+ const ctx = canvas.getContext("2d");
221
+
222
+ await renderReactElement(
223
+ ctx,
224
+ <div
225
+ style={{
226
+ width: 1080,
227
+ height: 1920,
228
+ display: "flex",
229
+ alignItems: "center",
230
+ justifyContent: "center",
231
+ transform: `scale(${scale})`,
232
+ fontSize: 72,
233
+ color: "white",
234
+ backgroundColor: "#1a1a2e",
235
+ }}
236
+ >
237
+ Animated!
238
+ </div>,
239
+ { fonts },
240
+ );
241
+
242
+ return canvas.encode("png");
243
+ });
244
+ }
245
+
246
+ const stream = annieStream(generateFrames());
247
+ ```
248
+
249
+ ### Lottie Animation to Frames
250
+
251
+ ```typescript
252
+ import { createCanvas, loadLottie, renderLottieFrame } from "@effing/canvas";
253
+
254
+ const anim = loadLottie(fs.readFileSync("confetti.json", "utf-8"));
255
+ const totalFrames = 60;
256
+
257
+ for (let i = 0; i < totalFrames; i++) {
258
+ const canvas = createCanvas(1080, 1080);
259
+ const ctx = canvas.getContext("2d");
260
+
261
+ renderLottieFrame(ctx, anim, i);
262
+
263
+ const png = canvas.encodeSync("png");
264
+ fs.writeFileSync(`frame-${String(i).padStart(3, "0")}.png`, png);
265
+ }
266
+ ```
267
+
268
+ ### Debug Mode
269
+
270
+ Pass `debug: true` to visualize layout bounding boxes:
271
+
272
+ ```typescript
273
+ await renderReactElement(ctx, <MyComponent />, {
274
+ fonts,
275
+ debug: true,
276
+ });
277
+ ```
278
+
279
+ ## Related Packages
280
+
281
+ - [`@effing/tween`](../tween) — Step iteration and easing for frame generation
282
+ - [`@effing/annie`](../annie) — Package rendered frames into animations
283
+ - [`@effing/satori`](../satori) — Alternative JSX-to-PNG renderer (Satori + Resvg)
@@ -0,0 +1,119 @@
1
+ import * as _napi_rs_canvas from '@napi-rs/canvas';
2
+ import { LottieAnimation, SKRSContext2D } from '@napi-rs/canvas';
3
+ export { Canvas, GlobalFonts, Image, LottieAnimation, SKRSContext2D, loadImage } from '@napi-rs/canvas';
4
+ import { ReactNode } from 'react';
5
+
6
+ /**
7
+ * Load a Lottie animation from a JSON string or Buffer.
8
+ *
9
+ * @param data - Lottie JSON string or Buffer
10
+ * @param options - Optional resource path for external assets
11
+ * @returns A `LottieAnimation` handle ready for rendering
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const anim = loadLottie(fs.readFileSync("animation.json", "utf-8"));
16
+ * ```
17
+ */
18
+ declare function loadLottie(data: string | Buffer, options?: {
19
+ resourcePath?: string;
20
+ }): LottieAnimation;
21
+ /**
22
+ * Render a specific frame of a Lottie animation to a canvas context.
23
+ *
24
+ * Seeks the animation to the given frame, then renders it onto the
25
+ * provided context. The canvas dimensions determine the render size.
26
+ *
27
+ * @param ctx - Canvas 2D rendering context to draw into
28
+ * @param animation - Lottie animation handle (from {@link loadLottie})
29
+ * @param frame - Zero-based frame number to render
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { createCanvas, loadLottie, renderLottieFrame } from "@effing/canvas";
34
+ *
35
+ * const canvas = createCanvas(1080, 1080);
36
+ * const ctx = canvas.getContext("2d");
37
+ * const anim = loadLottie(jsonString);
38
+ *
39
+ * renderLottieFrame(ctx, anim, 0);
40
+ * const png = canvas.encodeSync("png");
41
+ * ```
42
+ */
43
+ declare function renderLottieFrame(ctx: SKRSContext2D, animation: LottieAnimation, frame: number): void;
44
+
45
+ /**
46
+ * Emoji style options for rendering
47
+ */
48
+ type EmojiStyle = "twemoji" | "openmoji" | "blobmoji" | "noto" | "fluent" | "fluentFlat";
49
+
50
+ /**
51
+ * Font data for text rendering.
52
+ * Compatible with `@effing/satori`'s FontData type.
53
+ */
54
+ type FontData = {
55
+ name: string;
56
+ data: Buffer | ArrayBuffer;
57
+ weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
58
+ style: "normal" | "italic";
59
+ };
60
+ /**
61
+ * Options for {@link renderReactElement}.
62
+ */
63
+ type RenderReactElementOptions = {
64
+ /** Font data for text rendering */
65
+ fonts: FontData[];
66
+ /** Draw layout bounding boxes for debugging */
67
+ debug?: boolean;
68
+ /** Emoji style for rendering emoji characters as images. Defaults to "twemoji". Pass "none" to disable. */
69
+ emoji?: EmojiStyle | "none";
70
+ };
71
+
72
+ /**
73
+ * Render a React element tree to a canvas context.
74
+ *
75
+ * Width and height are taken from the canvas itself (`ctx.canvas.width` /
76
+ * `ctx.canvas.height`).
77
+ *
78
+ * @param ctx - Canvas 2D rendering context to draw into
79
+ * @param element - React element tree to render
80
+ * @param options - Rendering options (fonts, debug mode)
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * import { createCanvas, renderReactElement } from "@effing/canvas";
85
+ *
86
+ * const canvas = createCanvas(1080, 1080);
87
+ * const ctx = canvas.getContext("2d");
88
+ *
89
+ * await renderReactElement(ctx, <MyComponent />, { fonts: [myFont] });
90
+ *
91
+ * const png = canvas.encodeSync("png");
92
+ * ```
93
+ */
94
+ declare function renderReactElement(ctx: SKRSContext2D, element: ReactNode, options: RenderReactElementOptions): Promise<void>;
95
+
96
+ /**
97
+ * Register a font from a FontData buffer.
98
+ * Registration is idempotent — re-registering the same font name is a no-op.
99
+ *
100
+ * @param font - Font data to register
101
+ */
102
+ declare function registerFont(font: FontData): void;
103
+ /**
104
+ * Register a font from a file path.
105
+ *
106
+ * @param path - Path to the font file
107
+ * @param nameAlias - Optional font family name override
108
+ */
109
+ declare function registerFontFromPath(path: string, nameAlias?: string): void;
110
+ /**
111
+ * Get the list of registered font family names.
112
+ *
113
+ * @returns Array of font family names
114
+ */
115
+ declare function registeredFamilies(): string[];
116
+
117
+ declare function createCanvas(width: number, height: number): _napi_rs_canvas.Canvas;
118
+
119
+ export { type EmojiStyle, type FontData, type RenderReactElementOptions, createCanvas, loadLottie, registerFont, registerFontFromPath, registeredFamilies, renderLottieFrame, renderReactElement };