@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 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 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.
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 | 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"`) |
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 are taken from the canvas itself (`ctx.canvas.width` /
76
- * `ctx.canvas.height`).
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 - 1; i > 0; i--) {
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(