freshcoat 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.
Files changed (62) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +17 -0
  3. package/README.md +110 -0
  4. package/package.json +53 -0
  5. package/src/adjust.d.ts +18 -0
  6. package/src/adjust.js +153 -0
  7. package/src/approx-layout.d.ts +2 -0
  8. package/src/approx-layout.js +91 -0
  9. package/src/bake-text.d.ts +12 -0
  10. package/src/bake-text.js +321 -0
  11. package/src/browser.d.ts +7 -0
  12. package/src/browser.js +79 -0
  13. package/src/canvaskit.d.ts +4 -0
  14. package/src/canvaskit.js +1674 -0
  15. package/src/compile-scene.d.ts +24 -0
  16. package/src/compile-scene.js +325 -0
  17. package/src/css.d.ts +5 -0
  18. package/src/css.js +153 -0
  19. package/src/decode.d.ts +22 -0
  20. package/src/decode.js +112 -0
  21. package/src/export-scale.d.ts +25 -0
  22. package/src/export-scale.js +74 -0
  23. package/src/font-bytes.d.ts +4 -0
  24. package/src/font-bytes.js +165 -0
  25. package/src/font-metrics.d.ts +5 -0
  26. package/src/font-metrics.js +62 -0
  27. package/src/headless.d.ts +24 -0
  28. package/src/headless.js +58 -0
  29. package/src/index.d.ts +25 -0
  30. package/src/index.js +29 -0
  31. package/src/jpeg.d.ts +5 -0
  32. package/src/jpeg.js +20 -0
  33. package/src/line-height.d.ts +7 -0
  34. package/src/line-height.js +80 -0
  35. package/src/node.d.ts +139 -0
  36. package/src/node.js +47 -0
  37. package/src/paint-cache.d.ts +49 -0
  38. package/src/paint-cache.js +109 -0
  39. package/src/paint-helpers.d.ts +21 -0
  40. package/src/paint-helpers.js +71 -0
  41. package/src/paragraph-layout.d.ts +6 -0
  42. package/src/paragraph-layout.js +283 -0
  43. package/src/path-data.d.ts +5 -0
  44. package/src/path-data.js +101 -0
  45. package/src/png.d.ts +23 -0
  46. package/src/png.js +189 -0
  47. package/src/resolve-layout.d.ts +9 -0
  48. package/src/resolve-layout.js +657 -0
  49. package/src/runtime.d.ts +6 -0
  50. package/src/runtime.js +44 -0
  51. package/src/squircle.d.ts +1 -0
  52. package/src/squircle.js +60 -0
  53. package/src/text-cache.d.ts +14 -0
  54. package/src/text-cache.js +46 -0
  55. package/src/text-engine.d.ts +26 -0
  56. package/src/text-engine.js +1 -0
  57. package/src/text-types.d.ts +30 -0
  58. package/src/text-types.js +1 -0
  59. package/src/types.d.ts +406 -0
  60. package/src/types.js +13 -0
  61. package/src/validate-commands.d.ts +8 -0
  62. package/src/validate-commands.js +206 -0
