@effing/canvas 0.19.3 → 0.20.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 +135 -6
- package/dist/index.d.ts +22 -3
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ JSX → Yoga layout → Skia canvas → PNG
|
|
|
69
69
|
2. **Skia** draws each node to a canvas context (backgrounds, borders, text, images, SVG, gradients)
|
|
70
70
|
3. **Encode** the canvas to PNG or JPEG via `canvas.encode()` or `canvas.encodeSync()`
|
|
71
71
|
|
|
72
|
-
Canvas dimensions
|
|
72
|
+
Canvas dimensions default to `ctx.canvas.width` / `ctx.canvas.height`, but you can override them with the `width` and `height` options. This is useful for HiDPI rendering where the canvas is larger than the logical layout size.
|
|
73
73
|
|
|
74
74
|
### Font Loading
|
|
75
75
|
|
|
@@ -123,6 +123,114 @@ renderLottieFrame(ctx, anim, 0); // render frame 0
|
|
|
123
123
|
const png = canvas.encodeSync("png");
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
## Supported CSS Properties
|
|
127
|
+
|
|
128
|
+
### Layout
|
|
129
|
+
|
|
130
|
+
| Property | Values / Notes |
|
|
131
|
+
| -------------------------------- | --------------------------------------------------- |
|
|
132
|
+
| `display` | `flex`, `none` |
|
|
133
|
+
| `position` | `relative`, `absolute` |
|
|
134
|
+
| `top`, `right`, `bottom`, `left` | Length, percentage |
|
|
135
|
+
| `overflow` | `visible`, `hidden` (also `overflowX`, `overflowY`) |
|
|
136
|
+
|
|
137
|
+
### Flexbox
|
|
138
|
+
|
|
139
|
+
| Property | Values / Notes |
|
|
140
|
+
| --------------------- | ----------------------------------------------------------------------------------- |
|
|
141
|
+
| `flexDirection` | `row`, `column`, `row-reverse`, `column-reverse` |
|
|
142
|
+
| `flexWrap` | `nowrap`, `wrap`, `wrap-reverse` |
|
|
143
|
+
| `justifyContent` | `flex-start`, `flex-end`, `center`, `space-between`, `space-around`, `space-evenly` |
|
|
144
|
+
| `alignItems` | `flex-start`, `flex-end`, `center`, `stretch`, `baseline` |
|
|
145
|
+
| `alignSelf` | `auto`, `flex-start`, `flex-end`, `center`, `stretch`, `baseline` |
|
|
146
|
+
| `alignContent` | `flex-start`, `flex-end`, `center`, `stretch`, `space-between`, `space-around` |
|
|
147
|
+
| `flex` | Shorthand for `flexGrow`, `flexShrink`, `flexBasis` |
|
|
148
|
+
| `flexGrow` | Number |
|
|
149
|
+
| `flexShrink` | Number |
|
|
150
|
+
| `flexBasis` | Length, percentage |
|
|
151
|
+
| `gap` | Shorthand for `rowGap`, `columnGap` |
|
|
152
|
+
| `rowGap`, `columnGap` | Number |
|
|
153
|
+
|
|
154
|
+
### Dimensions
|
|
155
|
+
|
|
156
|
+
| Property | Values / Notes |
|
|
157
|
+
| ----------------------- | ------------------ |
|
|
158
|
+
| `width`, `height` | Length, percentage |
|
|
159
|
+
| `minWidth`, `minHeight` | Length, percentage |
|
|
160
|
+
| `maxWidth`, `maxHeight` | Length, percentage |
|
|
161
|
+
|
|
162
|
+
### Spacing
|
|
163
|
+
|
|
164
|
+
| Property | Values / Notes |
|
|
165
|
+
| ------------------------------------------------------------ | --------------------------------------- |
|
|
166
|
+
| `margin` | Shorthand (1–4 values), supports `auto` |
|
|
167
|
+
| `marginTop`, `marginRight`, `marginBottom`, `marginLeft` | Length, percentage, `auto` |
|
|
168
|
+
| `padding` | Shorthand (1–4 values) |
|
|
169
|
+
| `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` | Length, percentage |
|
|
170
|
+
|
|
171
|
+
### Border
|
|
172
|
+
|
|
173
|
+
| Property | Values / Notes |
|
|
174
|
+
| -------------------------------------------------------- | ----------------------------------- |
|
|
175
|
+
| `border` | Shorthand, e.g. `"1px solid black"` |
|
|
176
|
+
| `borderTop`, `borderRight`, `borderBottom`, `borderLeft` | Per-side shorthand |
|
|
177
|
+
| `borderWidth`, `borderColor`, `borderStyle` | Shorthand (1–4 values) |
|
|
178
|
+
| `border{Top,Right,Bottom,Left}{Width,Color,Style}` | Per-side longhands |
|
|
179
|
+
| `borderRadius` | Shorthand (1–4 values) |
|
|
180
|
+
| `border{TopLeft,TopRight,BottomRight,BottomLeft}Radius` | Per-corner longhands |
|
|
181
|
+
|
|
182
|
+
### Color & Background
|
|
183
|
+
|
|
184
|
+
| Property | Values / Notes |
|
|
185
|
+
| ----------------- | -------------------------------------------------- |
|
|
186
|
+
| `color` | Any CSS color (inherited) |
|
|
187
|
+
| `backgroundColor` | Any CSS color |
|
|
188
|
+
| `opacity` | Number (0–1) |
|
|
189
|
+
| `background` | Shorthand → `backgroundColor` or `backgroundImage` |
|
|
190
|
+
| `backgroundImage` | `linear-gradient()`, `radial-gradient()`, `url()` |
|
|
191
|
+
| `backgroundSize` | `cover`, `contain`, length, percentage |
|
|
192
|
+
|
|
193
|
+
### Typography
|
|
194
|
+
|
|
195
|
+
| Property | Values / Notes |
|
|
196
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
197
|
+
| `fontFamily` | Font name (inherited) |
|
|
198
|
+
| `fontSize` | Number or CSS length (inherited) |
|
|
199
|
+
| `fontWeight` | Numeric weight (inherited) |
|
|
200
|
+
| `fontStyle` | `normal`, `italic` (inherited) |
|
|
201
|
+
| `textAlign` | `left`, `center`, `right`, `justify` (inherited) |
|
|
202
|
+
| `textDecoration` | String (inherited) |
|
|
203
|
+
| `textTransform` | `none`, `uppercase`, `lowercase`, `capitalize` (inherited) |
|
|
204
|
+
| `lineHeight` | Number or string (inherited) |
|
|
205
|
+
| `letterSpacing` | Number or CSS length (inherited) |
|
|
206
|
+
| `whiteSpace` | `normal`, `nowrap`, `pre`, `pre-wrap`, `pre-line` (inherited) |
|
|
207
|
+
| `wordBreak` | `normal`, `break-all`, `break-word`, `keep-all` (inherited) |
|
|
208
|
+
| `textOverflow` | `clip`, `ellipsis` (inherited) |
|
|
209
|
+
| `lineClamp` | Number — max visible lines (adds ellipsis) |
|
|
210
|
+
| `textBox` | Shorthand for `textBoxTrim` and `textBoxEdge` |
|
|
211
|
+
| `textBoxTrim` | `none`, `trim-start`, `trim-end`, `trim-both` (inherited) |
|
|
212
|
+
| `textBoxEdge` | e.g. `"cap alphabetic"` (inherited) |
|
|
213
|
+
|
|
214
|
+
### Effects
|
|
215
|
+
|
|
216
|
+
| Property | Values / Notes |
|
|
217
|
+
| ----------------- | ------------------------------------------------ |
|
|
218
|
+
| `boxShadow` | CSS box-shadow string |
|
|
219
|
+
| `textShadow` | CSS text-shadow string |
|
|
220
|
+
| `transform` | `translate`, `scale`, `rotate`, `skewX`, `skewY` |
|
|
221
|
+
| `transformOrigin` | CSS transform-origin string |
|
|
222
|
+
| `filter` | CSS filter string |
|
|
223
|
+
|
|
224
|
+
### Image
|
|
225
|
+
|
|
226
|
+
| Property | Values / Notes |
|
|
227
|
+
| ----------- | ------------------------------------------------ |
|
|
228
|
+
| `objectFit` | `contain`, `cover`, `fill`, `none`, `scale-down` |
|
|
229
|
+
|
|
230
|
+
### Units
|
|
231
|
+
|
|
232
|
+
`px`, `em`, `rem`, `%`, `vw`, `vh`, `vmin`, `vmax`, `pt`, `pc`, `in`, `cm`, `mm`
|
|
233
|
+
|
|
126
234
|
## API Overview
|
|
127
235
|
|
|
128
236
|
### `createCanvas(width, height)`
|
|
@@ -176,11 +284,13 @@ function renderLottieFrame(
|
|
|
176
284
|
|
|
177
285
|
### Options
|
|
178
286
|
|
|
179
|
-
| Option
|
|
180
|
-
|
|
|
181
|
-
| `fonts`
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
287
|
+
| Option | Type | Required | Description |
|
|
288
|
+
| -------- | ---------------------- | -------- | ----------------------------------------------------- |
|
|
289
|
+
| `fonts` | `FontData[]` | Yes | Font data for text rendering |
|
|
290
|
+
| `width` | `number` | No | Layout width override (default: `ctx.canvas.width`) |
|
|
291
|
+
| `height` | `number` | No | Layout height override (default: `ctx.canvas.height`) |
|
|
292
|
+
| `debug` | `boolean` | No | Draw layout bounding boxes for debugging |
|
|
293
|
+
| `emoji` | `EmojiStyle \| "none"` | No | Emoji style (default: `"twemoji"`) |
|
|
184
294
|
|
|
185
295
|
### Types
|
|
186
296
|
|
|
@@ -276,6 +386,25 @@ await renderReactElement(ctx, <MyComponent />, {
|
|
|
276
386
|
});
|
|
277
387
|
```
|
|
278
388
|
|
|
389
|
+
### HiDPI Rendering
|
|
390
|
+
|
|
391
|
+
For sharp output on high-DPI displays, you can create a larger canvas and use `width`/`height` overrides to keep layout at the logical size:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const dpr = 2;
|
|
395
|
+
const canvas = createCanvas(1080 * dpr, 1080 * dpr);
|
|
396
|
+
const ctx = canvas.getContext("2d");
|
|
397
|
+
ctx.scale(dpr, dpr);
|
|
398
|
+
|
|
399
|
+
await renderReactElement(ctx, <MyComponent />, {
|
|
400
|
+
fonts,
|
|
401
|
+
width: 1080,
|
|
402
|
+
height: 1080,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const png = await canvas.encode("png");
|
|
406
|
+
```
|
|
407
|
+
|
|
279
408
|
## Related Packages
|
|
280
409
|
|
|
281
410
|
- [`@effing/tween`](../tween) — Step iteration and easing for frame generation
|
package/dist/index.d.ts
CHANGED
|
@@ -63,6 +63,10 @@ type FontData = {
|
|
|
63
63
|
type RenderReactElementOptions = {
|
|
64
64
|
/** Font data for text rendering */
|
|
65
65
|
fonts: FontData[];
|
|
66
|
+
/** Layout width override. Defaults to `ctx.canvas.width`. */
|
|
67
|
+
width?: number;
|
|
68
|
+
/** Layout height override. Defaults to `ctx.canvas.height`. */
|
|
69
|
+
height?: number;
|
|
66
70
|
/** Draw layout bounding boxes for debugging */
|
|
67
71
|
debug?: boolean;
|
|
68
72
|
/** Emoji style for rendering emoji characters as images. Defaults to "twemoji". Pass "none" to disable. */
|
|
@@ -72,12 +76,13 @@ type RenderReactElementOptions = {
|
|
|
72
76
|
/**
|
|
73
77
|
* Render a React element tree to a canvas context.
|
|
74
78
|
*
|
|
75
|
-
* Width and height
|
|
76
|
-
* `
|
|
79
|
+
* Width and height default to `ctx.canvas.width` / `ctx.canvas.height` but
|
|
80
|
+
* can be overridden via `options.width` and `options.height`. This is useful
|
|
81
|
+
* for HiDPI rendering where the canvas is larger than the logical layout size.
|
|
77
82
|
*
|
|
78
83
|
* @param ctx - Canvas 2D rendering context to draw into
|
|
79
84
|
* @param element - React element tree to render
|
|
80
|
-
* @param options - Rendering options (fonts, debug mode)
|
|
85
|
+
* @param options - Rendering options (fonts, dimensions, debug mode)
|
|
81
86
|
*
|
|
82
87
|
* @example
|
|
83
88
|
* ```tsx
|
|
@@ -90,6 +95,20 @@ type RenderReactElementOptions = {
|
|
|
90
95
|
*
|
|
91
96
|
* const png = canvas.encodeSync("png");
|
|
92
97
|
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example HiDPI rendering (2x)
|
|
100
|
+
* ```tsx
|
|
101
|
+
* const dpr = 2;
|
|
102
|
+
* const canvas = createCanvas(1080 * dpr, 1080 * dpr);
|
|
103
|
+
* const ctx = canvas.getContext("2d");
|
|
104
|
+
* ctx.scale(dpr, dpr);
|
|
105
|
+
*
|
|
106
|
+
* await renderReactElement(ctx, <MyComponent />, {
|
|
107
|
+
* fonts: [myFont],
|
|
108
|
+
* width: 1080,
|
|
109
|
+
* height: 1080,
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
93
112
|
*/
|
|
94
113
|
declare function renderReactElement(ctx: SKRSContext2D, element: ReactNode, options: RenderReactElementOptions): Promise<void>;
|
|
95
114
|
|
package/dist/index.js
CHANGED
|
@@ -303,6 +303,21 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
303
303
|
);
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
+
const lineClamp = style.lineClamp;
|
|
307
|
+
if (lineClamp && lineClamp > 0 && lines.length > lineClamp) {
|
|
308
|
+
const lastLineText = lines.slice(lineClamp - 1).join(" ");
|
|
309
|
+
lines.length = lineClamp;
|
|
310
|
+
lines[lineClamp - 1] = truncateWithEllipsis(
|
|
311
|
+
lastLineText,
|
|
312
|
+
maxWidth,
|
|
313
|
+
fontSize,
|
|
314
|
+
fontFamily,
|
|
315
|
+
fontWeight,
|
|
316
|
+
fontStyle,
|
|
317
|
+
ctx,
|
|
318
|
+
letterSpacing
|
|
319
|
+
);
|
|
320
|
+
}
|
|
306
321
|
const segments = [];
|
|
307
322
|
let totalHeight = 0;
|
|
308
323
|
let maxLineWidth = 0;
|
|
@@ -490,7 +505,7 @@ function truncateWithEllipsis(text, maxWidth, fontSize, fontFamily, fontWeight,
|
|
|
490
505
|
letterSpacing
|
|
491
506
|
);
|
|
492
507
|
const availWidth = maxWidth - ellipsisWidth;
|
|
493
|
-
for (let i = text.length
|
|
508
|
+
for (let i = text.length; i > 0; i--) {
|
|
494
509
|
const truncated = text.slice(0, i);
|
|
495
510
|
const w = measureWord(
|
|
496
511
|
truncated,
|
|
@@ -2363,6 +2378,7 @@ var INHERITABLE_PROPS = [
|
|
|
2363
2378
|
"whiteSpace",
|
|
2364
2379
|
"wordBreak",
|
|
2365
2380
|
"textOverflow",
|
|
2381
|
+
"lineClamp",
|
|
2366
2382
|
"textBoxTrim",
|
|
2367
2383
|
"textBoxEdge"
|
|
2368
2384
|
];
|
|
@@ -3021,8 +3037,8 @@ function hasElementChildren(children) {
|
|
|
3021
3037
|
// src/jsx/index.ts
|
|
3022
3038
|
async function renderReactElement(ctx, element, options) {
|
|
3023
3039
|
ensureFontsRegistered(options.fonts);
|
|
3024
|
-
const width = ctx.canvas.width;
|
|
3025
|
-
const height = ctx.canvas.height;
|
|
3040
|
+
const width = options.width ?? ctx.canvas.width;
|
|
3041
|
+
const height = options.height ?? ctx.canvas.height;
|
|
3026
3042
|
const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
|
|
3027
3043
|
const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
|
|
3028
3044
|
const layoutTree = await buildLayoutTree(
|