@jokerized/decksmith 0.3.2 → 0.4.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 +96 -14
- package/dist/cli.js +498 -163
- package/dist/deck-runtime.js +2 -2
- package/dist/index.js +407 -80
- package/dist/mcp.js +496 -186
- package/dist/types/deck/runtime.d.ts +127 -0
- package/dist/types/emit/camera.d.ts +14 -6
- package/dist/types/emit/composition.d.ts +13 -0
- package/dist/types/emit/kit.d.ts +33 -8
- package/dist/types/emit/svg.d.ts +27 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/render/capture.d.ts +27 -1
- package/dist/types/tmpdir.d.ts +33 -0
- package/dist/types/types.d.ts +28 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { cp, mkdir as mkdir10, mkdtemp as mkdtemp4, readdir as readdir4, readFile as readFile17, rm as rm9, stat as stat2, writeFile as writeFile12 } from "node:fs/promises";
|
|
5
|
-
import { createRequire as
|
|
6
|
-
import { tmpdir as
|
|
7
|
-
import { dirname as
|
|
5
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
6
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
7
|
+
import { dirname as dirname5, join as join17, relative as relative2, resolve as resolve6 } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
@@ -418,15 +418,53 @@ var dataTableParamsSchema = z.object({
|
|
|
418
418
|
path: ["highlight"],
|
|
419
419
|
message: "every highlight must name a row that params.rows draws"
|
|
420
420
|
});
|
|
421
|
+
var chartPointSchema = z.object({ x: z.string(), y: z.number() });
|
|
421
422
|
var lineChartParamsSchema = z.object({
|
|
422
423
|
eyebrow: z.string().optional(),
|
|
423
424
|
headline: z.string(),
|
|
424
425
|
xLabel: z.string(),
|
|
425
426
|
yLabel: z.string(),
|
|
426
|
-
points: z.array(
|
|
427
|
+
points: z.array(chartPointSchema).min(2),
|
|
427
428
|
/** Inter-point annotations, e.g. per-step deltas. One fewer than `points`. */
|
|
428
429
|
deltas: z.array(z.string()).optional(),
|
|
429
|
-
readout: z.string().optional()
|
|
430
|
+
readout: z.string().optional(),
|
|
431
|
+
/**
|
|
432
|
+
* THE SAME MEASUREMENT UNDER A SECOND CONDITION — a baseline the main series
|
|
433
|
+
* is to be read against, where the point is the change in the SHAPE of the
|
|
434
|
+
* curve rather than two numbers.
|
|
435
|
+
*
|
|
436
|
+
* Drawn first and alone; then the curve lifts off it and reshapes into
|
|
437
|
+
* `points`, leaving this one behind as a ghost. `label` names the ghost and
|
|
438
|
+
* is drawn wherever the chart has room for it.
|
|
439
|
+
*
|
|
440
|
+
* KEEP IT SHORT — two or three words. A label wider than the plot is refused
|
|
441
|
+
* outright, and that refusal reaches `onBeatError` and costs the whole beat.
|
|
442
|
+
* One that fits but cannot be placed clear of the axis names, the tick and
|
|
443
|
+
* category labels, the values, the deltas and both curves is DROPPED
|
|
444
|
+
* instead, silently: an unnamed ghost is still legibly the fainter, earlier
|
|
445
|
+
* curve, where a name printed through a number is a defect in both of them.
|
|
446
|
+
* Nothing warns about that one — see the placement note in
|
|
447
|
+
* `src/emit/archetypes/line-chart.ts`.
|
|
448
|
+
*/
|
|
449
|
+
compare: z.object({ label: z.string(), points: z.array(chartPointSchema).min(2) }).optional()
|
|
450
|
+
}).superRefine((p, ctx) => {
|
|
451
|
+
if (!p.compare) return;
|
|
452
|
+
if (p.compare.points.length !== p.points.length) {
|
|
453
|
+
ctx.addIssue({
|
|
454
|
+
code: z.ZodIssueCode.custom,
|
|
455
|
+
path: ["compare", "points"],
|
|
456
|
+
message: `compare has ${p.compare.points.length} points against ${p.points.length}; a comparison is over the same x values`
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const off = p.compare.points.findIndex((c, i) => c.x !== p.points[i]?.x);
|
|
461
|
+
if (off >= 0) {
|
|
462
|
+
ctx.addIssue({
|
|
463
|
+
code: z.ZodIssueCode.custom,
|
|
464
|
+
path: ["compare", "points", off, "x"],
|
|
465
|
+
message: `compare point ${off} is "${p.compare.points[off]?.x}" where points has "${p.points[off]?.x}"`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
430
468
|
});
|
|
431
469
|
var calloutParamsSchema = z.object({
|
|
432
470
|
eyebrow: z.string().optional(),
|
|
@@ -1206,8 +1244,8 @@ async function bundleFont(lang, glyphs2, dir) {
|
|
|
1206
1244
|
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1207
1245
|
await mkdir(dir, { recursive: true });
|
|
1208
1246
|
const cssPath = join(dir, "fonts.css");
|
|
1209
|
-
const
|
|
1210
|
-
if (
|
|
1247
|
+
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1248
|
+
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1211
1249
|
const res = await fetch(
|
|
1212
1250
|
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
1213
1251
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1420,6 +1458,21 @@ function nv(v) {
|
|
|
1420
1458
|
}
|
|
1421
1459
|
var DRAW_FROM = { drawSVG: "0%" };
|
|
1422
1460
|
var DRAW_TO = { drawSVG: "100%" };
|
|
1461
|
+
var SHAPE_INDEX = 0;
|
|
1462
|
+
function reshape(target, to, at, seconds, first) {
|
|
1463
|
+
if (!(seconds > 0)) throw new Error(`reshape ${target}: ${seconds}s is no time to reshape in`);
|
|
1464
|
+
return fromTo(
|
|
1465
|
+
target,
|
|
1466
|
+
{ morphSVG: { shape: target, shapeIndex: SHAPE_INDEX } },
|
|
1467
|
+
{
|
|
1468
|
+
morphSVG: { shape: to, shapeIndex: SHAPE_INDEX },
|
|
1469
|
+
duration: sec(seconds),
|
|
1470
|
+
ease: "power2.inOut",
|
|
1471
|
+
...first ? {} : { immediateRender: false }
|
|
1472
|
+
},
|
|
1473
|
+
sec(at)
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1423
1476
|
function travel(target, route, at, seconds) {
|
|
1424
1477
|
if (!(seconds > 0)) throw new Error(`travel ${target}: ${seconds}s is no time to travel in`);
|
|
1425
1478
|
const legs = [];
|
|
@@ -4101,6 +4154,12 @@ var CHART_GAP = 60;
|
|
|
4101
4154
|
var READOUT_W = 460;
|
|
4102
4155
|
var READOUT_LH = 1.35;
|
|
4103
4156
|
var TALL_ASPECT = 1.15;
|
|
4157
|
+
var RESHAPE_SECONDS = 1.2;
|
|
4158
|
+
var GHOST_OPACITY = 0.28;
|
|
4159
|
+
var DRAW_SECONDS = 1.8;
|
|
4160
|
+
var DRAW_FLOOR = 1;
|
|
4161
|
+
var SEPARATE = 0.5;
|
|
4162
|
+
var STEP_FLOOR = 0.15;
|
|
4104
4163
|
function chartScale(values) {
|
|
4105
4164
|
const lo = Math.min(...values);
|
|
4106
4165
|
const hi = Math.max(...values);
|
|
@@ -4131,7 +4190,8 @@ var lineChart = (beat, ctx) => {
|
|
|
4131
4190
|
const { sid, theme } = ctx;
|
|
4132
4191
|
const p = beat.params;
|
|
4133
4192
|
const face = faceOf(theme.fontStack);
|
|
4134
|
-
const
|
|
4193
|
+
const cmp = p.compare;
|
|
4194
|
+
const scale = chartScale([...p.points, ...cmp?.points ?? []].map((pt) => pt.y));
|
|
4135
4195
|
const box = contentW(ctx.format);
|
|
4136
4196
|
const tall = isPortrait(ctx.format);
|
|
4137
4197
|
const width = tall ? box : box - (p.readout ? CHART_GAP + READOUT_W : 0);
|
|
@@ -4174,11 +4234,15 @@ var lineChart = (beat, ctx) => {
|
|
|
4174
4234
|
const LABEL_SIZE3 = 40;
|
|
4175
4235
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4176
4236
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4237
|
+
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
4238
|
+
const ghostW = (s) => textWidth(s, LABEL_SIZE3, 600, 0, false, face);
|
|
4177
4239
|
const shownX = fitIndices((i) => catW(p.points[i]?.x ?? ""), false);
|
|
4178
4240
|
const xLabels = p.points.map(
|
|
4179
4241
|
(pt, i) => shownX.has(i) ? `<text x="${n2(x(i))}" y="${H - PAD2.b + 56}">${esc(pt.x)}</text>` : ""
|
|
4180
4242
|
).join("");
|
|
4181
|
-
const
|
|
4243
|
+
const pathOf = (pts) => pts.map((pt, i) => `${i === 0 ? "M" : "L"}${n2(x(i))},${n2(y(pt.y))}`).join(" ");
|
|
4244
|
+
const path2 = pathOf(p.points);
|
|
4245
|
+
const basePath = cmp ? pathOf(cmp.points) : "";
|
|
4182
4246
|
const dots = p.points.map(
|
|
4183
4247
|
(pt, i) => `<circle class="dot" cx="${n2(x(i))}" cy="${n2(y(pt.y))}" r="${i === p.points.length - 1 ? 11 : 9}" fill="${i === p.points.length - 1 ? theme.tones.b : theme.accent}" />`
|
|
4184
4248
|
).join("");
|
|
@@ -4214,6 +4278,75 @@ var lineChart = (beat, ctx) => {
|
|
|
4214
4278
|
}).join("");
|
|
4215
4279
|
const first = { x: x(0), y: y(p.points[0]?.y ?? 0) };
|
|
4216
4280
|
const ring = p.points.length > 1 ? `<circle id="${sid}-ring" cx="${n2(first.x)}" cy="${n2(first.y)}" r="20" fill="none" stroke="${theme.tones.b}" stroke-width="5" opacity="0" />` : "";
|
|
4281
|
+
const lineTag = (part, d, extra = "") => `<path class="chartline" id="${sid}-${part}" d="${d}" fill="none" stroke="${theme.accent}"${extra} />`;
|
|
4282
|
+
const chartlines = cmp ? [
|
|
4283
|
+
lineTag("base", basePath),
|
|
4284
|
+
lineTag("line", basePath, ' opacity="0"'),
|
|
4285
|
+
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
4286
|
+
].join("\n ") : lineTag("line", path2);
|
|
4287
|
+
const curveBand = (pts, x0, x1) => {
|
|
4288
|
+
const ys = [];
|
|
4289
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
4290
|
+
for (let i = 0; i < pts.length; i++) {
|
|
4291
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
4292
|
+
}
|
|
4293
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
4294
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
4295
|
+
for (const edge of [x0, x1]) {
|
|
4296
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
4297
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
4302
|
+
};
|
|
4303
|
+
const fixedBoxes = [
|
|
4304
|
+
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
4305
|
+
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
4306
|
+
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
4307
|
+
{ x: 0, y: 42 - LABEL_SIZE3, w: nameW(p.yLabel), h: LABEL_SIZE3 },
|
|
4308
|
+
{
|
|
4309
|
+
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
4310
|
+
y: H - 16 - LABEL_SIZE3,
|
|
4311
|
+
w: nameW(p.xLabel),
|
|
4312
|
+
h: LABEL_SIZE3
|
|
4313
|
+
},
|
|
4314
|
+
...ticks.map((v) => {
|
|
4315
|
+
const w = runW(v.toFixed(scale.decimals));
|
|
4316
|
+
return { x: PAD2.l - 22 - w, y: y(v) + 13 - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4317
|
+
}),
|
|
4318
|
+
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
4319
|
+
];
|
|
4320
|
+
const lastI = p.points.length - 1;
|
|
4321
|
+
const ghost = (() => {
|
|
4322
|
+
if (!cmp) return void 0;
|
|
4323
|
+
const w = ghostW(cmp.label);
|
|
4324
|
+
if (w > plotW) {
|
|
4325
|
+
throw new Error(
|
|
4326
|
+
`line-chart ${beat.id}: the compare label "${cmp.label}" is ${Math.ceil(w)}px against the ${Math.floor(plotW)}px of plot it is set in. Shorten it.`
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
const atEnd = (i, start) => {
|
|
4330
|
+
const cy = y(cmp.points[i]?.y ?? 0);
|
|
4331
|
+
const away = Math.max(44, cy - 26);
|
|
4332
|
+
const toward = Math.min(cy + 52, H - PAD2.b + 4);
|
|
4333
|
+
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
4334
|
+
return sides.map((by) => ({ i, start, by }));
|
|
4335
|
+
};
|
|
4336
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
4337
|
+
return [...atEnd(lastI, false), ...atEnd(0, true)].find((c) => {
|
|
4338
|
+
const box2 = { x: c.start ? x(c.i) : x(c.i) - w, y: c.by - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4339
|
+
if (fixedBoxes.some((f) => overlaps(box2, f))) return false;
|
|
4340
|
+
if (valueBoxes.some((v) => overlaps(box2, v))) return false;
|
|
4341
|
+
if (drawnDeltas.some((d) => d !== null && overlaps(box2, d))) return false;
|
|
4342
|
+
return ![p.points, cmp.points].some((series) => {
|
|
4343
|
+
const [top, bottom] = curveBand(series, box2.x, box2.x + box2.w);
|
|
4344
|
+
return Math.min(box2.y + box2.h, bottom) - Math.max(box2.y, top) > 8;
|
|
4345
|
+
});
|
|
4346
|
+
});
|
|
4347
|
+
})();
|
|
4348
|
+
const ghostLabel = cmp && ghost ? `
|
|
4349
|
+
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.start ? "start" : "end"}" x="${n2(x(ghost.i))}" y="${n2(ghost.by)}">${esc(cmp.label)}</text>` : "";
|
|
4217
4350
|
const readout = p.readout ? `
|
|
4218
4351
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
4219
4352
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -4226,7 +4359,7 @@ var lineChart = (beat, ctx) => {
|
|
|
4226
4359
|
the svg and the layout gate reports container_overflow. -->
|
|
4227
4360
|
<text class="axname" x="0" y="42">${esc(p.yLabel)}</text>
|
|
4228
4361
|
<text class="axname" x="${n2(PAD2.l + plotW / 2)}" y="${H - 16}" text-anchor="middle">${esc(p.xLabel)}</text>
|
|
4229
|
-
|
|
4362
|
+
${chartlines}${ghostLabel}
|
|
4230
4363
|
<g>${dots}</g>
|
|
4231
4364
|
${ring}
|
|
4232
4365
|
<g class="ptlab" text-anchor="middle">${values}</g>
|
|
@@ -4234,10 +4367,52 @@ var lineChart = (beat, ctx) => {
|
|
|
4234
4367
|
</svg>${readout}
|
|
4235
4368
|
</div>`;
|
|
4236
4369
|
const draw2 = 0.8;
|
|
4237
|
-
const
|
|
4370
|
+
const count = p.points.length;
|
|
4371
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count);
|
|
4372
|
+
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
4373
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count - 1) : 0;
|
|
4374
|
+
const tailAfter = (s) => Math.max(
|
|
4375
|
+
s * count + 0.4,
|
|
4376
|
+
// the ring: its walk, then the 0.4s it takes to leave
|
|
4377
|
+
0.3 + s * (count - 1),
|
|
4378
|
+
// the dots
|
|
4379
|
+
0.5 + s * (count - 1),
|
|
4380
|
+
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
4381
|
+
);
|
|
4382
|
+
const room = beat.seconds - spine;
|
|
4383
|
+
let drawCents = Math.round(DRAW_SECONDS * 100);
|
|
4384
|
+
let stepCents = Math.floor(idealStep * 100);
|
|
4385
|
+
if (cmp) {
|
|
4386
|
+
while (drawCents / 100 + tailAfter(stepCents / 100) > room) {
|
|
4387
|
+
if (drawCents > Math.round(DRAW_FLOOR * 100)) drawCents--;
|
|
4388
|
+
else if (stepCents > Math.round(STEP_FLOOR * 100)) stepCents--;
|
|
4389
|
+
else break;
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
const drawFor = cmp ? drawCents / 100 : DRAW_SECONDS;
|
|
4393
|
+
const step2 = cmp ? stepCents / 100 : idealStep;
|
|
4394
|
+
const lift2 = sec(draw2 + drawFor);
|
|
4395
|
+
const settled = cmp ? sec(lift2 + SEPARATE + RESHAPE_SECONDS) : draw2;
|
|
4238
4396
|
const tl = [
|
|
4239
4397
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
4240
|
-
tween(
|
|
4398
|
+
tween(
|
|
4399
|
+
`#${sid}-${cmp ? "base" : "line"}`,
|
|
4400
|
+
DRAW_FROM,
|
|
4401
|
+
{ ...DRAW_TO, duration: drawFor, ease: "none" },
|
|
4402
|
+
draw2
|
|
4403
|
+
)
|
|
4404
|
+
];
|
|
4405
|
+
if (cmp) {
|
|
4406
|
+
tl.push(
|
|
4407
|
+
...ghost ? [tween(`#${sid}-ghostlab`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, draw2 + 0.4)] : [],
|
|
4408
|
+
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
4409
|
+
// separating from itself rather than as a second line arriving.
|
|
4410
|
+
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
4411
|
+
tween(`#${sid}-base`, { opacity: 1 }, { opacity: GHOST_OPACITY, duration: SEPARATE }, lift2),
|
|
4412
|
+
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
tl.push(
|
|
4241
4416
|
tween(
|
|
4242
4417
|
`#${sid} .dot`,
|
|
4243
4418
|
// Origin in both halves, or GSAP's smoothOrigin compensates the change with
|
|
@@ -4245,37 +4420,43 @@ var lineChart = (beat, ctx) => {
|
|
|
4245
4420
|
// polyline they mark, inside the frame and so invisible to every gate.
|
|
4246
4421
|
{ opacity: 0, scale: 0, transformOrigin: "center" },
|
|
4247
4422
|
{ opacity: 1, scale: 1, transformOrigin: "center", duration: 0.3, stagger: step2 },
|
|
4248
|
-
|
|
4423
|
+
settled
|
|
4249
4424
|
),
|
|
4250
|
-
tween(
|
|
4251
|
-
|
|
4425
|
+
tween(
|
|
4426
|
+
`#${sid} .pv`,
|
|
4427
|
+
{ opacity: 0 },
|
|
4428
|
+
{ opacity: 1, duration: 0.3, stagger: step2 },
|
|
4429
|
+
settled + 0.2
|
|
4430
|
+
)
|
|
4431
|
+
);
|
|
4252
4432
|
if (deltas) {
|
|
4253
4433
|
tl.push(
|
|
4254
4434
|
tween(
|
|
4255
4435
|
`#${sid} .dv`,
|
|
4256
4436
|
{ opacity: 0, y: -10 },
|
|
4257
4437
|
{ opacity: 1, y: 0, duration: 0.35, stagger: step2 },
|
|
4258
|
-
|
|
4438
|
+
settled + 0.6
|
|
4259
4439
|
)
|
|
4260
4440
|
);
|
|
4261
4441
|
}
|
|
4262
|
-
|
|
4442
|
+
const walk = cmp ? sec(step2 * count) : DRAW_SECONDS;
|
|
4443
|
+
if (count > 1) {
|
|
4263
4444
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
4264
4445
|
tl.push(
|
|
4265
|
-
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 },
|
|
4266
|
-
...travel(`#${sid}-ring`, route,
|
|
4446
|
+
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
4447
|
+
...travel(`#${sid}-ring`, route, settled, walk),
|
|
4267
4448
|
// And it leaves once the curve is whole: a marker parked on the last
|
|
4268
4449
|
// point for the rest of the beat reads as a defect, not as emphasis.
|
|
4269
4450
|
tween(
|
|
4270
4451
|
`#${sid}-ring`,
|
|
4271
4452
|
{ opacity: 1 },
|
|
4272
4453
|
{ opacity: 0, duration: 0.4, immediateRender: false },
|
|
4273
|
-
|
|
4454
|
+
settled + walk
|
|
4274
4455
|
)
|
|
4275
4456
|
);
|
|
4276
4457
|
}
|
|
4277
|
-
const drawn =
|
|
4278
|
-
const holds = [drawn];
|
|
4458
|
+
const drawn = settled + (cmp ? tailAfter(step2) : step2 * count + 0.4);
|
|
4459
|
+
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
4279
4460
|
if (p.readout) {
|
|
4280
4461
|
tl.push(
|
|
4281
4462
|
// It enters from wherever it sits: from the right when it is beside the
|
|
@@ -4284,9 +4465,22 @@ var lineChart = (beat, ctx) => {
|
|
|
4284
4465
|
);
|
|
4285
4466
|
holds.push(drawn + 0.8);
|
|
4286
4467
|
}
|
|
4468
|
+
const end = holds[holds.length - 1] ?? 0;
|
|
4469
|
+
const need = sec(end + 0.15);
|
|
4470
|
+
if (cmp && need > beat.seconds + 1e-9) {
|
|
4471
|
+
return {
|
|
4472
|
+
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
4473
|
+
warnings: [
|
|
4474
|
+
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${count} points needs ${need}s and the beat is ${beat.seconds}s, so the chart was drawn without it. Lengthen the beat to keep the comparison.`
|
|
4475
|
+
]
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4287
4478
|
return {
|
|
4288
4479
|
html,
|
|
4289
4480
|
tl,
|
|
4481
|
+
// Named only when a beat actually reshapes, which is what keeps MorphSVG's
|
|
4482
|
+
// 21,195 bytes off every deck that does not. See `PLUGINS` in composition.ts.
|
|
4483
|
+
...cmp ? { plugins: ["morphSVG"] } : {},
|
|
4290
4484
|
holds: holdsWithin(holds, beat.seconds),
|
|
4291
4485
|
css: [
|
|
4292
4486
|
chromeCss(theme),
|
|
@@ -4311,6 +4505,14 @@ var lineChart = (beat, ctx) => {
|
|
|
4311
4505
|
`.axname{font-size:40px;fill:${theme.muted};font-weight:500}`,
|
|
4312
4506
|
`.ptlab{font-size:40px;fill:${theme.fg};font-weight:600}`,
|
|
4313
4507
|
`.delta{font-size:40px;fill:${theme.tones.b};font-weight:600}`,
|
|
4508
|
+
// Only when a ghost was actually named — not merely when there is a ghost,
|
|
4509
|
+
// since the label is dropped where neither side of the baseline is clear.
|
|
4510
|
+
// An unconditional rule would move the stylesheet bytes of every line chart
|
|
4511
|
+
// ever built for a part they do not draw, which is the whole thing
|
|
4512
|
+
// `Scene.plugins` is careful about one level up. `muted`, not `dim`: it
|
|
4513
|
+
// names a series, so it is read, and the curve it names is the thing that
|
|
4514
|
+
// has been faded, not its label.
|
|
4515
|
+
...ghost ? [`.ghostlab{font-size:40px;fill:${theme.muted};font-weight:600}`] : [],
|
|
4314
4516
|
// 1.7 set the two lines of a wrapped readout 68px apart, which reads as two
|
|
4315
4517
|
// unrelated fragments rather than one sentence. 1.35 keeps it a paragraph.
|
|
4316
4518
|
`.readout{font-size:${BODY_SIZE}px;line-height:${READOUT_LH};color:${theme.muted};max-width:${READOUT_W}px}`,
|
|
@@ -5556,7 +5758,10 @@ function round4(n3) {
|
|
|
5556
5758
|
// src/emit/composition.ts
|
|
5557
5759
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5558
5760
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5559
|
-
var
|
|
5761
|
+
var PLUGINS = {
|
|
5762
|
+
dsMorph: { src: "./vendor/ds-morph.js", global: "DSMorphPlugin" },
|
|
5763
|
+
morphSVG: { src: "./vendor/MorphSVGPlugin.min.js", global: "MorphSVGPlugin" }
|
|
5764
|
+
};
|
|
5560
5765
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5561
5766
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5562
5767
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5664,7 +5869,15 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5664
5869
|
}
|
|
5665
5870
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5666
5871
|
if (scene.measure?.length) builds = true;
|
|
5667
|
-
for (const
|
|
5872
|
+
for (const w of cut2.scene.warnings ?? []) opts.onBeatWarning?.(beat.id, w);
|
|
5873
|
+
for (const p of scene.plugins ?? []) {
|
|
5874
|
+
if (!Object.hasOwn(PLUGINS, p)) {
|
|
5875
|
+
throw new Error(
|
|
5876
|
+
`${beat.archetype} ${beat.id}: no vendored plugin named "${p}" \u2014 Scene.plugins takes ${Object.keys(PLUGINS).join(" or ")}`
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
plugins.add(p);
|
|
5880
|
+
}
|
|
5668
5881
|
scenes.push(
|
|
5669
5882
|
sceneHtml(
|
|
5670
5883
|
sid,
|
|
@@ -5781,9 +5994,11 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5781
5994
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5782
5995
|
const island = format.navigable ? `
|
|
5783
5996
|
${emitIsland(slides)}` : "";
|
|
5784
|
-
const
|
|
5785
|
-
|
|
5786
|
-
<script
|
|
5997
|
+
const plugins = Object.entries(PLUGINS).filter(([name]) => laid.plugins.has(name)).map(
|
|
5998
|
+
([, p]) => `
|
|
5999
|
+
<script src="${p.src}"></script>
|
|
6000
|
+
<script>gsap.registerPlugin(${p.global});</script>`
|
|
6001
|
+
).join("");
|
|
5787
6002
|
return `<!doctype html>
|
|
5788
6003
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5789
6004
|
<head>
|
|
@@ -5792,7 +6007,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5792
6007
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5793
6008
|
<script src="${GSAP_SRC}"></script>
|
|
5794
6009
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5795
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${
|
|
6010
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${plugins}
|
|
5796
6011
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5797
6012
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5798
6013
|
<style>
|
|
@@ -6187,8 +6402,8 @@ async function send(url2, target, headers, signal, opts) {
|
|
|
6187
6402
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6188
6403
|
};
|
|
6189
6404
|
try {
|
|
6190
|
-
return await new Promise((
|
|
6191
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
6405
|
+
return await new Promise((resolve7, reject) => {
|
|
6406
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve7);
|
|
6192
6407
|
req.on("error", reject);
|
|
6193
6408
|
req.end();
|
|
6194
6409
|
});
|
|
@@ -6274,7 +6489,7 @@ function message(err) {
|
|
|
6274
6489
|
async function fetchFigures(source, dir, warnings) {
|
|
6275
6490
|
const drops = warnings ?? [];
|
|
6276
6491
|
await mkdir2(dir, { recursive: true });
|
|
6277
|
-
const
|
|
6492
|
+
const cached2 = await readdir(dir).catch(() => []);
|
|
6278
6493
|
const figures = [];
|
|
6279
6494
|
for (const figure of source.figures) {
|
|
6280
6495
|
if (figure.kind === "clip") {
|
|
@@ -6282,7 +6497,7 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6282
6497
|
continue;
|
|
6283
6498
|
}
|
|
6284
6499
|
try {
|
|
6285
|
-
figures.push(figureSchema.parse(await localize(figure, dir,
|
|
6500
|
+
figures.push(figureSchema.parse(await localize(figure, dir, cached2)));
|
|
6286
6501
|
} catch (err) {
|
|
6287
6502
|
drops.push(`figure ${figure.id} was left out: ${reason(err)}`);
|
|
6288
6503
|
}
|
|
@@ -6296,15 +6511,15 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6296
6511
|
}
|
|
6297
6512
|
return parsed.data;
|
|
6298
6513
|
}
|
|
6299
|
-
async function localize(figure, dir,
|
|
6514
|
+
async function localize(figure, dir, cached2) {
|
|
6300
6515
|
const stem = assetStem(figure.id, figure.src);
|
|
6301
|
-
const hit =
|
|
6516
|
+
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
6302
6517
|
const bytes = hit ? await readFile3(join2(dir, hit)) : await load(figure.src);
|
|
6303
6518
|
const size3 = imageSize(bytes);
|
|
6304
6519
|
if (hit) return { ...figure, src: hit, ...size3 };
|
|
6305
6520
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6306
6521
|
await writeFile2(join2(dir, src), bytes);
|
|
6307
|
-
|
|
6522
|
+
cached2.push(src);
|
|
6308
6523
|
return { ...figure, src, ...size3 };
|
|
6309
6524
|
}
|
|
6310
6525
|
function assetStem(id2, src) {
|
|
@@ -6853,7 +7068,10 @@ var REVEALS = {
|
|
|
6853
7068
|
"equation-walk": "one per term",
|
|
6854
7069
|
"equation-morph": "2",
|
|
6855
7070
|
"data-table": "one per highlighted row, plus 1",
|
|
6856
|
-
"
|
|
7071
|
+
// "long enough" is not a hedge: below the emitter's own floor the comparison
|
|
7072
|
+
// is dropped and the chart stops twice becoming a chart that stops once. See
|
|
7073
|
+
// the line-chart entry under THE THIRTEEN ARCHETYPES for what that costs.
|
|
7074
|
+
"line-chart": "1, or 2 when compare is given and the beat is long enough for it",
|
|
6857
7075
|
callout: "one per panel",
|
|
6858
7076
|
pipeline: "one per stage",
|
|
6859
7077
|
"annotated-figure": "one per note, plus 1",
|
|
@@ -6976,6 +7194,29 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
6976
7194
|
scale, over training. Points come from the source's numbers;
|
|
6977
7195
|
deltas, if given, are the steps between consecutive points, so
|
|
6978
7196
|
there is always one fewer.
|
|
7197
|
+
\`compare\` adds a SECOND CONDITION over the same axis \u2014 the tell
|
|
7198
|
+
there is THE SAME QUANTITY MEASURED TWO WAYS, a baseline and the
|
|
7199
|
+
result, where what the source is claiming is the change in the
|
|
7200
|
+
SHAPE of the curve and not two numbers. Its \`label\` names the
|
|
7201
|
+
baseline ("without pretraining", "FP32") and is drawn on the
|
|
7202
|
+
slide where the chart has room for it. Keep it SHORT \u2014 two or
|
|
7203
|
+
three words. A label wider than the plot is refused and the
|
|
7204
|
+
whole slide goes with it, and one that cannot be placed clear of
|
|
7205
|
+
the axis names, the values and both curves is dropped without a
|
|
7206
|
+
word, leaving the baseline unnamed. A \`readout\` takes the plot
|
|
7207
|
+
down to about two thirds of its width, so a beat that has one
|
|
7208
|
+
has room for a shorter label still.
|
|
7209
|
+
\`compare.points\` must carry the SAME x values, in the
|
|
7210
|
+
same order, as \`points\`; the baseline is drawn first, then the
|
|
7211
|
+
curve reshapes into \`points\` and leaves the baseline behind. Do
|
|
7212
|
+
not use it for two unrelated series that happen to share an
|
|
7213
|
+
axis \u2014 nothing reshapes into something it is not a version of.
|
|
7214
|
+
A compare beat also costs TIME \u2014 the baseline draws, is held,
|
|
7215
|
+
then reshapes \u2014 so give it \`seconds\` of 7, or 8 with a
|
|
7216
|
+
\`readout\`. Give it too few and the emitter draws the chart
|
|
7217
|
+
without the comparison rather than stopping on a half-drawn one:
|
|
7218
|
+
the slide is still there, but the point about the baseline is
|
|
7219
|
+
gone.
|
|
6979
7220
|
|
|
6980
7221
|
DESCRIBING ARCHETYPES \u2014 the fallbacks
|
|
6981
7222
|
|
|
@@ -7760,7 +8001,7 @@ function codexCommand(args) {
|
|
|
7760
8001
|
}
|
|
7761
8002
|
function runCodex(args) {
|
|
7762
8003
|
const { argv, env } = codexCommand(args);
|
|
7763
|
-
return new Promise((
|
|
8004
|
+
return new Promise((resolve7, reject) => {
|
|
7764
8005
|
const child = spawn("codex", argv, {
|
|
7765
8006
|
stdio: ["pipe", "ignore", "pipe"],
|
|
7766
8007
|
...env === void 0 ? {} : { env }
|
|
@@ -7783,7 +8024,7 @@ function runCodex(args) {
|
|
|
7783
8024
|
});
|
|
7784
8025
|
child.on("close", (code) => {
|
|
7785
8026
|
clearTimeout(timer);
|
|
7786
|
-
if (code === 0) return
|
|
8027
|
+
if (code === 0) return resolve7();
|
|
7787
8028
|
reject(
|
|
7788
8029
|
new Error(`codex exec exited ${code}.
|
|
7789
8030
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -8244,7 +8485,7 @@ async function find2(can) {
|
|
|
8244
8485
|
throw new Error(MISSING);
|
|
8245
8486
|
}
|
|
8246
8487
|
function runArgv(cmd, args) {
|
|
8247
|
-
return new Promise((
|
|
8488
|
+
return new Promise((resolve7) => {
|
|
8248
8489
|
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8249
8490
|
let stdout = "";
|
|
8250
8491
|
let stderr = "";
|
|
@@ -8254,8 +8495,8 @@ function runArgv(cmd, args) {
|
|
|
8254
8495
|
child.stderr.on("data", (b) => {
|
|
8255
8496
|
stderr += b.toString();
|
|
8256
8497
|
});
|
|
8257
|
-
child.on("error", (e) =>
|
|
8258
|
-
child.on("close", (code) =>
|
|
8498
|
+
child.on("error", (e) => resolve7({ code: -1, stderr: String(e), stdout }));
|
|
8499
|
+
child.on("close", (code) => resolve7({ code: code ?? -1, stderr, stdout }));
|
|
8259
8500
|
});
|
|
8260
8501
|
}
|
|
8261
8502
|
var edgeTts = {
|
|
@@ -8342,8 +8583,8 @@ async function synthesize(text2, opts) {
|
|
|
8342
8583
|
const file = `${key}.mp3`;
|
|
8343
8584
|
const audio = join6(opts.dir, file);
|
|
8344
8585
|
const sidecar = join6(opts.dir, `${key}.json`);
|
|
8345
|
-
const
|
|
8346
|
-
if (
|
|
8586
|
+
const cached2 = await readSidecar(sidecar);
|
|
8587
|
+
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
8347
8588
|
await mkdir4(opts.dir, { recursive: true });
|
|
8348
8589
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
8349
8590
|
const { cues, seconds } = spoken;
|
|
@@ -8681,18 +8922,26 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
8681
8922
|
import { homedir as homedir2 } from "node:os";
|
|
8682
8923
|
import { join as join8 } from "node:path";
|
|
8683
8924
|
import { pathToFileURL } from "node:url";
|
|
8684
|
-
async function
|
|
8685
|
-
const
|
|
8686
|
-
if (
|
|
8925
|
+
async function resolveChrome(need = "open the deck with") {
|
|
8926
|
+
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
8927
|
+
if (env) return { path: env, source: "env" };
|
|
8687
8928
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
8688
|
-
const
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8929
|
+
const pick = async (cacheDir) => {
|
|
8930
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
8931
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
8932
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
8933
|
+
};
|
|
8934
|
+
const hf = await pick(join8(homedir2(), ".cache", "hyperframes", "chrome"));
|
|
8935
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
8936
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join8(homedir2(), ".cache", "puppeteer"));
|
|
8937
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
8692
8938
|
throw new Error(
|
|
8693
8939
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
8694
8940
|
);
|
|
8695
8941
|
}
|
|
8942
|
+
async function chromePath(need = "open the deck with") {
|
|
8943
|
+
return (await resolveChrome(need)).path;
|
|
8944
|
+
}
|
|
8696
8945
|
function runtimePath() {
|
|
8697
8946
|
return createRequire2(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
8698
8947
|
}
|
|
@@ -8709,12 +8958,24 @@ async function openDeck(dir, opts = {}) {
|
|
|
8709
8958
|
if (!width || !height) throw new Error("index.html declares no canvas size");
|
|
8710
8959
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
8711
8960
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
8961
|
+
const chrome2 = await resolveChrome();
|
|
8712
8962
|
const browser = await puppeteer.launch({
|
|
8713
|
-
executablePath:
|
|
8963
|
+
executablePath: chrome2.path,
|
|
8714
8964
|
headless: true,
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8965
|
+
args: [
|
|
8966
|
+
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
8967
|
+
// renderer's.
|
|
8968
|
+
"--force-device-scale-factor=1",
|
|
8969
|
+
"--hide-scrollbars",
|
|
8970
|
+
// WITHOUT THESE A WebGL CANVAS GETS NO CONTEXT AND SCREENSHOTS EMPTY. The
|
|
8971
|
+
// renderer passes its own GL arguments; this path passed none, so it drew
|
|
8972
|
+
// a blank rectangle and reported success. SwiftShader rather than the host
|
|
8973
|
+
// GPU on purpose: it is the one backend available on every machine, and a
|
|
8974
|
+
// gate that quietly follows the host's hardware is measuring the host.
|
|
8975
|
+
"--use-gl=angle",
|
|
8976
|
+
"--use-angle=swiftshader",
|
|
8977
|
+
"--enable-unsafe-swiftshader"
|
|
8978
|
+
]
|
|
8718
8979
|
});
|
|
8719
8980
|
try {
|
|
8720
8981
|
const page = await browser.newPage();
|
|
@@ -8730,6 +8991,18 @@ async function openDeck(dir, opts = {}) {
|
|
|
8730
8991
|
{ timeout }
|
|
8731
8992
|
);
|
|
8732
8993
|
await page.evaluate(() => document.fonts.ready);
|
|
8994
|
+
const canvas = await page.evaluate(() => {
|
|
8995
|
+
const el = document.querySelector("canvas");
|
|
8996
|
+
if (!el) return { present: false, gl: true };
|
|
8997
|
+
const probe2 = document.createElement("canvas");
|
|
8998
|
+
const gl = !!(probe2.getContext("webgl2") ?? probe2.getContext("webgl"));
|
|
8999
|
+
return { present: true, gl };
|
|
9000
|
+
});
|
|
9001
|
+
if (canvas.present && !canvas.gl) {
|
|
9002
|
+
throw new Error(
|
|
9003
|
+
`this deck draws on a <canvas> and ${chrome2.path} cannot create a WebGL context, so the capture would be of an empty rectangle. Chrome came from ${chrome2.source}; the renderer uses hyperframes' own shell. Set HYPERFRAMES_BROWSER_PATH to the binary \`hyperframes render\` uses so the gate and the render are the same browser.`
|
|
9004
|
+
);
|
|
9005
|
+
}
|
|
8733
9006
|
const cdp = await page.createCDPSession();
|
|
8734
9007
|
return {
|
|
8735
9008
|
width,
|
|
@@ -9251,7 +9524,7 @@ ${tail3}`);
|
|
|
9251
9524
|
}
|
|
9252
9525
|
}
|
|
9253
9526
|
function runLive(file, args) {
|
|
9254
|
-
return new Promise((
|
|
9527
|
+
return new Promise((resolve7, reject) => {
|
|
9255
9528
|
const child = spawn3(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
9256
9529
|
child.on("error", (err) => {
|
|
9257
9530
|
reject(
|
|
@@ -9259,7 +9532,7 @@ function runLive(file, args) {
|
|
|
9259
9532
|
);
|
|
9260
9533
|
});
|
|
9261
9534
|
child.on("close", (code, signal) => {
|
|
9262
|
-
if (code === 0)
|
|
9535
|
+
if (code === 0) resolve7();
|
|
9263
9536
|
else if (signal) {
|
|
9264
9537
|
reject(
|
|
9265
9538
|
new Error(
|
|
@@ -10083,7 +10356,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
10083
10356
|
"--disable-background-networking",
|
|
10084
10357
|
"--disable-extensions",
|
|
10085
10358
|
"--no-default-browser-check",
|
|
10086
|
-
"--no-first-run"
|
|
10359
|
+
"--no-first-run",
|
|
10360
|
+
// The floor under interception — see the note above. Not a hardening
|
|
10361
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
10362
|
+
// happens to be that run.
|
|
10363
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
10087
10364
|
]
|
|
10088
10365
|
});
|
|
10089
10366
|
try {
|
|
@@ -11052,9 +11329,57 @@ function textOf(node) {
|
|
|
11052
11329
|
}
|
|
11053
11330
|
}
|
|
11054
11331
|
|
|
11332
|
+
// src/tmpdir.ts
|
|
11333
|
+
import { realpathSync } from "node:fs";
|
|
11334
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
11335
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
11336
|
+
import { basename as basename4, dirname as dirname4, join as join12, resolve as resolve5, sep } from "node:path";
|
|
11337
|
+
var cached;
|
|
11338
|
+
function real(dir) {
|
|
11339
|
+
let at = resolve5(dir);
|
|
11340
|
+
const tail3 = [];
|
|
11341
|
+
for (; ; ) {
|
|
11342
|
+
try {
|
|
11343
|
+
return join12(realpathSync(at), ...tail3);
|
|
11344
|
+
} catch {
|
|
11345
|
+
const up = dirname4(at);
|
|
11346
|
+
if (up === at) return resolve5(dir);
|
|
11347
|
+
tail3.unshift(basename4(at));
|
|
11348
|
+
at = up;
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11351
|
+
}
|
|
11352
|
+
function packageRoot() {
|
|
11353
|
+
cached ??= real(dirname4(createRequire3(import.meta.url).resolve("../package.json")));
|
|
11354
|
+
return cached;
|
|
11355
|
+
}
|
|
11356
|
+
function insideRoot(dir) {
|
|
11357
|
+
const root = packageRoot();
|
|
11358
|
+
const at = real(dir);
|
|
11359
|
+
return at === root || at.startsWith(root + sep);
|
|
11360
|
+
}
|
|
11361
|
+
function guardTmpdir() {
|
|
11362
|
+
const before = tmpdir3();
|
|
11363
|
+
if (!insideRoot(before)) return;
|
|
11364
|
+
delete process.env.TMPDIR;
|
|
11365
|
+
const after = tmpdir3();
|
|
11366
|
+
if (insideRoot(after)) {
|
|
11367
|
+
throw new Error(
|
|
11368
|
+
`tmpdir: ${after} is still inside ${packageRoot()}. Set TMPDIR to a real temp directory.`
|
|
11369
|
+
);
|
|
11370
|
+
}
|
|
11371
|
+
const worker = process.env.VITEST_WORKER_ID;
|
|
11372
|
+
if (worker === void 0 || worker === "1") {
|
|
11373
|
+
process.stderr.write(
|
|
11374
|
+
`tmpdir: TMPDIR pointed at ${before}, inside the source tree, so every mkdtemp would land in the repo. Using ${after}. Fix the environment \u2014 this only stops the mess.
|
|
11375
|
+
`
|
|
11376
|
+
);
|
|
11377
|
+
}
|
|
11378
|
+
}
|
|
11379
|
+
|
|
11055
11380
|
// src/verify/index.ts
|
|
11056
11381
|
import { readdir as readdir3, readFile as readFile16 } from "node:fs/promises";
|
|
11057
|
-
import { join as
|
|
11382
|
+
import { join as join16, relative, sep as sep2 } from "node:path";
|
|
11058
11383
|
|
|
11059
11384
|
// src/verify/budget.ts
|
|
11060
11385
|
function readCanvas(html) {
|
|
@@ -11162,7 +11487,7 @@ function clock2(seconds) {
|
|
|
11162
11487
|
// src/verify/check.ts
|
|
11163
11488
|
import { execFile as execFile2 } from "node:child_process";
|
|
11164
11489
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
11165
|
-
import { join as
|
|
11490
|
+
import { join as join13 } from "node:path";
|
|
11166
11491
|
import { promisify as promisify2 } from "node:util";
|
|
11167
11492
|
var run2 = promisify2(execFile2);
|
|
11168
11493
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -11192,7 +11517,7 @@ async function check2(dir, opts = {}) {
|
|
|
11192
11517
|
async function readComposition(dir) {
|
|
11193
11518
|
let html;
|
|
11194
11519
|
try {
|
|
11195
|
-
html = await readFile13(
|
|
11520
|
+
html = await readFile13(join13(dir, "index.html"), "utf8");
|
|
11196
11521
|
} catch {
|
|
11197
11522
|
return { transit: [], duration: 0 };
|
|
11198
11523
|
}
|
|
@@ -11341,7 +11666,7 @@ function tail(s) {
|
|
|
11341
11666
|
|
|
11342
11667
|
// src/verify/fidelity.ts
|
|
11343
11668
|
import { readFile as readFile14 } from "node:fs/promises";
|
|
11344
|
-
import { join as
|
|
11669
|
+
import { join as join14 } from "node:path";
|
|
11345
11670
|
|
|
11346
11671
|
// src/verify/typefloor.ts
|
|
11347
11672
|
var TYPE_FLOOR_PX = 40;
|
|
@@ -11788,8 +12113,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
11788
12113
|
elapsedMs: Date.now() - started
|
|
11789
12114
|
});
|
|
11790
12115
|
const stops = opts.stops ?? readStops(
|
|
11791
|
-
await readFile14(
|
|
11792
|
-
await readFile14(
|
|
12116
|
+
await readFile14(join14(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12117
|
+
await readFile14(join14(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
11793
12118
|
);
|
|
11794
12119
|
if (stops.length === 0) return notMeasured("the deck declares no stops");
|
|
11795
12120
|
let deck = null;
|
|
@@ -11848,8 +12173,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
11848
12173
|
import { execFile as execFile3 } from "node:child_process";
|
|
11849
12174
|
import { createHash as createHash9 } from "node:crypto";
|
|
11850
12175
|
import { mkdtemp as mkdtemp3, readdir as readdir2, readFile as readFile15, rm as rm8 } from "node:fs/promises";
|
|
11851
|
-
import { tmpdir as
|
|
11852
|
-
import { join as
|
|
12176
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
12177
|
+
import { join as join15 } from "node:path";
|
|
11853
12178
|
import { promisify as promisify3 } from "node:util";
|
|
11854
12179
|
var run3 = promisify3(execFile3);
|
|
11855
12180
|
var FLOOR_DB = 40;
|
|
@@ -11888,9 +12213,9 @@ function measureMotion(hashes, scenes, seconds) {
|
|
|
11888
12213
|
async function drift(dir, opts = {}) {
|
|
11889
12214
|
const mode = opts.mode ?? "psnr";
|
|
11890
12215
|
const floorDb = opts.floorDb ?? FLOOR_DB;
|
|
11891
|
-
const work = opts.workDir ?? await mkdtemp3(
|
|
11892
|
-
const a =
|
|
11893
|
-
const b =
|
|
12216
|
+
const work = opts.workDir ?? await mkdtemp3(join15(tmpdir4(), "decksmith-drift-"));
|
|
12217
|
+
const a = join15(work, "a");
|
|
12218
|
+
const b = join15(work, "b");
|
|
11894
12219
|
const report2 = await compare(dir, a, b, mode, floorDb, opts);
|
|
11895
12220
|
const keep = opts.keep || !report2.passed;
|
|
11896
12221
|
if (keep) report2.kept = { a, b };
|
|
@@ -11945,11 +12270,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
|
|
|
11945
12270
|
const differing = [];
|
|
11946
12271
|
const ha = [];
|
|
11947
12272
|
for (const [i, name] of fa.entries()) {
|
|
11948
|
-
const one = await sha(
|
|
12273
|
+
const one = await sha(join15(a, name));
|
|
11949
12274
|
ha.push(one);
|
|
11950
|
-
if (one !== await sha(
|
|
12275
|
+
if (one !== await sha(join15(b, name))) differing.push(i + 1);
|
|
11951
12276
|
}
|
|
11952
|
-
const html = await readFile15(
|
|
12277
|
+
const html = await readFile15(join15(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
|
|
11953
12278
|
const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
|
|
11954
12279
|
let worst;
|
|
11955
12280
|
if (differing.length > 0) {
|
|
@@ -12097,11 +12422,11 @@ async function psnr(a, b, firstFrame) {
|
|
|
12097
12422
|
"-start_number",
|
|
12098
12423
|
String(seq.start),
|
|
12099
12424
|
"-i",
|
|
12100
|
-
|
|
12425
|
+
join15(a, seq.pattern),
|
|
12101
12426
|
"-start_number",
|
|
12102
12427
|
String(seq.start),
|
|
12103
12428
|
"-i",
|
|
12104
|
-
|
|
12429
|
+
join15(b, seq.pattern),
|
|
12105
12430
|
"-lavfi",
|
|
12106
12431
|
"psnr=stats_file=-",
|
|
12107
12432
|
"-f",
|
|
@@ -12176,7 +12501,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12176
12501
|
const html = await readCompositions(dir);
|
|
12177
12502
|
const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
|
|
12178
12503
|
const narration = scanNarration(
|
|
12179
|
-
await readFile16(
|
|
12504
|
+
await readFile16(join16(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12180
12505
|
await listFiles(dir)
|
|
12181
12506
|
);
|
|
12182
12507
|
const budget3 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
|
|
@@ -12213,8 +12538,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12213
12538
|
}
|
|
12214
12539
|
async function declaredStops(dir) {
|
|
12215
12540
|
return readStops(
|
|
12216
|
-
await readFile16(
|
|
12217
|
-
await readFile16(
|
|
12541
|
+
await readFile16(join16(dir, TIMING_FILE), "utf8").catch(() => null),
|
|
12542
|
+
await readFile16(join16(dir, DECK_PAGE), "utf8").catch(() => null)
|
|
12218
12543
|
);
|
|
12219
12544
|
}
|
|
12220
12545
|
var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
|
|
@@ -12254,7 +12579,7 @@ function scanNarration(page, files) {
|
|
|
12254
12579
|
];
|
|
12255
12580
|
}
|
|
12256
12581
|
async function readTiming2(dir) {
|
|
12257
|
-
const raw2 = await readFile16(
|
|
12582
|
+
const raw2 = await readFile16(join16(dir, TIMING_FILE), "utf8").catch(() => "");
|
|
12258
12583
|
if (!raw2) return void 0;
|
|
12259
12584
|
try {
|
|
12260
12585
|
const parsed = JSON.parse(raw2);
|
|
@@ -12266,7 +12591,7 @@ async function readTiming2(dir) {
|
|
|
12266
12591
|
async function listFiles(dir) {
|
|
12267
12592
|
const entries = await readdir3(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12268
12593
|
return new Set(
|
|
12269
|
-
entries.filter((e) => e.isFile()).map((e) => relative(dir,
|
|
12594
|
+
entries.filter((e) => e.isFile()).map((e) => relative(dir, join16(e.parentPath, e.name)).split(sep2).join("/"))
|
|
12270
12595
|
);
|
|
12271
12596
|
}
|
|
12272
12597
|
var INSTEAD = {
|
|
@@ -12525,13 +12850,14 @@ async function readCompositions(dir) {
|
|
|
12525
12850
|
const entries = await readdir3(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
12526
12851
|
const files = entries.filter(
|
|
12527
12852
|
(e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
|
|
12528
|
-
).map((e) =>
|
|
12853
|
+
).map((e) => join16(e.parentPath, e.name));
|
|
12529
12854
|
return Promise.all(
|
|
12530
12855
|
files.map(async (f) => [relative(dir, f), await readFile16(f, "utf8")])
|
|
12531
12856
|
);
|
|
12532
12857
|
}
|
|
12533
12858
|
|
|
12534
12859
|
// src/cli.ts
|
|
12860
|
+
guardTmpdir();
|
|
12535
12861
|
var AUDIO_DIR = "audio";
|
|
12536
12862
|
var NARRATION_FILE = "narration.json";
|
|
12537
12863
|
var DEFAULTS = prefsSchema.parse({});
|
|
@@ -12544,43 +12870,48 @@ async function deckRuntime() {
|
|
|
12544
12870
|
}
|
|
12545
12871
|
}
|
|
12546
12872
|
function playerBundle() {
|
|
12547
|
-
const require2 =
|
|
12873
|
+
const require2 = createRequire4(import.meta.url);
|
|
12548
12874
|
try {
|
|
12549
|
-
return
|
|
12875
|
+
return join17(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12550
12876
|
} catch {
|
|
12551
12877
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12552
12878
|
}
|
|
12553
12879
|
}
|
|
12554
12880
|
async function vendorKatex(out) {
|
|
12555
|
-
const require2 =
|
|
12556
|
-
const dist =
|
|
12557
|
-
const css = await readFile17(
|
|
12558
|
-
await mkdir10(
|
|
12559
|
-
for (const file of await readdir4(
|
|
12881
|
+
const require2 = createRequire4(import.meta.url);
|
|
12882
|
+
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12883
|
+
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12884
|
+
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12885
|
+
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12560
12886
|
if (file.endsWith(".woff2"))
|
|
12561
|
-
await cp(
|
|
12887
|
+
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12562
12888
|
}
|
|
12563
12889
|
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12564
12890
|
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12565
12891
|
return kept ? `src:${kept}` : whole;
|
|
12566
12892
|
});
|
|
12567
|
-
await writeFile12(
|
|
12893
|
+
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12568
12894
|
}
|
|
12569
|
-
async function vendorScripts(out) {
|
|
12570
|
-
const require2 =
|
|
12571
|
-
await mkdir10(
|
|
12895
|
+
async function vendorScripts(out, composition) {
|
|
12896
|
+
const require2 = createRequire4(import.meta.url);
|
|
12897
|
+
await mkdir10(join17(out, "vendor"), { recursive: true });
|
|
12898
|
+
const wanted = (name) => composition.includes(`./vendor/${name}`);
|
|
12572
12899
|
for (const [pkg, rel, name] of [
|
|
12573
12900
|
["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
|
|
12574
12901
|
["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
|
|
12902
|
+
["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
|
|
12575
12903
|
["katex/package.json", "dist/katex.min.js", "katex.min.js"]
|
|
12576
12904
|
]) {
|
|
12577
|
-
|
|
12578
|
-
|
|
12905
|
+
if (!wanted(name)) continue;
|
|
12906
|
+
const from = join17(dirname5(require2.resolve(pkg)), rel);
|
|
12907
|
+
await cp(from, join17(out, "vendor", name));
|
|
12908
|
+
}
|
|
12909
|
+
if (wanted("ds-morph.js")) {
|
|
12910
|
+
await cp(
|
|
12911
|
+
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
12912
|
+
join17(out, "vendor", "ds-morph.js")
|
|
12913
|
+
);
|
|
12579
12914
|
}
|
|
12580
|
-
await cp(
|
|
12581
|
-
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
12582
|
-
join16(out, "vendor", "ds-morph.js")
|
|
12583
|
-
);
|
|
12584
12915
|
}
|
|
12585
12916
|
var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
12586
12917
|
{
|
|
@@ -12683,13 +13014,13 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
12683
13014
|
"--max-clip-seconds <n>",
|
|
12684
13015
|
"URL only: seconds of each clip kept, the rest trimmed (maxClipSeconds; default 60)"
|
|
12685
13016
|
).option("--no-transcode", "URL only: ship each clip as the page served it, unshrunk").action(async (input, o) => {
|
|
12686
|
-
const assets =
|
|
12687
|
-
const scratch = url(input) ? await mkdtemp4(
|
|
13017
|
+
const assets = join17(dirname5(resolve6(o.out)), "assets");
|
|
13018
|
+
const scratch = url(input) ? await mkdtemp4(join17(tmpdir5(), "decksmith-harvest-")) : void 0;
|
|
12688
13019
|
try {
|
|
12689
13020
|
let md;
|
|
12690
13021
|
let clips = [];
|
|
12691
13022
|
if (scratch === void 0) {
|
|
12692
|
-
md = await readFile17(
|
|
13023
|
+
md = await readFile17(resolve6(input), "utf8").catch(() => {
|
|
12693
13024
|
throw new Error(`Cannot read ${input}.`);
|
|
12694
13025
|
});
|
|
12695
13026
|
} else {
|
|
@@ -12710,7 +13041,7 @@ program.command("ingest").description("Parse a document or a web page into sourc
|
|
|
12710
13041
|
const dropped = [];
|
|
12711
13042
|
const source = await fetchFigures(withClips, assets, dropped);
|
|
12712
13043
|
for (const why3 of dropped) step(`ingest: ${why3}`);
|
|
12713
|
-
const bundle = await bundleFont(source.lang, glyphs(source),
|
|
13044
|
+
const bundle = await bundleFont(source.lang, glyphs(source), join17(assets, "fonts"));
|
|
12714
13045
|
if (bundle) step(`ingest: bundled ${bundle.family} for ${source.lang}`);
|
|
12715
13046
|
await writeJson(o.out, source);
|
|
12716
13047
|
step(`ingest: wrote ${o.out}`);
|
|
@@ -12771,7 +13102,7 @@ imageFlags(
|
|
|
12771
13102
|
program.command("illustrate").description("Draw the pictures the plan asked for, and point the storyboard at them.").argument("<storyboard>", "storyboard.json carrying illustration briefs").requiredOption("--source <file>", "source.json the storyboard was planned from")
|
|
12772
13103
|
).action(async (sbPath, o) => {
|
|
12773
13104
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
12774
|
-
const sourcePath =
|
|
13105
|
+
const sourcePath = resolve6(String(o.source));
|
|
12775
13106
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
12776
13107
|
assertRefsResolve(storyboard, source, { pending: "allow" });
|
|
12777
13108
|
const prefs = await loadPrefs(prefsFromFlags({ ...flags(o), images: true }));
|
|
@@ -12785,7 +13116,7 @@ imageFlags(
|
|
|
12785
13116
|
);
|
|
12786
13117
|
const drawn = await illustrate(storyboard, source, {
|
|
12787
13118
|
prefs,
|
|
12788
|
-
assetsDir:
|
|
13119
|
+
assetsDir: join17(dirname5(sourcePath), "assets"),
|
|
12789
13120
|
onStep: step
|
|
12790
13121
|
});
|
|
12791
13122
|
for (const p of drawn.illustrated) {
|
|
@@ -12795,9 +13126,9 @@ imageFlags(
|
|
|
12795
13126
|
}
|
|
12796
13127
|
await writeJson(sourcePath, drawn.source);
|
|
12797
13128
|
await writeJson(sbPath, drawn.storyboard);
|
|
12798
|
-
const
|
|
13129
|
+
const cached2 = drawn.illustrated.filter((p) => p.cached).length;
|
|
12799
13130
|
step(
|
|
12800
|
-
`illustrate: ${drawn.illustrated.length} picture(s)${
|
|
13131
|
+
`illustrate: ${drawn.illustrated.length} picture(s)${cached2 ? `, ${cached2} cached` : ""} \u2192 ${o.source}, ${sbPath}`
|
|
12801
13132
|
);
|
|
12802
13133
|
});
|
|
12803
13134
|
voiceFlags(
|
|
@@ -12810,18 +13141,18 @@ voiceFlags(
|
|
|
12810
13141
|
const format = pickFormat(String(o.format), o.width, o.height);
|
|
12811
13142
|
const chosen = await loadPrefs(prefsFromFlags(flags(o)));
|
|
12812
13143
|
const prefs = stated(chosen, "lang") ? chosen : { ...chosen, lang: storyboard.lang };
|
|
12813
|
-
const dir =
|
|
13144
|
+
const dir = resolve6(String(o.out));
|
|
12814
13145
|
const speaking = storyboard.beats.filter((b) => b.narration?.trim()).length;
|
|
12815
13146
|
if (speaking === 0) {
|
|
12816
13147
|
throw new Error(`No beat in ${sbPath} has a "narration" field, so there is nothing to speak.`);
|
|
12817
13148
|
}
|
|
12818
13149
|
step(`narrate: ${speaking} of ${storyboard.beats.length} beats have narration`);
|
|
12819
13150
|
const narration = await narrate(storyboard, source, prefs, { dir, format });
|
|
12820
|
-
await writeJson(
|
|
13151
|
+
await writeJson(join17(dir, NARRATION_FILE), narration);
|
|
12821
13152
|
const segments2 = Object.values(narration.beats).flat();
|
|
12822
13153
|
const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
|
|
12823
13154
|
step(
|
|
12824
|
-
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${
|
|
13155
|
+
`narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${join17(dir, NARRATION_FILE)}`
|
|
12825
13156
|
);
|
|
12826
13157
|
});
|
|
12827
13158
|
lookFlags(
|
|
@@ -12836,7 +13167,7 @@ lookFlags(
|
|
|
12836
13167
|
const format = withMinWeight(pickFormat(o.format, o.width, o.height), o.minWeight);
|
|
12837
13168
|
const prefs = await loadPrefs(prefsFromFlags(flags(o)), process.cwd(), source);
|
|
12838
13169
|
const theme = stated(prefs, "theme") ?? storyboard.theme;
|
|
12839
|
-
const out =
|
|
13170
|
+
const out = resolve6(o.out);
|
|
12840
13171
|
await mkdir10(out, { recursive: true });
|
|
12841
13172
|
const found = await findNarration(sbPath, o.narration);
|
|
12842
13173
|
const narration = found ? await loadNarration(found) : void 0;
|
|
@@ -12861,10 +13192,14 @@ lookFlags(
|
|
|
12861
13192
|
// takes eleven good ones with it at the last stage. Printed, not swallowed:
|
|
12862
13193
|
// a missing slide the terminal never mentioned is the failure this project
|
|
12863
13194
|
// keeps finding by eye.
|
|
12864
|
-
onBeatError: (id2, err) => step(`build: left out ${id2} \u2014 ${err.message}`)
|
|
13195
|
+
onBeatError: (id2, err) => step(`build: left out ${id2} \u2014 ${err.message}`),
|
|
13196
|
+
// And the slides that ARE here but not as they were planned. A beat drawn
|
|
13197
|
+
// with one of its parts dropped looks finished, so this line is the only
|
|
13198
|
+
// place anyone learns it is not.
|
|
13199
|
+
onBeatWarning: (id2, warning) => step(`build: kept ${id2} \u2014 ${warning}`)
|
|
12865
13200
|
});
|
|
12866
|
-
await writeFile12(
|
|
12867
|
-
await writeFile12(
|
|
13201
|
+
await writeFile12(join17(out, "index.html"), deck.composition);
|
|
13202
|
+
await writeFile12(join17(out, "hyperframes.json"), HYPERFRAMES_JSON);
|
|
12868
13203
|
await writeTiming(out, {
|
|
12869
13204
|
storyboard,
|
|
12870
13205
|
source,
|
|
@@ -12880,36 +13215,36 @@ lookFlags(
|
|
|
12880
13215
|
...narration ? { narration } : {}
|
|
12881
13216
|
});
|
|
12882
13217
|
if (deck.page) {
|
|
12883
|
-
await writeFile12(
|
|
12884
|
-
await cp(playerBundle(),
|
|
13218
|
+
await writeFile12(join17(out, DECK_PAGE), deck.page);
|
|
13219
|
+
await cp(playerBundle(), join17(out, PLAYER_FILE));
|
|
12885
13220
|
}
|
|
12886
13221
|
await vendorKatex(out);
|
|
12887
|
-
await vendorScripts(out);
|
|
12888
|
-
await copyAssets(
|
|
12889
|
-
if (found && narration) await copyAudio(
|
|
13222
|
+
await vendorScripts(out, deck.composition);
|
|
13223
|
+
await copyAssets(dirname5(resolve6(o.source)), out, source.figures);
|
|
13224
|
+
if (found && narration) await copyAudio(dirname5(found), narration, out);
|
|
12890
13225
|
const look = [theme, paced.speed === 1 ? "" : `${paced.speed}\xD7 speed`].filter(Boolean).join(", ");
|
|
12891
13226
|
const { cut } = deck;
|
|
12892
13227
|
const floor = cut.dropped.filter((d) => d.rule === "below_min_weight").length;
|
|
12893
13228
|
const of = cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
12894
13229
|
step(
|
|
12895
|
-
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${
|
|
13230
|
+
`build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${join17(out, "index.html")}`
|
|
12896
13231
|
);
|
|
12897
13232
|
for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
|
|
12898
13233
|
step(`build: ${f.message}`);
|
|
12899
13234
|
reportCut(cut);
|
|
12900
|
-
if (deck.page) step(`build: navigable deck \u2192 ${
|
|
13235
|
+
if (deck.page) step(`build: navigable deck \u2192 ${join17(out, DECK_PAGE)}`);
|
|
12901
13236
|
await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
|
|
12902
13237
|
}
|
|
12903
13238
|
);
|
|
12904
13239
|
program.command("verify").description("Re-run the gates on a built deck.").argument("<dir>", "a built deck directory").option("--snapshots", "also write the contrast-pass PNGs to <dir>/snapshots", false).option("--no-fidelity", "skip the frame check \u2014 only for a machine with no browser").action(async (dir, o) => {
|
|
12905
|
-
await gate(
|
|
13240
|
+
await gate(resolve6(dir), o.snapshots, void 0, void 0, o.fidelity !== false);
|
|
12906
13241
|
});
|
|
12907
13242
|
program.command("frames").description("Write PNGs of a built deck through the capture path the renderer uses.").argument("<dir>", "a built deck directory").option(
|
|
12908
13243
|
"--at <seconds...>",
|
|
12909
13244
|
"absolute times to photograph; defaults to every hold the deck declares"
|
|
12910
13245
|
).option("--out <dir>", "where to write the PNGs (default: <dir>/frames)").action(async (dir, o) => {
|
|
12911
|
-
const deck =
|
|
12912
|
-
const out =
|
|
13246
|
+
const deck = resolve6(dir);
|
|
13247
|
+
const out = resolve6(o.out ?? join17(deck, "frames"));
|
|
12913
13248
|
let times;
|
|
12914
13249
|
if (o.at?.length) {
|
|
12915
13250
|
times = o.at.map((raw2) => {
|
|
@@ -12919,8 +13254,8 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
12919
13254
|
});
|
|
12920
13255
|
} else {
|
|
12921
13256
|
times = readStops(
|
|
12922
|
-
await readFile17(
|
|
12923
|
-
await readFile17(
|
|
13257
|
+
await readFile17(join17(deck, TIMING_FILE), "utf8").catch(() => null),
|
|
13258
|
+
await readFile17(join17(deck, DECK_PAGE), "utf8").catch(() => null)
|
|
12924
13259
|
).map((s) => s.t);
|
|
12925
13260
|
if (times.length === 0)
|
|
12926
13261
|
throw new Error(
|
|
@@ -12934,7 +13269,7 @@ program.command("frames").description("Write PNGs of a built deck through the ca
|
|
|
12934
13269
|
program.command("drift").description("Render a built deck twice and compare, frame by frame. Minutes, not seconds.").argument("<dir>", "a built deck directory").option("--identical", "require every frame byte-identical \u2014 image-free decks only", false).option("--floor <dB>", "per-frame PSNR floor", String(FLOOR_DB)).option("--keep", "keep both frame directories even when they agree").option("--workers <n>", "pin both renders to one worker count (default: 1 vs 3)").action(
|
|
12935
13270
|
async (dir, o) => {
|
|
12936
13271
|
step("drift: rendering twice, several minutes a render");
|
|
12937
|
-
const verdict = await drift(
|
|
13272
|
+
const verdict = await drift(resolve6(dir), {
|
|
12938
13273
|
mode: o.identical ? "identical" : "psnr",
|
|
12939
13274
|
floorDb: Number(o.floor),
|
|
12940
13275
|
...o.keep ? { keep: true } : {},
|
|
@@ -12961,8 +13296,8 @@ program.command("render").description("Render a built deck to a finished video:
|
|
|
12961
13296
|
throw new Error(`Unknown --subtitles "${o.subtitles}". Use sidecar, burn or none.`);
|
|
12962
13297
|
}
|
|
12963
13298
|
const result = await render({
|
|
12964
|
-
deck:
|
|
12965
|
-
out:
|
|
13299
|
+
deck: resolve6(dir),
|
|
13300
|
+
out: resolve6(o.out),
|
|
12966
13301
|
subtitles: o.subtitles,
|
|
12967
13302
|
protocolTimeoutMs: Number(o.protocolTimeout),
|
|
12968
13303
|
log: step,
|
|
@@ -12990,7 +13325,7 @@ voiceFlags(
|
|
|
12990
13325
|
)
|
|
12991
13326
|
).action(async (sbPath, o) => {
|
|
12992
13327
|
const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
|
|
12993
|
-
const sourcePath =
|
|
13328
|
+
const sourcePath = resolve6(String(o.source));
|
|
12994
13329
|
const source = await readValidated(sourcePath, sourceSchema, "source");
|
|
12995
13330
|
assertRefsResolve(storyboard, source);
|
|
12996
13331
|
if (o.bake && o.link) throw new Error("Choose --bake or --link, not both.");
|
|
@@ -13005,7 +13340,7 @@ voiceFlags(
|
|
|
13005
13340
|
const prefer = o.link ? "link" : "bake";
|
|
13006
13341
|
const plan = await planMedia(figureAssets(source, sourcePath, prefer));
|
|
13007
13342
|
const files = { ...plan.files };
|
|
13008
|
-
if (found && narration) Object.assign(files, await audioFiles(
|
|
13343
|
+
if (found && narration) Object.assign(files, await audioFiles(dirname5(found), narration));
|
|
13009
13344
|
const pack2 = {
|
|
13010
13345
|
version: PACK_VERSION,
|
|
13011
13346
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -13020,7 +13355,7 @@ voiceFlags(
|
|
|
13020
13355
|
...narration ? { narration } : {},
|
|
13021
13356
|
media: plan.media
|
|
13022
13357
|
};
|
|
13023
|
-
const out =
|
|
13358
|
+
const out = resolve6(String(o.out));
|
|
13024
13359
|
const bytes = await writePack(pack2, files, out);
|
|
13025
13360
|
if (plan.demoted.length) {
|
|
13026
13361
|
step(`pack: kept as links, not bakeable \u2014 ${plan.demoted.join(", ")}`);
|
|
@@ -13034,19 +13369,19 @@ voiceFlags(
|
|
|
13034
13369
|
step(`pack: ${size2(bytes)} \u2192 ${out}`);
|
|
13035
13370
|
});
|
|
13036
13371
|
program.command("unpack").description("Open a .deck back into the files build reads.").argument("<file>", "a .deck archive").requiredOption("-o, --out <dir>", "directory to open it into").action(async (file, o) => {
|
|
13037
|
-
const { pack: pack2, files } = await readPack(
|
|
13038
|
-
const out =
|
|
13039
|
-
await writeJson(
|
|
13040
|
-
await writeJson(
|
|
13041
|
-
await writeJson(
|
|
13042
|
-
if (pack2.narration) await writeJson(
|
|
13372
|
+
const { pack: pack2, files } = await readPack(resolve6(file));
|
|
13373
|
+
const out = resolve6(o.out);
|
|
13374
|
+
await writeJson(join17(out, "source.json"), pack2.source);
|
|
13375
|
+
await writeJson(join17(out, "storyboard.json"), pack2.storyboard);
|
|
13376
|
+
await writeJson(join17(out, "decksmith.config.json"), pack2.prefs);
|
|
13377
|
+
if (pack2.narration) await writeJson(join17(out, AUDIO_DIR, NARRATION_FILE), pack2.narration);
|
|
13043
13378
|
const figures = new Map(pack2.source.figures.map((f) => [f.id, f.src]));
|
|
13044
13379
|
for (const [path2, bytes] of Object.entries(files)) {
|
|
13045
13380
|
const media = pack2.media.find((m) => m.path === path2);
|
|
13046
13381
|
const src = media && figures.get(media.id);
|
|
13047
|
-
const to = src ?
|
|
13048
|
-
await mkdir10(
|
|
13049
|
-
await writeFile12(
|
|
13382
|
+
const to = src ? join17("assets", src) : path2;
|
|
13383
|
+
await mkdir10(dirname5(join17(out, to)), { recursive: true });
|
|
13384
|
+
await writeFile12(join17(out, to), bytes);
|
|
13050
13385
|
}
|
|
13051
13386
|
const linked = pack2.media.filter((m) => m.policy !== "bake");
|
|
13052
13387
|
step(`unpack: "${pack2.title}", ${pack2.storyboard.beats.length} beats \u2192 ${out}`);
|
|
@@ -13056,7 +13391,7 @@ program.command("unpack").description("Open a .deck back into the files build re
|
|
|
13056
13391
|
);
|
|
13057
13392
|
}
|
|
13058
13393
|
step(
|
|
13059
|
-
`unpack: decksmith build ${
|
|
13394
|
+
`unpack: decksmith build ${join17(relative2(process.cwd(), out) || ".", "storyboard.json")} --source ${join17(relative2(process.cwd(), out) || ".", "source.json")} -o deck`
|
|
13060
13395
|
);
|
|
13061
13396
|
});
|
|
13062
13397
|
program.parseAsync(process.argv).catch((err) => {
|
|
@@ -13077,7 +13412,7 @@ async function gate(dir, snapshots = false, storyboard, kept, fidelity2 = true,
|
|
|
13077
13412
|
);
|
|
13078
13413
|
const verdict = await verify(dir, { snapshots, fidelity: fidelity2 }, storyboard, kept, source);
|
|
13079
13414
|
process.stdout.write(report(verdict));
|
|
13080
|
-
if (snapshots) step(`verify: snapshots in ${
|
|
13415
|
+
if (snapshots) step(`verify: snapshots in ${join17(dir, "snapshots")}`);
|
|
13081
13416
|
if (!verdict.passed) process.exitCode = 1;
|
|
13082
13417
|
}
|
|
13083
13418
|
function report(v) {
|
|
@@ -13094,12 +13429,12 @@ function report(v) {
|
|
|
13094
13429
|
async function findNarration(sbPath, flag) {
|
|
13095
13430
|
if (flag === false) return void 0;
|
|
13096
13431
|
if (typeof flag === "string") {
|
|
13097
|
-
const path2 =
|
|
13432
|
+
const path2 = resolve6(flag);
|
|
13098
13433
|
if (!await stat2(path2).catch(() => null)) throw new Error(`Cannot read narration ${flag}.`);
|
|
13099
13434
|
return path2;
|
|
13100
13435
|
}
|
|
13101
|
-
const beside =
|
|
13102
|
-
for (const candidate of [
|
|
13436
|
+
const beside = dirname5(resolve6(sbPath));
|
|
13437
|
+
for (const candidate of [join17(beside, AUDIO_DIR, NARRATION_FILE), join17(beside, NARRATION_FILE)]) {
|
|
13103
13438
|
if (await stat2(candidate).catch(() => null)) {
|
|
13104
13439
|
step(`narration: using ${candidate}`);
|
|
13105
13440
|
return candidate;
|
|
@@ -13119,11 +13454,11 @@ function audioNames(narration) {
|
|
|
13119
13454
|
].sort();
|
|
13120
13455
|
}
|
|
13121
13456
|
async function copyAudio(from, narration, out) {
|
|
13122
|
-
const dir =
|
|
13457
|
+
const dir = join17(out, AUDIO_DIR);
|
|
13123
13458
|
await mkdir10(dir, { recursive: true });
|
|
13124
13459
|
const names = audioNames(narration);
|
|
13125
13460
|
for (const name of names) {
|
|
13126
|
-
await cp(
|
|
13461
|
+
await cp(join17(from, name), join17(dir, name)).catch(() => {
|
|
13127
13462
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13128
13463
|
});
|
|
13129
13464
|
}
|
|
@@ -13132,7 +13467,7 @@ async function copyAudio(from, narration, out) {
|
|
|
13132
13467
|
async function audioFiles(from, narration) {
|
|
13133
13468
|
const files = {};
|
|
13134
13469
|
for (const name of audioNames(narration)) {
|
|
13135
|
-
const bytes = await readFile17(
|
|
13470
|
+
const bytes = await readFile17(join17(from, name)).catch(() => {
|
|
13136
13471
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
13137
13472
|
});
|
|
13138
13473
|
files[`${AUDIO_DIR}/${name}`] = new Uint8Array(bytes);
|
|
@@ -13154,9 +13489,9 @@ function wrapScript(text2, width) {
|
|
|
13154
13489
|
async function writeTiming(out, input) {
|
|
13155
13490
|
try {
|
|
13156
13491
|
const timing = planTiming(input);
|
|
13157
|
-
await writeJson(
|
|
13492
|
+
await writeJson(join17(out, TIMING_FILE), timing);
|
|
13158
13493
|
step(
|
|
13159
|
-
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${
|
|
13494
|
+
`build: timing for ${timing.segments.length} narration segment(s) \u2192 ${join17(out, TIMING_FILE)}`
|
|
13160
13495
|
);
|
|
13161
13496
|
} catch (err) {
|
|
13162
13497
|
step(
|
|
@@ -13165,10 +13500,10 @@ async function writeTiming(out, input) {
|
|
|
13165
13500
|
}
|
|
13166
13501
|
}
|
|
13167
13502
|
function figureAssets(source, sourcePath, prefer) {
|
|
13168
|
-
const assets =
|
|
13503
|
+
const assets = join17(dirname5(sourcePath), "assets");
|
|
13169
13504
|
return source.figures.map((f) => ({
|
|
13170
13505
|
id: f.id,
|
|
13171
|
-
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src :
|
|
13506
|
+
url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src : join17(assets, f.src),
|
|
13172
13507
|
prefer
|
|
13173
13508
|
}));
|
|
13174
13509
|
}
|
|
@@ -13197,7 +13532,7 @@ function withMinWeight(format, raw2) {
|
|
|
13197
13532
|
return { ...format, minWeight };
|
|
13198
13533
|
}
|
|
13199
13534
|
async function copyAssets(sourceDir, out, figures) {
|
|
13200
|
-
const from =
|
|
13535
|
+
const from = join17(sourceDir, "assets");
|
|
13201
13536
|
if (!await stat2(from).catch(() => null)) {
|
|
13202
13537
|
step(`build: no assets/ beside source.json, skipping`);
|
|
13203
13538
|
return;
|
|
@@ -13205,19 +13540,19 @@ async function copyAssets(sourceDir, out, figures) {
|
|
|
13205
13540
|
const wanted = new Set(
|
|
13206
13541
|
figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
|
|
13207
13542
|
);
|
|
13208
|
-
await mkdir10(
|
|
13543
|
+
await mkdir10(join17(out, "assets"), { recursive: true });
|
|
13209
13544
|
let copied = 0;
|
|
13210
13545
|
for (const name of wanted) {
|
|
13211
|
-
const src =
|
|
13212
|
-
if (!src.startsWith(`${
|
|
13546
|
+
const src = resolve6(join17(from, name));
|
|
13547
|
+
if (!src.startsWith(`${resolve6(from)}/`)) continue;
|
|
13213
13548
|
if (!await stat2(src).catch(() => null)) continue;
|
|
13214
|
-
await mkdir10(
|
|
13215
|
-
await cp(src,
|
|
13549
|
+
await mkdir10(dirname5(join17(out, "assets", name)), { recursive: true });
|
|
13550
|
+
await cp(src, join17(out, "assets", name));
|
|
13216
13551
|
copied++;
|
|
13217
13552
|
}
|
|
13218
|
-
const fonts =
|
|
13553
|
+
const fonts = join17(from, "fonts");
|
|
13219
13554
|
if (await stat2(fonts).catch(() => null)) {
|
|
13220
|
-
await cp(fonts,
|
|
13555
|
+
await cp(fonts, join17(out, "assets", "fonts"), { recursive: true });
|
|
13221
13556
|
}
|
|
13222
13557
|
step(`build: copied ${copied} referenced figure(s)`);
|
|
13223
13558
|
}
|
|
@@ -13226,7 +13561,7 @@ async function refreshFont(storyboard, source, out) {
|
|
|
13226
13561
|
const bundle = await bundleFont(
|
|
13227
13562
|
storyboard.lang,
|
|
13228
13563
|
glyphs(source) + glyphs(storyboard),
|
|
13229
|
-
|
|
13564
|
+
join17(out, "assets", "fonts")
|
|
13230
13565
|
);
|
|
13231
13566
|
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
13232
13567
|
return bundle?.css;
|
|
@@ -13241,7 +13576,7 @@ function glyphs(value) {
|
|
|
13241
13576
|
return JSON.stringify(value);
|
|
13242
13577
|
}
|
|
13243
13578
|
async function readValidated(file, schema, label) {
|
|
13244
|
-
const text2 = await readFile17(
|
|
13579
|
+
const text2 = await readFile17(resolve6(file), "utf8").catch(() => {
|
|
13245
13580
|
throw new Error(`Cannot read ${label} file ${file}.`);
|
|
13246
13581
|
});
|
|
13247
13582
|
let json;
|
|
@@ -13259,8 +13594,8 @@ ${issues}`);
|
|
|
13259
13594
|
return parsed.data;
|
|
13260
13595
|
}
|
|
13261
13596
|
async function writeJson(file, value) {
|
|
13262
|
-
const path2 =
|
|
13263
|
-
await mkdir10(
|
|
13597
|
+
const path2 = resolve6(file);
|
|
13598
|
+
await mkdir10(dirname5(path2), { recursive: true });
|
|
13264
13599
|
await writeFile12(path2, `${JSON.stringify(value, null, 2)}
|
|
13265
13600
|
`);
|
|
13266
13601
|
}
|