@effing/canvas 0.19.3 → 0.20.1
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 +115 -50
- 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
|
@@ -21,7 +21,7 @@ function renderLottieFrame(ctx, animation, frame) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// src/jsx/draw/index.ts
|
|
24
|
-
import {
|
|
24
|
+
import { loadImage as loadImage3 } from "@napi-rs/canvas";
|
|
25
25
|
|
|
26
26
|
// src/jsx/language.ts
|
|
27
27
|
function isEmoji(char) {
|
|
@@ -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,
|
|
@@ -737,6 +752,37 @@ async function drawImage(ctx, src, x, y, width, height, style, preloadedImage) {
|
|
|
737
752
|
}
|
|
738
753
|
}
|
|
739
754
|
|
|
755
|
+
// src/jsx/draw/offscreen.ts
|
|
756
|
+
import { createCanvas as createCanvas2 } from "@napi-rs/canvas";
|
|
757
|
+
var canvasPool = /* @__PURE__ */ new Map();
|
|
758
|
+
function acquireOffscreen(w, h) {
|
|
759
|
+
const key = `${w}x${h}`;
|
|
760
|
+
const stack = canvasPool.get(key);
|
|
761
|
+
if (stack) {
|
|
762
|
+
while (stack.length > 0) {
|
|
763
|
+
const ref = stack.pop();
|
|
764
|
+
const canvas2 = ref.deref();
|
|
765
|
+
if (canvas2) {
|
|
766
|
+
const ctx = canvas2.getContext("2d");
|
|
767
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
768
|
+
ctx.clearRect(0, 0, w, h);
|
|
769
|
+
return [canvas2, ctx];
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const canvas = createCanvas2(w, h);
|
|
774
|
+
return [canvas, canvas.getContext("2d")];
|
|
775
|
+
}
|
|
776
|
+
function releaseOffscreen(canvas) {
|
|
777
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
778
|
+
let stack = canvasPool.get(key);
|
|
779
|
+
if (!stack) {
|
|
780
|
+
stack = [];
|
|
781
|
+
canvasPool.set(key, stack);
|
|
782
|
+
}
|
|
783
|
+
stack.push(new WeakRef(canvas));
|
|
784
|
+
}
|
|
785
|
+
|
|
740
786
|
// src/jsx/draw/utils.ts
|
|
741
787
|
function parseCSSLength(value, referenceSize) {
|
|
742
788
|
if (value.endsWith("%")) return parseFloat(value) / 100 * referenceSize;
|
|
@@ -911,17 +957,19 @@ function mergeStyleIntoProps(props) {
|
|
|
911
957
|
function collectDefs(children) {
|
|
912
958
|
const clips = /* @__PURE__ */ new Map();
|
|
913
959
|
const gradients = /* @__PURE__ */ new Map();
|
|
960
|
+
const masks = /* @__PURE__ */ new Map();
|
|
914
961
|
for (const child of children) {
|
|
915
962
|
if (child.type !== "defs") continue;
|
|
916
963
|
for (const def of normalizeChildren(child)) {
|
|
917
964
|
const id = def.props.id;
|
|
918
965
|
if (!id) continue;
|
|
919
966
|
if (def.type === "clipPath") clips.set(id, normalizeChildren(def));
|
|
967
|
+
else if (def.type === "mask") masks.set(id, normalizeChildren(def));
|
|
920
968
|
else if (def.type === "radialGradient" || def.type === "linearGradient")
|
|
921
969
|
gradients.set(id, def);
|
|
922
970
|
}
|
|
923
971
|
}
|
|
924
|
-
return { clips, gradients };
|
|
972
|
+
return { clips, gradients, masks };
|
|
925
973
|
}
|
|
926
974
|
function parseUrlRef(value) {
|
|
927
975
|
if (typeof value !== "string") return void 0;
|
|
@@ -1211,13 +1259,23 @@ function buildPath(child, vbW, vbH) {
|
|
|
1211
1259
|
return p;
|
|
1212
1260
|
}
|
|
1213
1261
|
case "rect": {
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1262
|
+
const x = svgLength(props.x, vbW);
|
|
1263
|
+
const y = svgLength(props.y, vbH);
|
|
1216
1264
|
const w = svgLength(props.width, vbW);
|
|
1217
1265
|
const h = svgLength(props.height, vbH);
|
|
1218
1266
|
if (w <= 0 || h <= 0) return void 0;
|
|
1267
|
+
const rxRaw = svgLength(props.rx, vbW, -1);
|
|
1268
|
+
const ryRaw = svgLength(props.ry, vbH, -1);
|
|
1269
|
+
let rx = rxRaw >= 0 ? rxRaw : ryRaw >= 0 ? ryRaw : 0;
|
|
1270
|
+
let ry = ryRaw >= 0 ? ryRaw : rx;
|
|
1271
|
+
rx = Math.min(rx, w / 2);
|
|
1272
|
+
ry = Math.min(ry, h / 2);
|
|
1219
1273
|
const p = new Path2D();
|
|
1220
|
-
|
|
1274
|
+
if (rx > 0 || ry > 0) {
|
|
1275
|
+
p.roundRect(x, y, w, h, [rx]);
|
|
1276
|
+
} else {
|
|
1277
|
+
p.rect(x, y, w, h);
|
|
1278
|
+
}
|
|
1221
1279
|
return p;
|
|
1222
1280
|
}
|
|
1223
1281
|
case "ellipse": {
|
|
@@ -1294,43 +1352,67 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
|
1294
1352
|
}
|
|
1295
1353
|
var EMPTY_DEFS = {
|
|
1296
1354
|
clips: /* @__PURE__ */ new Map(),
|
|
1297
|
-
gradients: /* @__PURE__ */ new Map()
|
|
1355
|
+
gradients: /* @__PURE__ */ new Map(),
|
|
1356
|
+
masks: /* @__PURE__ */ new Map()
|
|
1298
1357
|
};
|
|
1299
1358
|
function drawSvgChild(ctx, child, inheritedFill, color, defs = EMPTY_DEFS, vbW = 0, vbH = 0) {
|
|
1300
1359
|
const { type } = child;
|
|
1301
1360
|
const props = mergeStyleIntoProps(child.props);
|
|
1302
|
-
if (type === "defs" || type === "clipPath") return;
|
|
1361
|
+
if (type === "defs" || type === "clipPath" || type === "mask") return;
|
|
1303
1362
|
const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
|
|
1304
1363
|
const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
|
|
1305
1364
|
const clipPath = clipShapes ? buildClipPath(clipShapes, vbW, vbH) : void 0;
|
|
1365
|
+
const maskRef = parseUrlRef(props.mask);
|
|
1366
|
+
const maskShapes = maskRef ? defs.masks.get(maskRef) : void 0;
|
|
1306
1367
|
if (clipPath) ctx.save();
|
|
1307
1368
|
if (clipPath) ctx.clip(clipPath);
|
|
1369
|
+
let elemCanvas;
|
|
1370
|
+
const targetCtx = maskShapes ? (() => {
|
|
1371
|
+
const [c, cCtx] = acquireOffscreen(Math.ceil(vbW), Math.ceil(vbH));
|
|
1372
|
+
elemCanvas = c;
|
|
1373
|
+
return cCtx;
|
|
1374
|
+
})() : ctx;
|
|
1308
1375
|
switch (type) {
|
|
1309
1376
|
case "path":
|
|
1310
|
-
drawPath(
|
|
1377
|
+
drawPath(targetCtx, props, inheritedFill, color, defs);
|
|
1311
1378
|
break;
|
|
1312
1379
|
case "circle":
|
|
1313
|
-
drawCircle(
|
|
1380
|
+
drawCircle(targetCtx, props, inheritedFill, color, defs, vbW, vbH);
|
|
1314
1381
|
break;
|
|
1315
1382
|
case "rect":
|
|
1316
|
-
drawSvgRect(
|
|
1383
|
+
drawSvgRect(targetCtx, props, inheritedFill, color, defs, vbW, vbH);
|
|
1317
1384
|
break;
|
|
1318
1385
|
case "line":
|
|
1319
|
-
drawLine(
|
|
1386
|
+
drawLine(targetCtx, props, color, defs, vbW, vbH);
|
|
1320
1387
|
break;
|
|
1321
1388
|
case "ellipse":
|
|
1322
|
-
drawEllipse(
|
|
1389
|
+
drawEllipse(targetCtx, props, inheritedFill, color, defs, vbW, vbH);
|
|
1323
1390
|
break;
|
|
1324
1391
|
case "polygon":
|
|
1325
|
-
drawPolygon(
|
|
1392
|
+
drawPolygon(targetCtx, props, inheritedFill, color, defs);
|
|
1326
1393
|
break;
|
|
1327
1394
|
case "polyline":
|
|
1328
|
-
drawPolyline(
|
|
1395
|
+
drawPolyline(targetCtx, props, inheritedFill, color, defs);
|
|
1329
1396
|
break;
|
|
1330
1397
|
case "g":
|
|
1331
|
-
drawGroup(
|
|
1398
|
+
drawGroup(targetCtx, child, inheritedFill, color, defs, vbW, vbH);
|
|
1332
1399
|
break;
|
|
1333
1400
|
}
|
|
1401
|
+
if (maskShapes && elemCanvas) {
|
|
1402
|
+
const [maskCanvas, maskCtx] = acquireOffscreen(
|
|
1403
|
+
Math.ceil(vbW),
|
|
1404
|
+
Math.ceil(vbH)
|
|
1405
|
+
);
|
|
1406
|
+
for (const shape of maskShapes) {
|
|
1407
|
+
drawSvgChild(maskCtx, shape, "white", "white", defs, vbW, vbH);
|
|
1408
|
+
}
|
|
1409
|
+
targetCtx.globalCompositeOperation = "destination-in";
|
|
1410
|
+
targetCtx.drawImage(maskCanvas, 0, 0);
|
|
1411
|
+
targetCtx.globalCompositeOperation = "source-over";
|
|
1412
|
+
ctx.drawImage(elemCanvas, 0, 0);
|
|
1413
|
+
releaseOffscreen(maskCanvas);
|
|
1414
|
+
releaseOffscreen(elemCanvas);
|
|
1415
|
+
}
|
|
1334
1416
|
if (clipPath) ctx.restore();
|
|
1335
1417
|
}
|
|
1336
1418
|
function drawPath(ctx, props, inheritedFill, color, defs) {
|
|
@@ -1351,14 +1433,24 @@ function drawCircle(ctx, props, inheritedFill, color, defs, vbW = 0, vbH = 0) {
|
|
|
1351
1433
|
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
1352
1434
|
}
|
|
1353
1435
|
function drawSvgRect(ctx, props, inheritedFill, color, defs, vbW = 0, vbH = 0) {
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1436
|
+
const x = svgLength(props.x, vbW);
|
|
1437
|
+
const y = svgLength(props.y, vbH);
|
|
1356
1438
|
const w = svgLength(props.width, vbW);
|
|
1357
1439
|
const h = svgLength(props.height, vbH);
|
|
1358
1440
|
if (w <= 0 || h <= 0) return;
|
|
1441
|
+
const rxRaw = svgLength(props.rx, vbW, -1);
|
|
1442
|
+
const ryRaw = svgLength(props.ry, vbH, -1);
|
|
1443
|
+
let rx = rxRaw >= 0 ? rxRaw : ryRaw >= 0 ? ryRaw : 0;
|
|
1444
|
+
let ry = ryRaw >= 0 ? ryRaw : rx;
|
|
1445
|
+
rx = Math.min(rx, w / 2);
|
|
1446
|
+
ry = Math.min(ry, h / 2);
|
|
1359
1447
|
const path = new Path2D();
|
|
1360
|
-
|
|
1361
|
-
|
|
1448
|
+
if (rx > 0 || ry > 0) {
|
|
1449
|
+
path.roundRect(x, y, w, h, [rx]);
|
|
1450
|
+
} else {
|
|
1451
|
+
path.rect(x, y, w, h);
|
|
1452
|
+
}
|
|
1453
|
+
const bbox = { x, y, width: w, height: h };
|
|
1362
1454
|
applyFillAndStroke(ctx, props, path, inheritedFill, color, defs, bbox);
|
|
1363
1455
|
}
|
|
1364
1456
|
function drawLine(ctx, props, color, defs, vbW = 0, vbH = 0) {
|
|
@@ -1761,34 +1853,6 @@ function drawTextDecoration(ctx, seg, offsetX, offsetY) {
|
|
|
1761
1853
|
}
|
|
1762
1854
|
|
|
1763
1855
|
// src/jsx/draw/index.ts
|
|
1764
|
-
var canvasPool = /* @__PURE__ */ new Map();
|
|
1765
|
-
function acquireOffscreen(w, h) {
|
|
1766
|
-
const key = `${w}x${h}`;
|
|
1767
|
-
const stack = canvasPool.get(key);
|
|
1768
|
-
if (stack) {
|
|
1769
|
-
while (stack.length > 0) {
|
|
1770
|
-
const ref = stack.pop();
|
|
1771
|
-
const canvas2 = ref.deref();
|
|
1772
|
-
if (canvas2) {
|
|
1773
|
-
const ctx = canvas2.getContext("2d");
|
|
1774
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1775
|
-
ctx.clearRect(0, 0, w, h);
|
|
1776
|
-
return [canvas2, ctx];
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
const canvas = createCanvas2(w, h);
|
|
1781
|
-
return [canvas, canvas.getContext("2d")];
|
|
1782
|
-
}
|
|
1783
|
-
function releaseOffscreen(canvas) {
|
|
1784
|
-
const key = `${canvas.width}x${canvas.height}`;
|
|
1785
|
-
let stack = canvasPool.get(key);
|
|
1786
|
-
if (!stack) {
|
|
1787
|
-
stack = [];
|
|
1788
|
-
canvasPool.set(key, stack);
|
|
1789
|
-
}
|
|
1790
|
-
stack.push(new WeakRef(canvas));
|
|
1791
|
-
}
|
|
1792
1856
|
async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
1793
1857
|
const x = parentX + node.x;
|
|
1794
1858
|
const y = parentY + node.y;
|
|
@@ -2363,6 +2427,7 @@ var INHERITABLE_PROPS = [
|
|
|
2363
2427
|
"whiteSpace",
|
|
2364
2428
|
"wordBreak",
|
|
2365
2429
|
"textOverflow",
|
|
2430
|
+
"lineClamp",
|
|
2366
2431
|
"textBoxTrim",
|
|
2367
2432
|
"textBoxEdge"
|
|
2368
2433
|
];
|
|
@@ -3021,8 +3086,8 @@ function hasElementChildren(children) {
|
|
|
3021
3086
|
// src/jsx/index.ts
|
|
3022
3087
|
async function renderReactElement(ctx, element, options) {
|
|
3023
3088
|
ensureFontsRegistered(options.fonts);
|
|
3024
|
-
const width = ctx.canvas.width;
|
|
3025
|
-
const height = ctx.canvas.height;
|
|
3089
|
+
const width = options.width ?? ctx.canvas.width;
|
|
3090
|
+
const height = options.height ?? ctx.canvas.height;
|
|
3026
3091
|
const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
|
|
3027
3092
|
const fontFamilies = [...new Set(options.fonts.map((f) => f.name))];
|
|
3028
3093
|
const layoutTree = await buildLayoutTree(
|