@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 +11 -0
- package/README.md +283 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +2070 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
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)
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|