@@ -0,0 +1,657 @@
1
+ export function resolveLayout(root, opts) {
2
+ return resolveNode(root, opts.measure);
3
+ }
4
+ function resolveNode(node, measure) {
5
+ if (node.kind === "mask")
6
+ return resolveMask(node, measure);
7
+ if (node.kind !== "group")
8
+ return node;
9
+ if (!node.layout) {
10
+ // A layout-less group is a static container: its children's pos is
11
+ // relative to it. Fold the group's own pos into them so the flattened
12
+ // output is absolute (the painter's space; drawGroup never translates).
13
+ return {
14
+ ...node,
15
+ children: resolveChildren(node.children, node.pos ?? { x: 0, y: 0 }, measure),
16
+ };
17
+ }
18
+ const pos = node.pos ?? { x: 0, y: 0 };
19
+ const size = node.size ?? { width: 0, height: 0 };
20
+ return node.layout.type === "grid"
21
+ ? resolveGrid(node, node.layout, pos, size, measure)
22
+ : resolveFlex(node, node.layout, pos, size, measure);
23
+ }
24
+ // A mask node is a static container: its mask + children are relative to it,
25
+ // folded to absolute (like a layout-less group).
26
+ function resolveMask(node, measure) {
27
+ const offset = node.pos ?? { x: 0, y: 0 };
28
+ return {
29
+ ...node,
30
+ mask: resolveChildren([node.mask], offset, measure)[0],
31
+ children: resolveChildren(node.children, offset, measure),
32
+ };
33
+ }
34
+ const ROW_AXIS = { main: (s) => s.width, cross: (s) => s.height };
35
+ const COLUMN_AXIS = { main: (s) => s.height, cross: (s) => s.width };
36
+ // ─────────────── rotation ───────────────
37
+ /** The axis-aligned box a turned node covers — what its container has to make
38
+ * room for. Identity for an unrotated node, so nothing changes for one. */
39
+ export function rotatedFootprint(size, rotation) {
40
+ if (!rotation)
41
+ return size;
42
+ const rad = (rotation * Math.PI) / 180;
43
+ // Math.cos(π/2) is 6e-17, not 0, and that dust survives into every downstream
44
+ // coordinate. A right angle is the overwhelmingly common case here (a card's
45
+ // vertical label), so snap the axis it zeroes.
46
+ const c = snapZero(Math.abs(Math.cos(rad)));
47
+ const s = snapZero(Math.abs(Math.sin(rad)));
48
+ return {
49
+ width: size.width * c + size.height * s,
50
+ height: size.width * s + size.height * c,
51
+ };
52
+ }
53
+ const snapZero = (n) => (n < 1e-9 ? 0 : n);
54
+ const isRotated = (node) => (node.rotation ?? 0) !== 0;
55
+ // A turned child's own box, resolved in ITS OWN axes. The container's axes are
56
+ // no longer the child's, so `layoutChild.width`/`height` — which describe the
57
+ // node's own sides — can't be read through the container's main/cross mapping,
58
+ // and `fill`/`grow`/`stretch` have nothing to mean (there is no container axis
59
+ // left to fill). Those fall back to the authored size; `hug` and fixed numbers
60
+ // still resolve, which is what a turned label or a turned nested stack needs.
61
+ function ownSizeOf(child, measure) {
62
+ const lc = child.layoutChild ?? {};
63
+ const base = child.size ?? { width: 0, height: 0 };
64
+ const fixed = (mode, fallback) => typeof mode === "number" ? mode : fallback;
65
+ if (child.kind === "text") {
66
+ const width = lc.width === "hug"
67
+ ? measure(textOf(child), child.font, null).width
68
+ : fixed(lc.width, base.width);
69
+ const height = lc.height === "hug"
70
+ ? capLines(measure(textOf(child), child.font, width).height, child)
71
+ : fixed(lc.height, base.height);
72
+ return { width, height };
73
+ }
74
+ if (child.kind === "group" && child.layout?.type === "flex") {
75
+ // resolveFlex hugs exactly the axes layoutChild marks `hug`, so hand it the
76
+ // other two and read the natural box back off the result.
77
+ const probe = {
78
+ width: lc.width === "hug" ? 0 : fixed(lc.width, base.width),
79
+ height: lc.height === "hug" ? 0 : fixed(lc.height, base.height),
80
+ };
81
+ const resolved = resolveFlex(child, child.layout, { x: 0, y: 0 }, probe, measure);
82
+ return resolved.size ?? base;
83
+ }
84
+ return {
85
+ width: fixed(lc.width, base.width),
86
+ height: fixed(lc.height, base.height),
87
+ };
88
+ }
89
+ function normalizePad(p) {
90
+ if (p === undefined)
91
+ return { top: 0, right: 0, bottom: 0, left: 0 };
92
+ if (typeof p === "number")
93
+ return { top: p, right: p, bottom: p, left: p };
94
+ const [top, right, bottom, left] = p;
95
+ return { top, right, bottom, left };
96
+ }
97
+ // ─────────────── the container two-pass ───────────────
98
+ function resolveFlex(group, layout, pos, size, measure) {
99
+ const isRow = layout.direction === "row";
100
+ const axis = isRow ? ROW_AXIS : COLUMN_AXIS;
101
+ const gap = layout.gap ?? 0;
102
+ const crossGap = layout.crossGap ?? gap;
103
+ const p = normalizePad(layout.padding);
104
+ const mainPadTotal = isRow ? p.left + p.right : p.top + p.bottom;
105
+ const crossPadTotal = isRow ? p.top + p.bottom : p.left + p.right;
106
+ const contentMain = axis.main(size) - mainPadTotal;
107
+ const contentCross = axis.cross(size) - crossPadTotal;
108
+ // Absolute placement origin: container pos + leading padding.
109
+ const originMain = isRow ? pos.x : pos.y;
110
+ const originCross = isRow ? pos.y : pos.x;
111
+ const mainStart = originMain + (isRow ? p.left : p.top);
112
+ const crossStart = originCross + (isRow ? p.top : p.left);
113
+ const flow = group.children.filter((c) => c.layoutChild?.absolute !== true);
114
+ const absolute = group.children.filter((c) => c.layoutChild?.absolute === true);
115
+ // Pass 1 — measure each flow child's intrinsic main/cross + grow + clamps.
116
+ const measured = flow.map((child) => measureChild(child, isRow, contentCross, measure));
117
+ // Grow: distribute leftover main-axis space by weight, honoring per-child max.
118
+ const totalMain = measured.reduce((s, m) => s + m.main, 0) +
119
+ gap * Math.max(0, measured.length - 1);
120
+ const free = contentMain - totalMain;
121
+ const growable = measured.filter((m) => m.grow > 0);
122
+ if (growable.length > 0 && free > 0) {
123
+ let remaining = free;
124
+ const open = new Set(growable);
125
+ while (open.size > 0 && remaining > 0.001) {
126
+ const share = remaining / open.size;
127
+ let consumed = 0;
128
+ let pinned = false;
129
+ for (const m of [...open]) {
130
+ const room = m.maxMain - m.main;
131
+ const take = Math.min(share, room);
132
+ m.main += take;
133
+ consumed += take;
134
+ if (take >= room - 0.001) {
135
+ open.delete(m);
136
+ pinned = true;
137
+ }
138
+ }
139
+ remaining -= consumed;
140
+ if (!pinned && consumed <= 0.001)
141
+ break;
142
+ }
143
+ }
144
+ const grownTotal = measured.reduce((s, m) => s + m.main, 0) +
145
+ gap * Math.max(0, measured.length - 1);
146
+ const freeAfterGrow = contentMain - grownTotal;
147
+ // Wrap into lines (single line when wrap is off).
148
+ const lines = [];
149
+ if (layout.wrap) {
150
+ let line = [];
151
+ let used = 0;
152
+ for (const m of measured) {
153
+ const add = (line.length > 0 ? gap : 0) + m.main;
154
+ if (line.length > 0 && used + add > contentMain) {
155
+ lines.push(line);
156
+ line = [m];
157
+ used = m.main;
158
+ }
159
+ else {
160
+ line.push(m);
161
+ used += add;
162
+ }
163
+ }
164
+ if (line.length > 0)
165
+ lines.push(line);
166
+ }
167
+ else {
168
+ lines.push(measured);
169
+ }
170
+ // Pass 2 — place. Justify along main, align along cross, per line.
171
+ const placed = [];
172
+ let lineCrossCursor = crossStart;
173
+ for (const line of lines) {
174
+ const single = lines.length === 1;
175
+ const lineMain = line.reduce((s, m) => s + m.main, 0) + gap * Math.max(0, line.length - 1);
176
+ const lineFree = single ? freeAfterGrow : contentMain - lineMain;
177
+ const dist = distributeMain(layout.justify ?? "start", lineFree, gap, line.length);
178
+ const lineCross = line.reduce((mx, m) => Math.max(mx, m.cross), 0);
179
+ let cursor = mainStart + dist.leading;
180
+ for (const m of line) {
181
+ const alignMode = m.child.layoutChild?.alignSelf ?? layout.align ?? "start";
182
+ const crossExtent = single ? contentCross : lineCross;
183
+ const crossOff = alignCross(alignMode, crossExtent, m.cross);
184
+ const mainCoord = cursor;
185
+ const crossCoord = lineCrossCursor + crossOff;
186
+ const footprint = isRow
187
+ ? { x: mainCoord, y: crossCoord }
188
+ : { x: crossCoord, y: mainCoord };
189
+ const footSize = isRow
190
+ ? { width: m.main, height: m.cross }
191
+ : { width: m.cross, height: m.main };
192
+ // The flow placed the FOOTPRINT. A turned child's own box is centred in
193
+ // it, because that is the point the painter rotates around. Identical to
194
+ // the footprint when nothing is turned.
195
+ const childSize = m.own ?? footSize;
196
+ const abs = {
197
+ x: footprint.x + (footSize.width - childSize.width) / 2,
198
+ y: footprint.y + (footSize.height - childSize.height) / 2,
199
+ };
200
+ cursor += m.main + dist.between;
201
+ placed.push(placeChild(m.child, abs, childSize, measure));
202
+ }
203
+ lineCrossCursor += lineCross + crossGap;
204
+ }
205
+ // Absolute children: placed at container origin + their frame-relative pos,
206
+ // out of flow. Nested layout groups still resolve.
207
+ const placedAbsolute = absolute.map((c) => {
208
+ const local = c.pos ?? { x: 0, y: 0 };
209
+ const abs = { x: pos.x + local.x, y: pos.y + local.y };
210
+ return placeChild(c, abs, c.size ?? { width: 0, height: 0 }, measure);
211
+ });
212
+ // Hug axes grow to enclose content; fixed/fill axes keep the handed-in size.
213
+ const maxCross = measured.reduce((mx, m) => Math.max(mx, m.cross), 0);
214
+ let naturalMain = grownTotal + mainPadTotal;
215
+ let naturalCross = maxCross + crossPadTotal;
216
+ if (layout.wrap) {
217
+ const lineMains = lines.map((line) => line.reduce((s, m) => s + m.main, 0) +
218
+ gap * Math.max(0, line.length - 1));
219
+ const stackedCross = lines.reduce((s, line) => s + line.reduce((mx, m) => Math.max(mx, m.cross), 0), 0) +
220
+ crossGap * Math.max(0, lines.length - 1);
221
+ naturalMain = Math.max(0, ...lineMains) + mainPadTotal;
222
+ naturalCross = stackedCross + crossPadTotal;
223
+ }
224
+ const hugsWidth = group.layoutChild?.width === "hug";
225
+ const hugsHeight = group.layoutChild?.height === "hug";
226
+ const outSize = {
227
+ width: hugsWidth ? (isRow ? naturalMain : naturalCross) : size.width,
228
+ height: hugsHeight ? (isRow ? naturalCross : naturalMain) : size.height,
229
+ };
230
+ return {
231
+ ...stripChild(group),
232
+ pos,
233
+ size: outSize,
234
+ layout: undefined,
235
+ children: [...placed, ...placedAbsolute],
236
+ };
237
+ }
238
+ function placeChild(child, abs, size, measure) {
239
+ if (child.kind === "mask") {
240
+ return {
241
+ ...stripChild(child),
242
+ pos: abs,
243
+ size,
244
+ mask: resolveChildren([child.mask], abs, measure)[0],
245
+ children: resolveChildren(child.children, abs, measure),
246
+ };
247
+ }
248
+ if (child.kind === "group" && child.layout) {
249
+ return child.layout.type === "grid"
250
+ ? resolveGrid(child, child.layout, abs, size, measure)
251
+ : resolveFlex(child, child.layout, abs, size, measure);
252
+ }
253
+ if (child.kind === "group") {
254
+ // Layout-less group placed by a layout parent: fold its resolved absolute
255
+ // pos into its (relative) children so the whole subtree stays absolute.
256
+ return {
257
+ ...stripChild(child),
258
+ pos: abs,
259
+ size,
260
+ children: resolveChildren(child.children, abs, measure),
261
+ };
262
+ }
263
+ return { ...stripChild(child), pos: abs, size };
264
+ }
265
+ // Resolve each child after folding `offset` into its (parent-relative) pos, so
266
+ // a static container's subtree flattens to absolute coordinates.
267
+ function resolveChildren(children, offset, measure) {
268
+ return children.map((c) => {
269
+ const local = c.pos ?? { x: 0, y: 0 };
270
+ const moved = {
271
+ ...c,
272
+ pos: { x: offset.x + local.x, y: offset.y + local.y },
273
+ };
274
+ return resolveNode(moved, measure);
275
+ });
276
+ }
277
+ function normalizeGap(gap) {
278
+ if (gap === undefined)
279
+ return [0, 0];
280
+ if (typeof gap === "number")
281
+ return [gap, gap];
282
+ return gap;
283
+ }
284
+ // 1-based number/[start,end] (inclusive) → 0-based line + span. Undefined = auto.
285
+ function trackSpan(v) {
286
+ if (v === undefined)
287
+ return { line: null, span: 1 };
288
+ if (typeof v === "number")
289
+ return { line: v - 1, span: 1 };
290
+ const [s, e] = v;
291
+ return { line: s - 1, span: Math.max(1, e - s + 1) };
292
+ }
293
+ function resolveGrid(group, layout, pos, size, measure) {
294
+ const p = normalizePad(layout.padding);
295
+ const [rowGap, colGap] = normalizeGap(layout.gap);
296
+ const contentLeft = pos.x + p.left;
297
+ const contentTop = pos.y + p.top;
298
+ const contentWidth = size.width - p.left - p.right;
299
+ const contentHeight = size.height - p.top - p.bottom;
300
+ const colDefs = layout.columns;
301
+ const nCols = colDefs.length;
302
+ const flow = group.children.filter((c) => c.layoutChild?.absolute !== true);
303
+ const absolute = group.children.filter((c) => c.layoutChild?.absolute === true);
304
+ const placements = placeGrid(flow, nCols);
305
+ const maxRow = placements.reduce((m, pl) => Math.max(m, pl.row + pl.rowSpan), 0);
306
+ const explicitRows = layout.rows ?? [];
307
+ const nRows = Math.max(maxRow, explicitRows.length);
308
+ const rowDefs = Array.from({ length: nRows }, (_, i) => explicitRows[i] ?? "auto");
309
+ // Column tracks: auto columns size to the widest single-column item.
310
+ const colAuto = new Array(nCols).fill(0);
311
+ placements.forEach((pl, i) => {
312
+ if (pl.colSpan === 1) {
313
+ colAuto[pl.col] = Math.max(colAuto[pl.col], gridIntrinsicWidth(flow[i], measure));
314
+ }
315
+ });
316
+ const colSizes = resolveTracks(colDefs, contentWidth, colGap, colAuto);
317
+ const colOffsets = trackOffsets(colSizes, colGap, contentLeft);
318
+ // Row tracks: auto rows size to the tallest single-row item measured at its
319
+ // (now known) cell width — the width→height coupling, grid-side.
320
+ const rowAuto = new Array(nRows).fill(0);
321
+ placements.forEach((pl, i) => {
322
+ if (pl.rowSpan === 1) {
323
+ const cellW = spanExtent(colSizes, pl.col, pl.colSpan, colGap);
324
+ rowAuto[pl.row] = Math.max(rowAuto[pl.row], gridIntrinsicHeight(flow[i], cellW, measure));
325
+ }
326
+ });
327
+ const rowSizes = resolveTracks(rowDefs, contentHeight, rowGap, rowAuto);
328
+ const rowOffsets = trackOffsets(rowSizes, rowGap, contentTop);
329
+ const placed = flow.map((child, i) => {
330
+ const pl = placements[i];
331
+ const cellX = colOffsets[pl.col] ?? contentLeft;
332
+ const cellY = rowOffsets[pl.row] ?? contentTop;
333
+ const cellW = spanExtent(colSizes, pl.col, pl.colSpan, colGap);
334
+ const cellH = spanExtent(rowSizes, pl.row, pl.rowSpan, rowGap);
335
+ // A turned item keeps its own box — there is no cell axis left for it to
336
+ // fill — and sits centred on the cell, the point the painter turns it
337
+ // around. An upright item is sized and placed exactly as before.
338
+ if (isRotated(child)) {
339
+ const own = ownSizeOf(child, measure);
340
+ const foot = rotatedFootprint(own, child.rotation);
341
+ return placeChild(child, {
342
+ x: cellX + (cellW - foot.width) / 2 + (foot.width - own.width) / 2,
343
+ y: cellY + (cellH - foot.height) / 2 + (foot.height - own.height) / 2,
344
+ }, own, measure);
345
+ }
346
+ const lc = child.layoutChild ?? {};
347
+ const w = gridItemSize(lc.width, cellW, () => gridIntrinsicWidth(child, measure));
348
+ const h = gridItemSize(lc.height, cellH, () => gridIntrinsicHeight(child, w, measure));
349
+ return placeChild(child, { x: cellX, y: cellY }, { width: w, height: h }, measure);
350
+ });
351
+ const placedAbsolute = absolute.map((c) => {
352
+ const local = c.pos ?? { x: 0, y: 0 };
353
+ const abs = { x: pos.x + local.x, y: pos.y + local.y };
354
+ return placeChild(c, abs, c.size ?? { width: 0, height: 0 }, measure);
355
+ });
356
+ const hugsWidth = group.layoutChild?.width === "hug";
357
+ const hugsHeight = group.layoutChild?.height === "hug";
358
+ const naturalWidth = colSizes.reduce((s, x) => s + x, 0) +
359
+ colGap * Math.max(0, nCols - 1) +
360
+ p.left +
361
+ p.right;
362
+ const naturalHeight = rowSizes.reduce((s, x) => s + x, 0) +
363
+ rowGap * Math.max(0, nRows - 1) +
364
+ p.top +
365
+ p.bottom;
366
+ const outSize = {
367
+ width: hugsWidth ? naturalWidth : size.width,
368
+ height: hugsHeight ? naturalHeight : size.height,
369
+ };
370
+ return {
371
+ ...stripChild(group),
372
+ pos,
373
+ size: outSize,
374
+ layout: undefined,
375
+ children: [...placed, ...placedAbsolute],
376
+ };
377
+ }
378
+ function placeGrid(children, nCols) {
379
+ const occ = new Set();
380
+ const key = (r, c) => `${r},${c}`;
381
+ const fits = (r, c, rowSpan, colSpan) => {
382
+ if (c < 0 || c + colSpan > nCols)
383
+ return false;
384
+ for (let dr = 0; dr < rowSpan; dr++)
385
+ for (let dc = 0; dc < colSpan; dc++)
386
+ if (occ.has(key(r + dr, c + dc)))
387
+ return false;
388
+ return true;
389
+ };
390
+ const mark = (r, c, rowSpan, colSpan) => {
391
+ for (let dr = 0; dr < rowSpan; dr++)
392
+ for (let dc = 0; dc < colSpan; dc++)
393
+ occ.add(key(r + dr, c + dc));
394
+ };
395
+ const out = [];
396
+ let curR = 0;
397
+ let curC = 0;
398
+ for (const child of children) {
399
+ const lc = child.layoutChild ?? {};
400
+ const cp = trackSpan(lc.column);
401
+ const rp = trackSpan(lc.row);
402
+ const colSpan = Math.min(cp.span, nCols);
403
+ const rowSpan = rp.span;
404
+ let col = cp.line;
405
+ let row = rp.line;
406
+ if (col !== null && row !== null) {
407
+ // exact placement
408
+ }
409
+ else if (col !== null) {
410
+ let r = 0;
411
+ while (!fits(r, col, rowSpan, colSpan))
412
+ r++;
413
+ row = r;
414
+ }
415
+ else if (row !== null) {
416
+ let c = 0;
417
+ while (!fits(row, c, rowSpan, colSpan))
418
+ c++;
419
+ col = c;
420
+ }
421
+ else {
422
+ let r = curR;
423
+ let c = curC;
424
+ while (!fits(r, c, rowSpan, colSpan)) {
425
+ c++;
426
+ if (c + colSpan > nCols) {
427
+ c = 0;
428
+ r++;
429
+ }
430
+ }
431
+ row = r;
432
+ col = c;
433
+ curR = r;
434
+ curC = c + colSpan;
435
+ if (curC >= nCols) {
436
+ curC = 0;
437
+ curR++;
438
+ }
439
+ }
440
+ mark(row, col, rowSpan, colSpan);
441
+ out.push({ col, colSpan, row, rowSpan });
442
+ }
443
+ return out;
444
+ }
445
+ function resolveTracks(defs, available, gap, autoSizes) {
446
+ const n = defs.length;
447
+ const sizes = new Array(n).fill(0);
448
+ let used = 0;
449
+ let frTotal = 0;
450
+ for (let i = 0; i < n; i++) {
451
+ const d = defs[i];
452
+ if (typeof d === "number") {
453
+ sizes[i] = d;
454
+ used += d;
455
+ }
456
+ else if (d === "auto") {
457
+ sizes[i] = autoSizes[i] ?? 0;
458
+ used += sizes[i];
459
+ }
460
+ else {
461
+ frTotal += parseFloat(d); // "Nfr"
462
+ }
463
+ }
464
+ const leftover = Math.max(0, available - gap * Math.max(0, n - 1) - used);
465
+ if (frTotal > 0) {
466
+ for (let i = 0; i < n; i++) {
467
+ const d = defs[i];
468
+ if (typeof d === "string" && d.endsWith("fr")) {
469
+ sizes[i] = (parseFloat(d) / frTotal) * leftover;
470
+ }
471
+ }
472
+ }
473
+ return sizes;
474
+ }
475
+ function trackOffsets(sizes, gap, start) {
476
+ const offs = [];
477
+ let cur = start;
478
+ for (const s of sizes) {
479
+ offs.push(cur);
480
+ cur += s + gap;
481
+ }
482
+ return offs;
483
+ }
484
+ function spanExtent(sizes, start, span, gap) {
485
+ let total = gap * Math.max(0, span - 1);
486
+ for (let i = 0; i < span; i++)
487
+ total += sizes[start + i] ?? 0;
488
+ return total;
489
+ }
490
+ function gridItemSize(mode, cell, intrinsic) {
491
+ if (mode === "hug")
492
+ return intrinsic();
493
+ if (typeof mode === "number")
494
+ return mode;
495
+ return cell; // "fill" or unset → stretch to the cell
496
+ }
497
+ // Auto tracks size to the space an item COVERS, so a turned item contributes
498
+ // its footprint — the same rule the flex flow uses.
499
+ function gridIntrinsicWidth(child, measure) {
500
+ if (isRotated(child))
501
+ return rotatedFootprint(ownSizeOf(child, measure), child.rotation).width;
502
+ if (child.kind === "text")
503
+ return measure(textOf(child), child.font, null).width;
504
+ return child.size?.width ?? 0;
505
+ }
506
+ function gridIntrinsicHeight(child, width, measure) {
507
+ if (isRotated(child))
508
+ return rotatedFootprint(ownSizeOf(child, measure), child.rotation).height;
509
+ if (child.kind === "text")
510
+ return measure(textOf(child), child.font, width).height;
511
+ return child.size?.height ?? 0;
512
+ }
513
+ function measureChild(child, isRow, containerCross, measure) {
514
+ if (isRotated(child)) {
515
+ // A turned child keeps its own box and only claims a different amount of
516
+ // the flow. It never grows: growing is a main-axis notion of the
517
+ // container's, and the container's main axis is not one of this child's.
518
+ const own = ownSizeOf(child, measure);
519
+ const foot = rotatedFootprint(own, child.rotation);
520
+ return {
521
+ child,
522
+ main: axisMain(foot, isRow),
523
+ cross: axisCross(foot, isRow),
524
+ grow: 0,
525
+ maxMain: Number.POSITIVE_INFINITY,
526
+ own,
527
+ };
528
+ }
529
+ const lc = child.layoutChild ?? {};
530
+ // Cross axis first — its size is what text (main axis) wraps against.
531
+ const crossMode = isRow ? lc.height : lc.width;
532
+ let cross;
533
+ if (crossMode === "fill" || lc.alignSelf === "stretch") {
534
+ cross = containerCross;
535
+ }
536
+ else if (crossMode === "hug") {
537
+ cross = intrinsicCross(child, isRow, measure);
538
+ }
539
+ else if (typeof crossMode === "number") {
540
+ cross = crossMode;
541
+ }
542
+ else {
543
+ cross = axisCross(child.size, isRow);
544
+ }
545
+ // Main axis. `fill`/fixed use the base size (main-axis fill grows via the
546
+ // grow pass); `hug` measures content.
547
+ const mainMode = isRow ? lc.width : lc.height;
548
+ let main;
549
+ if (mainMode === "hug") {
550
+ main = intrinsicMain(child, isRow, cross, measure);
551
+ }
552
+ else if (typeof mainMode === "number") {
553
+ main = mainMode;
554
+ }
555
+ else {
556
+ main = axisMain(child.size, isRow);
557
+ }
558
+ const minMain = isRow ? (lc.min?.width ?? 0) : (lc.min?.height ?? 0);
559
+ const maxMain = isRow
560
+ ? (lc.max?.width ?? Number.POSITIVE_INFINITY)
561
+ : (lc.max?.height ?? Number.POSITIVE_INFINITY);
562
+ main = Math.min(Math.max(main, minMain), maxMain);
563
+ return { child, main, cross, grow: lc.grow ?? 0, maxMain };
564
+ }
565
+ function intrinsicCross(child, isRow, measure) {
566
+ if (child.kind === "text") {
567
+ const m = measure(textOf(child), child.font, null);
568
+ return isRow ? m.height : m.width;
569
+ }
570
+ if (child.kind === "group" && child.layout?.type === "flex") {
571
+ // Resolve at the child's fixed width (in a row) so a fixed-width column
572
+ // measures its height at that width instead of collapsing to 0 — else
573
+ // wrapping text inside it wraps to nothing and reports a runaway cross size.
574
+ // A hug/fill child has no fixed number here, so it still probes at 0 (natural).
575
+ const fixedW = (typeof child.layoutChild?.width === "number"
576
+ ? child.layoutChild.width
577
+ : child.size?.width) ?? 0;
578
+ const probe = isRow
579
+ ? { width: fixedW, height: child.size?.height ?? 0 }
580
+ : { width: child.size?.width ?? 0, height: 0 };
581
+ const resolved = resolveFlex(child, child.layout, { x: 0, y: 0 }, probe, measure);
582
+ return axisCross(resolved.size, isRow);
583
+ }
584
+ return axisCross(child.size, isRow);
585
+ }
586
+ function intrinsicMain(child, isRow, cross, measure) {
587
+ if (child.kind === "text") {
588
+ const maxWidth = isRow ? null : cross;
589
+ const m = measure(textOf(child), child.font, maxWidth);
590
+ return isRow ? m.width : capLines(m.height, child);
591
+ }
592
+ if (child.kind === "group" && child.layout?.type === "flex") {
593
+ const probe = isRow
594
+ ? { width: 0, height: cross }
595
+ : { width: cross, height: 0 };
596
+ const resolved = resolveFlex(child, child.layout, { x: 0, y: 0 }, probe, measure);
597
+ return axisMain(resolved.size, isRow);
598
+ }
599
+ return axisMain(child.size, isRow);
600
+ }
601
+ // Cap a measured text height at its `maxLines` (CSS line-clamp): the text bakes
602
+ // to at most maxLines, so its box must not reserve space for the clipped
603
+ // remainder — otherwise a long, clamped paragraph leaves a gap below it.
604
+ function capLines(height, node) {
605
+ if (!node.maxLines || node.maxLines <= 0)
606
+ return height;
607
+ return Math.min(height, node.maxLines * node.font.size * node.font.lineHeight);
608
+ }
609
+ function textOf(node) {
610
+ if (node.spans && node.spans.length > 0)
611
+ return node.spans.map((s) => s.text).join("");
612
+ return node.text ?? "";
613
+ }
614
+ // ─────────────── small helpers ───────────────
615
+ function axisMain(size, isRow) {
616
+ const s = size ?? { width: 0, height: 0 };
617
+ return isRow ? s.width : s.height;
618
+ }
619
+ function axisCross(size, isRow) {
620
+ const s = size ?? { width: 0, height: 0 };
621
+ return isRow ? s.height : s.width;
622
+ }
623
+ function stripChild(node) {
624
+ if (node.layoutChild === undefined)
625
+ return node;
626
+ const { layoutChild: _omit, ...rest } = node;
627
+ return rest;
628
+ }
629
+ function distributeMain(justify, free, gap, n) {
630
+ if (free <= 0 || n === 0)
631
+ return { leading: 0, between: gap };
632
+ switch (justify) {
633
+ case "center":
634
+ return { leading: free / 2, between: gap };
635
+ case "end":
636
+ return { leading: free, between: gap };
637
+ case "space-between":
638
+ return { leading: 0, between: gap + (n > 1 ? free / (n - 1) : 0) };
639
+ case "space-around": {
640
+ const unit = free / n;
641
+ return { leading: unit / 2, between: gap + unit };
642
+ }
643
+ case "space-evenly": {
644
+ const unit = free / (n + 1);
645
+ return { leading: unit, between: gap + unit };
646
+ }
647
+ default:
648
+ return { leading: 0, between: gap };
649
+ }
650
+ }
651
+ function alignCross(align, containerCross, childCross) {
652
+ if (align === "center")
653
+ return (containerCross - childCross) / 2;
654
+ if (align === "end")
655
+ return containerCross - childCross;
656
+ return 0; // start / stretch (child already filled cross in measure)
657
+ }
@@ -0,0 +1,6 @@
1
+ import type { EncodeOptions } from "./png.js";
2
+ import type { Painter, PaintOutput, PaintResult, PaintRuntime } from "./types.js";
3
+ export type DisposePolicy = "keep" | "encode";
4
+ export declare function applyDisposePolicy(output: PaintOutput, policy: DisposePolicy, encode?: EncodeOptions): Promise<PaintResult>;
5
+ export declare const paintCanvasKit: (ck: unknown) => Painter;
6
+ export declare function makeRuntime(base: Omit<PaintRuntime, "paint">, policy: DisposePolicy, encode?: EncodeOptions, painter?: Painter): PaintRuntime;