@jokerized/decksmith 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 createRequire3 } from "node:module";
6
- import { tmpdir as tmpdir4 } from "node:os";
7
- import { dirname as dirname4, join as join16, relative as relative2, resolve as resolve5 } from "node:path";
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(z.object({ x: z.string(), y: z.number() })).min(2),
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 cached = await readFile2(cssPath, "utf8").catch(() => "");
1210
- if (cached.startsWith(stamp)) return { family, css: cached, files: localNames(cached) };
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 scale = chartScale(p.points.map((pt) => pt.y));
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 path2 = p.points.map((pt, i) => `${i === 0 ? "M" : "L"}${n2(x(i))},${n2(y(pt.y))}`).join(" ");
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
- <path class="chartline" id="${sid}-line" d="${path2}" fill="none" stroke="${theme.accent}" />
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 step2 = Math.min(0.45, 1.8 / p.points.length);
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(`#${sid}-line`, DRAW_FROM, { ...DRAW_TO, duration: 1.8, ease: "none" }, draw2),
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
- draw2
4423
+ settled
4249
4424
  ),
4250
- tween(`#${sid} .pv`, { opacity: 0 }, { opacity: 1, duration: 0.3, stagger: step2 }, draw2 + 0.2)
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
- draw2 + 0.6
4438
+ settled + 0.6
4259
4439
  )
4260
4440
  );
4261
4441
  }
4262
- if (p.points.length > 1) {
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 }, draw2),
4266
- ...travel(`#${sid}-ring`, route, draw2, 1.8),
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
- draw2 + 1.8
4454
+ settled + walk
4274
4455
  )
4275
4456
  );
4276
4457
  }
4277
- const drawn = draw2 + step2 * p.points.length + 0.4;
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 MORPH_SRC = "./vendor/ds-morph.js";
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 p of scene.plugins ?? []) plugins.add(p);
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 morph = laid.plugins.has("dsMorph") ? `
5785
- <script src="${MORPH_SRC}"></script>
5786
- <script>gsap.registerPlugin(DSMorphPlugin);</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>${morph}
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((resolve6, reject) => {
6191
- const req = (secure ? httpsRequest : httpRequest)(options, resolve6);
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 cached = await readdir(dir).catch(() => []);
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, cached)));
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, cached) {
6514
+ async function localize(figure, dir, cached2) {
6300
6515
  const stem = assetStem(figure.id, figure.src);
6301
- const hit = cached.find((name) => name.startsWith(`${stem}.`));
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
- cached.push(src);
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
- "line-chart": "1",
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((resolve6, reject) => {
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 resolve6();
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((resolve6) => {
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) => resolve6({ code: -1, stderr: String(e), stdout }));
8258
- child.on("close", (code) => resolve6({ code: code ?? -1, stderr, stdout }));
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 cached = await readSidecar(sidecar);
8346
- if (cached) return { audio, file, seconds: cached.seconds, cues: cached.cues };
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;
@@ -9251,7 +9492,7 @@ ${tail3}`);
9251
9492
  }
9252
9493
  }
9253
9494
  function runLive(file, args) {
9254
- return new Promise((resolve6, reject) => {
9495
+ return new Promise((resolve7, reject) => {
9255
9496
  const child = spawn3(file, args, { stdio: ["ignore", "inherit", "inherit"] });
9256
9497
  child.on("error", (err) => {
9257
9498
  reject(
@@ -9259,7 +9500,7 @@ function runLive(file, args) {
9259
9500
  );
9260
9501
  });
9261
9502
  child.on("close", (code, signal) => {
9262
- if (code === 0) resolve6();
9503
+ if (code === 0) resolve7();
9263
9504
  else if (signal) {
9264
9505
  reject(
9265
9506
  new Error(
@@ -11052,9 +11293,57 @@ function textOf(node) {
11052
11293
  }
11053
11294
  }
11054
11295
 
11296
+ // src/tmpdir.ts
11297
+ import { realpathSync } from "node:fs";
11298
+ import { createRequire as createRequire3 } from "node:module";
11299
+ import { tmpdir as tmpdir3 } from "node:os";
11300
+ import { basename as basename4, dirname as dirname4, join as join12, resolve as resolve5, sep } from "node:path";
11301
+ var cached;
11302
+ function real(dir) {
11303
+ let at = resolve5(dir);
11304
+ const tail3 = [];
11305
+ for (; ; ) {
11306
+ try {
11307
+ return join12(realpathSync(at), ...tail3);
11308
+ } catch {
11309
+ const up = dirname4(at);
11310
+ if (up === at) return resolve5(dir);
11311
+ tail3.unshift(basename4(at));
11312
+ at = up;
11313
+ }
11314
+ }
11315
+ }
11316
+ function packageRoot() {
11317
+ cached ??= real(dirname4(createRequire3(import.meta.url).resolve("../package.json")));
11318
+ return cached;
11319
+ }
11320
+ function insideRoot(dir) {
11321
+ const root = packageRoot();
11322
+ const at = real(dir);
11323
+ return at === root || at.startsWith(root + sep);
11324
+ }
11325
+ function guardTmpdir() {
11326
+ const before = tmpdir3();
11327
+ if (!insideRoot(before)) return;
11328
+ delete process.env.TMPDIR;
11329
+ const after = tmpdir3();
11330
+ if (insideRoot(after)) {
11331
+ throw new Error(
11332
+ `tmpdir: ${after} is still inside ${packageRoot()}. Set TMPDIR to a real temp directory.`
11333
+ );
11334
+ }
11335
+ const worker = process.env.VITEST_WORKER_ID;
11336
+ if (worker === void 0 || worker === "1") {
11337
+ process.stderr.write(
11338
+ `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.
11339
+ `
11340
+ );
11341
+ }
11342
+ }
11343
+
11055
11344
  // src/verify/index.ts
11056
11345
  import { readdir as readdir3, readFile as readFile16 } from "node:fs/promises";
11057
- import { join as join15, relative, sep } from "node:path";
11346
+ import { join as join16, relative, sep as sep2 } from "node:path";
11058
11347
 
11059
11348
  // src/verify/budget.ts
11060
11349
  function readCanvas(html) {
@@ -11162,7 +11451,7 @@ function clock2(seconds) {
11162
11451
  // src/verify/check.ts
11163
11452
  import { execFile as execFile2 } from "node:child_process";
11164
11453
  import { readFile as readFile13 } from "node:fs/promises";
11165
- import { join as join12 } from "node:path";
11454
+ import { join as join13 } from "node:path";
11166
11455
  import { promisify as promisify2 } from "node:util";
11167
11456
  var run2 = promisify2(execFile2);
11168
11457
  var DEFAULT_TIMEOUT_MS3 = 24e4;
@@ -11192,7 +11481,7 @@ async function check2(dir, opts = {}) {
11192
11481
  async function readComposition(dir) {
11193
11482
  let html;
11194
11483
  try {
11195
- html = await readFile13(join12(dir, "index.html"), "utf8");
11484
+ html = await readFile13(join13(dir, "index.html"), "utf8");
11196
11485
  } catch {
11197
11486
  return { transit: [], duration: 0 };
11198
11487
  }
@@ -11341,7 +11630,7 @@ function tail(s) {
11341
11630
 
11342
11631
  // src/verify/fidelity.ts
11343
11632
  import { readFile as readFile14 } from "node:fs/promises";
11344
- import { join as join13 } from "node:path";
11633
+ import { join as join14 } from "node:path";
11345
11634
 
11346
11635
  // src/verify/typefloor.ts
11347
11636
  var TYPE_FLOOR_PX = 40;
@@ -11788,8 +12077,8 @@ async function fidelity(dir, opts = {}) {
11788
12077
  elapsedMs: Date.now() - started
11789
12078
  });
11790
12079
  const stops = opts.stops ?? readStops(
11791
- await readFile14(join13(dir, TIMING_FILE), "utf8").catch(() => null),
11792
- await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => null)
12080
+ await readFile14(join14(dir, TIMING_FILE), "utf8").catch(() => null),
12081
+ await readFile14(join14(dir, DECK_PAGE), "utf8").catch(() => null)
11793
12082
  );
11794
12083
  if (stops.length === 0) return notMeasured("the deck declares no stops");
11795
12084
  let deck = null;
@@ -11848,8 +12137,8 @@ async function fidelity(dir, opts = {}) {
11848
12137
  import { execFile as execFile3 } from "node:child_process";
11849
12138
  import { createHash as createHash9 } from "node:crypto";
11850
12139
  import { mkdtemp as mkdtemp3, readdir as readdir2, readFile as readFile15, rm as rm8 } from "node:fs/promises";
11851
- import { tmpdir as tmpdir3 } from "node:os";
11852
- import { join as join14 } from "node:path";
12140
+ import { tmpdir as tmpdir4 } from "node:os";
12141
+ import { join as join15 } from "node:path";
11853
12142
  import { promisify as promisify3 } from "node:util";
11854
12143
  var run3 = promisify3(execFile3);
11855
12144
  var FLOOR_DB = 40;
@@ -11888,9 +12177,9 @@ function measureMotion(hashes, scenes, seconds) {
11888
12177
  async function drift(dir, opts = {}) {
11889
12178
  const mode = opts.mode ?? "psnr";
11890
12179
  const floorDb = opts.floorDb ?? FLOOR_DB;
11891
- const work = opts.workDir ?? await mkdtemp3(join14(tmpdir3(), "decksmith-drift-"));
11892
- const a = join14(work, "a");
11893
- const b = join14(work, "b");
12180
+ const work = opts.workDir ?? await mkdtemp3(join15(tmpdir4(), "decksmith-drift-"));
12181
+ const a = join15(work, "a");
12182
+ const b = join15(work, "b");
11894
12183
  const report2 = await compare(dir, a, b, mode, floorDb, opts);
11895
12184
  const keep = opts.keep || !report2.passed;
11896
12185
  if (keep) report2.kept = { a, b };
@@ -11945,11 +12234,11 @@ async function compare(dir, a, b, mode, floorDb, opts) {
11945
12234
  const differing = [];
11946
12235
  const ha = [];
11947
12236
  for (const [i, name] of fa.entries()) {
11948
- const one = await sha(join14(a, name));
12237
+ const one = await sha(join15(a, name));
11949
12238
  ha.push(one);
11950
- if (one !== await sha(join14(b, name))) differing.push(i + 1);
12239
+ if (one !== await sha(join15(b, name))) differing.push(i + 1);
11951
12240
  }
11952
- const html = await readFile15(join14(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
12241
+ const html = await readFile15(join15(dir, COMPOSITION_PAGE), "utf8").catch(() => "");
11953
12242
  const motion = measureMotion(ha, readScenes(html), readCanvas(html)?.seconds ?? 0);
11954
12243
  let worst;
11955
12244
  if (differing.length > 0) {
@@ -12097,11 +12386,11 @@ async function psnr(a, b, firstFrame) {
12097
12386
  "-start_number",
12098
12387
  String(seq.start),
12099
12388
  "-i",
12100
- join14(a, seq.pattern),
12389
+ join15(a, seq.pattern),
12101
12390
  "-start_number",
12102
12391
  String(seq.start),
12103
12392
  "-i",
12104
- join14(b, seq.pattern),
12393
+ join15(b, seq.pattern),
12105
12394
  "-lavfi",
12106
12395
  "psnr=stats_file=-",
12107
12396
  "-f",
@@ -12176,7 +12465,7 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
12176
12465
  const html = await readCompositions(dir);
12177
12466
  const determinism = html.flatMap(([file, text2]) => scanDeterminism(text2, file));
12178
12467
  const narration = scanNarration(
12179
- await readFile16(join15(dir, DECK_PAGE), "utf8").catch(() => ""),
12468
+ await readFile16(join16(dir, DECK_PAGE), "utf8").catch(() => ""),
12180
12469
  await listFiles(dir)
12181
12470
  );
12182
12471
  const budget3 = html.flatMap(([, text2]) => scanBudget(text2, storyboard, kept));
@@ -12213,8 +12502,8 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
12213
12502
  }
12214
12503
  async function declaredStops(dir) {
12215
12504
  return readStops(
12216
- await readFile16(join15(dir, TIMING_FILE), "utf8").catch(() => null),
12217
- await readFile16(join15(dir, DECK_PAGE), "utf8").catch(() => null)
12505
+ await readFile16(join16(dir, TIMING_FILE), "utf8").catch(() => null),
12506
+ await readFile16(join16(dir, DECK_PAGE), "utf8").catch(() => null)
12218
12507
  );
12219
12508
  }
12220
12509
  var NARRATION_ISLAND = /<script type="application\/decksmith-narration\+json">([\s\S]*?)<\/script>/;
@@ -12254,7 +12543,7 @@ function scanNarration(page, files) {
12254
12543
  ];
12255
12544
  }
12256
12545
  async function readTiming2(dir) {
12257
- const raw2 = await readFile16(join15(dir, TIMING_FILE), "utf8").catch(() => "");
12546
+ const raw2 = await readFile16(join16(dir, TIMING_FILE), "utf8").catch(() => "");
12258
12547
  if (!raw2) return void 0;
12259
12548
  try {
12260
12549
  const parsed = JSON.parse(raw2);
@@ -12266,7 +12555,7 @@ async function readTiming2(dir) {
12266
12555
  async function listFiles(dir) {
12267
12556
  const entries = await readdir3(dir, { recursive: true, withFileTypes: true }).catch(() => []);
12268
12557
  return new Set(
12269
- entries.filter((e) => e.isFile()).map((e) => relative(dir, join15(e.parentPath, e.name)).split(sep).join("/"))
12558
+ entries.filter((e) => e.isFile()).map((e) => relative(dir, join16(e.parentPath, e.name)).split(sep2).join("/"))
12270
12559
  );
12271
12560
  }
12272
12561
  var INSTEAD = {
@@ -12525,13 +12814,14 @@ async function readCompositions(dir) {
12525
12814
  const entries = await readdir3(dir, { recursive: true, withFileTypes: true }).catch(() => []);
12526
12815
  const files = entries.filter(
12527
12816
  (e) => e.isFile() && e.name.endsWith(".html") && e.name !== DECK_PAGE && !e.parentPath.includes("node_modules")
12528
- ).map((e) => join15(e.parentPath, e.name));
12817
+ ).map((e) => join16(e.parentPath, e.name));
12529
12818
  return Promise.all(
12530
12819
  files.map(async (f) => [relative(dir, f), await readFile16(f, "utf8")])
12531
12820
  );
12532
12821
  }
12533
12822
 
12534
12823
  // src/cli.ts
12824
+ guardTmpdir();
12535
12825
  var AUDIO_DIR = "audio";
12536
12826
  var NARRATION_FILE = "narration.json";
12537
12827
  var DEFAULTS = prefsSchema.parse({});
@@ -12544,43 +12834,48 @@ async function deckRuntime() {
12544
12834
  }
12545
12835
  }
12546
12836
  function playerBundle() {
12547
- const require2 = createRequire3(import.meta.url);
12837
+ const require2 = createRequire4(import.meta.url);
12548
12838
  try {
12549
- return join16(dirname4(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
12839
+ return join17(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
12550
12840
  } catch {
12551
12841
  throw new Error('Cannot locate the hyperframes player. Run "npm install".');
12552
12842
  }
12553
12843
  }
12554
12844
  async function vendorKatex(out) {
12555
- const require2 = createRequire3(import.meta.url);
12556
- const dist = join16(dirname4(require2.resolve("katex/package.json")), "dist");
12557
- const css = await readFile17(join16(dist, "katex.min.css"), "utf8");
12558
- await mkdir10(join16(out, "katex/fonts"), { recursive: true });
12559
- for (const file of await readdir4(join16(dist, "fonts"))) {
12845
+ const require2 = createRequire4(import.meta.url);
12846
+ const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
12847
+ const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
12848
+ await mkdir10(join17(out, "katex/fonts"), { recursive: true });
12849
+ for (const file of await readdir4(join17(dist, "fonts"))) {
12560
12850
  if (file.endsWith(".woff2"))
12561
- await cp(join16(dist, "fonts", file), join16(out, "katex/fonts", file));
12851
+ await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
12562
12852
  }
12563
12853
  const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
12564
12854
  const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
12565
12855
  return kept ? `src:${kept}` : whole;
12566
12856
  });
12567
- await writeFile12(join16(out, "katex/katex.min.css"), woff2Only);
12857
+ await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
12568
12858
  }
12569
- async function vendorScripts(out) {
12570
- const require2 = createRequire3(import.meta.url);
12571
- await mkdir10(join16(out, "vendor"), { recursive: true });
12859
+ async function vendorScripts(out, composition) {
12860
+ const require2 = createRequire4(import.meta.url);
12861
+ await mkdir10(join17(out, "vendor"), { recursive: true });
12862
+ const wanted = (name) => composition.includes(`./vendor/${name}`);
12572
12863
  for (const [pkg, rel, name] of [
12573
12864
  ["gsap/package.json", "dist/gsap.min.js", "gsap.min.js"],
12574
12865
  ["gsap/package.json", "dist/DrawSVGPlugin.min.js", "DrawSVGPlugin.min.js"],
12866
+ ["gsap/package.json", "dist/MorphSVGPlugin.min.js", "MorphSVGPlugin.min.js"],
12575
12867
  ["katex/package.json", "dist/katex.min.js", "katex.min.js"]
12576
12868
  ]) {
12577
- const from = join16(dirname4(require2.resolve(pkg)), rel);
12578
- await cp(from, join16(out, "vendor", name));
12869
+ if (!wanted(name)) continue;
12870
+ const from = join17(dirname5(require2.resolve(pkg)), rel);
12871
+ await cp(from, join17(out, "vendor", name));
12872
+ }
12873
+ if (wanted("ds-morph.js")) {
12874
+ await cp(
12875
+ fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
12876
+ join17(out, "vendor", "ds-morph.js")
12877
+ );
12579
12878
  }
12580
- await cp(
12581
- fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
12582
- join16(out, "vendor", "ds-morph.js")
12583
- );
12584
12879
  }
12585
12880
  var HYPERFRAMES_JSON = `${JSON.stringify(
12586
12881
  {
@@ -12683,13 +12978,13 @@ program.command("ingest").description("Parse a document or a web page into sourc
12683
12978
  "--max-clip-seconds <n>",
12684
12979
  "URL only: seconds of each clip kept, the rest trimmed (maxClipSeconds; default 60)"
12685
12980
  ).option("--no-transcode", "URL only: ship each clip as the page served it, unshrunk").action(async (input, o) => {
12686
- const assets = join16(dirname4(resolve5(o.out)), "assets");
12687
- const scratch = url(input) ? await mkdtemp4(join16(tmpdir4(), "decksmith-harvest-")) : void 0;
12981
+ const assets = join17(dirname5(resolve6(o.out)), "assets");
12982
+ const scratch = url(input) ? await mkdtemp4(join17(tmpdir5(), "decksmith-harvest-")) : void 0;
12688
12983
  try {
12689
12984
  let md;
12690
12985
  let clips = [];
12691
12986
  if (scratch === void 0) {
12692
- md = await readFile17(resolve5(input), "utf8").catch(() => {
12987
+ md = await readFile17(resolve6(input), "utf8").catch(() => {
12693
12988
  throw new Error(`Cannot read ${input}.`);
12694
12989
  });
12695
12990
  } else {
@@ -12710,7 +13005,7 @@ program.command("ingest").description("Parse a document or a web page into sourc
12710
13005
  const dropped = [];
12711
13006
  const source = await fetchFigures(withClips, assets, dropped);
12712
13007
  for (const why3 of dropped) step(`ingest: ${why3}`);
12713
- const bundle = await bundleFont(source.lang, glyphs(source), join16(assets, "fonts"));
13008
+ const bundle = await bundleFont(source.lang, glyphs(source), join17(assets, "fonts"));
12714
13009
  if (bundle) step(`ingest: bundled ${bundle.family} for ${source.lang}`);
12715
13010
  await writeJson(o.out, source);
12716
13011
  step(`ingest: wrote ${o.out}`);
@@ -12771,7 +13066,7 @@ imageFlags(
12771
13066
  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
13067
  ).action(async (sbPath, o) => {
12773
13068
  const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
12774
- const sourcePath = resolve5(String(o.source));
13069
+ const sourcePath = resolve6(String(o.source));
12775
13070
  const source = await readValidated(sourcePath, sourceSchema, "source");
12776
13071
  assertRefsResolve(storyboard, source, { pending: "allow" });
12777
13072
  const prefs = await loadPrefs(prefsFromFlags({ ...flags(o), images: true }));
@@ -12785,7 +13080,7 @@ imageFlags(
12785
13080
  );
12786
13081
  const drawn = await illustrate(storyboard, source, {
12787
13082
  prefs,
12788
- assetsDir: join16(dirname4(sourcePath), "assets"),
13083
+ assetsDir: join17(dirname5(sourcePath), "assets"),
12789
13084
  onStep: step
12790
13085
  });
12791
13086
  for (const p of drawn.illustrated) {
@@ -12795,9 +13090,9 @@ imageFlags(
12795
13090
  }
12796
13091
  await writeJson(sourcePath, drawn.source);
12797
13092
  await writeJson(sbPath, drawn.storyboard);
12798
- const cached = drawn.illustrated.filter((p) => p.cached).length;
13093
+ const cached2 = drawn.illustrated.filter((p) => p.cached).length;
12799
13094
  step(
12800
- `illustrate: ${drawn.illustrated.length} picture(s)${cached ? `, ${cached} cached` : ""} \u2192 ${o.source}, ${sbPath}`
13095
+ `illustrate: ${drawn.illustrated.length} picture(s)${cached2 ? `, ${cached2} cached` : ""} \u2192 ${o.source}, ${sbPath}`
12801
13096
  );
12802
13097
  });
12803
13098
  voiceFlags(
@@ -12810,18 +13105,18 @@ voiceFlags(
12810
13105
  const format = pickFormat(String(o.format), o.width, o.height);
12811
13106
  const chosen = await loadPrefs(prefsFromFlags(flags(o)));
12812
13107
  const prefs = stated(chosen, "lang") ? chosen : { ...chosen, lang: storyboard.lang };
12813
- const dir = resolve5(String(o.out));
13108
+ const dir = resolve6(String(o.out));
12814
13109
  const speaking = storyboard.beats.filter((b) => b.narration?.trim()).length;
12815
13110
  if (speaking === 0) {
12816
13111
  throw new Error(`No beat in ${sbPath} has a "narration" field, so there is nothing to speak.`);
12817
13112
  }
12818
13113
  step(`narrate: ${speaking} of ${storyboard.beats.length} beats have narration`);
12819
13114
  const narration = await narrate(storyboard, source, prefs, { dir, format });
12820
- await writeJson(join16(dir, NARRATION_FILE), narration);
13115
+ await writeJson(join17(dir, NARRATION_FILE), narration);
12821
13116
  const segments2 = Object.values(narration.beats).flat();
12822
13117
  const seconds = segments2.reduce((sum, s) => sum + s.seconds, 0);
12823
13118
  step(
12824
- `narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${join16(dir, NARRATION_FILE)}`
13119
+ `narrate: ${segments2.length} segments, ${seconds.toFixed(1)}s in ${narration.voice} \u2192 ${join17(dir, NARRATION_FILE)}`
12825
13120
  );
12826
13121
  });
12827
13122
  lookFlags(
@@ -12836,7 +13131,7 @@ lookFlags(
12836
13131
  const format = withMinWeight(pickFormat(o.format, o.width, o.height), o.minWeight);
12837
13132
  const prefs = await loadPrefs(prefsFromFlags(flags(o)), process.cwd(), source);
12838
13133
  const theme = stated(prefs, "theme") ?? storyboard.theme;
12839
- const out = resolve5(o.out);
13134
+ const out = resolve6(o.out);
12840
13135
  await mkdir10(out, { recursive: true });
12841
13136
  const found = await findNarration(sbPath, o.narration);
12842
13137
  const narration = found ? await loadNarration(found) : void 0;
@@ -12861,10 +13156,14 @@ lookFlags(
12861
13156
  // takes eleven good ones with it at the last stage. Printed, not swallowed:
12862
13157
  // a missing slide the terminal never mentioned is the failure this project
12863
13158
  // keeps finding by eye.
12864
- onBeatError: (id2, err) => step(`build: left out ${id2} \u2014 ${err.message}`)
13159
+ onBeatError: (id2, err) => step(`build: left out ${id2} \u2014 ${err.message}`),
13160
+ // And the slides that ARE here but not as they were planned. A beat drawn
13161
+ // with one of its parts dropped looks finished, so this line is the only
13162
+ // place anyone learns it is not.
13163
+ onBeatWarning: (id2, warning) => step(`build: kept ${id2} \u2014 ${warning}`)
12865
13164
  });
12866
- await writeFile12(join16(out, "index.html"), deck.composition);
12867
- await writeFile12(join16(out, "hyperframes.json"), HYPERFRAMES_JSON);
13165
+ await writeFile12(join17(out, "index.html"), deck.composition);
13166
+ await writeFile12(join17(out, "hyperframes.json"), HYPERFRAMES_JSON);
12868
13167
  await writeTiming(out, {
12869
13168
  storyboard,
12870
13169
  source,
@@ -12880,36 +13179,36 @@ lookFlags(
12880
13179
  ...narration ? { narration } : {}
12881
13180
  });
12882
13181
  if (deck.page) {
12883
- await writeFile12(join16(out, DECK_PAGE), deck.page);
12884
- await cp(playerBundle(), join16(out, PLAYER_FILE));
13182
+ await writeFile12(join17(out, DECK_PAGE), deck.page);
13183
+ await cp(playerBundle(), join17(out, PLAYER_FILE));
12885
13184
  }
12886
13185
  await vendorKatex(out);
12887
- await vendorScripts(out);
12888
- await copyAssets(dirname4(resolve5(o.source)), out, source.figures);
12889
- if (found && narration) await copyAudio(dirname4(found), narration, out);
13186
+ await vendorScripts(out, deck.composition);
13187
+ await copyAssets(dirname5(resolve6(o.source)), out, source.figures);
13188
+ if (found && narration) await copyAudio(dirname5(found), narration, out);
12890
13189
  const look = [theme, paced.speed === 1 ? "" : `${paced.speed}\xD7 speed`].filter(Boolean).join(", ");
12891
13190
  const { cut } = deck;
12892
13191
  const floor = cut.dropped.filter((d) => d.rule === "below_min_weight").length;
12893
13192
  const of = cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
12894
13193
  step(
12895
- `build: ${cut.kept.length}${of} beats at ${format.width}\xD7${format.height} in ${look}${floor > 0 ? ` (${floor} below minWeight ${format.minWeight})` : ""} \u2192 ${join16(out, "index.html")}`
13194
+ `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
13195
  );
12897
13196
  for (const f of scanPaperArc({ ...storyboard, beats: cut.kept }, prefs))
12898
13197
  step(`build: ${f.message}`);
12899
13198
  reportCut(cut);
12900
- if (deck.page) step(`build: navigable deck \u2192 ${join16(out, DECK_PAGE)}`);
13199
+ if (deck.page) step(`build: navigable deck \u2192 ${join17(out, DECK_PAGE)}`);
12901
13200
  await gate(out, false, storyboard, cut.kept, o.fidelity !== false, source);
12902
13201
  }
12903
13202
  );
12904
13203
  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(resolve5(dir), o.snapshots, void 0, void 0, o.fidelity !== false);
13204
+ await gate(resolve6(dir), o.snapshots, void 0, void 0, o.fidelity !== false);
12906
13205
  });
12907
13206
  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
13207
  "--at <seconds...>",
12909
13208
  "absolute times to photograph; defaults to every hold the deck declares"
12910
13209
  ).option("--out <dir>", "where to write the PNGs (default: <dir>/frames)").action(async (dir, o) => {
12911
- const deck = resolve5(dir);
12912
- const out = resolve5(o.out ?? join16(deck, "frames"));
13210
+ const deck = resolve6(dir);
13211
+ const out = resolve6(o.out ?? join17(deck, "frames"));
12913
13212
  let times;
12914
13213
  if (o.at?.length) {
12915
13214
  times = o.at.map((raw2) => {
@@ -12919,8 +13218,8 @@ program.command("frames").description("Write PNGs of a built deck through the ca
12919
13218
  });
12920
13219
  } else {
12921
13220
  times = readStops(
12922
- await readFile17(join16(deck, TIMING_FILE), "utf8").catch(() => null),
12923
- await readFile17(join16(deck, DECK_PAGE), "utf8").catch(() => null)
13221
+ await readFile17(join17(deck, TIMING_FILE), "utf8").catch(() => null),
13222
+ await readFile17(join17(deck, DECK_PAGE), "utf8").catch(() => null)
12924
13223
  ).map((s) => s.t);
12925
13224
  if (times.length === 0)
12926
13225
  throw new Error(
@@ -12934,7 +13233,7 @@ program.command("frames").description("Write PNGs of a built deck through the ca
12934
13233
  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
13234
  async (dir, o) => {
12936
13235
  step("drift: rendering twice, several minutes a render");
12937
- const verdict = await drift(resolve5(dir), {
13236
+ const verdict = await drift(resolve6(dir), {
12938
13237
  mode: o.identical ? "identical" : "psnr",
12939
13238
  floorDb: Number(o.floor),
12940
13239
  ...o.keep ? { keep: true } : {},
@@ -12961,8 +13260,8 @@ program.command("render").description("Render a built deck to a finished video:
12961
13260
  throw new Error(`Unknown --subtitles "${o.subtitles}". Use sidecar, burn or none.`);
12962
13261
  }
12963
13262
  const result = await render({
12964
- deck: resolve5(dir),
12965
- out: resolve5(o.out),
13263
+ deck: resolve6(dir),
13264
+ out: resolve6(o.out),
12966
13265
  subtitles: o.subtitles,
12967
13266
  protocolTimeoutMs: Number(o.protocolTimeout),
12968
13267
  log: step,
@@ -12990,7 +13289,7 @@ voiceFlags(
12990
13289
  )
12991
13290
  ).action(async (sbPath, o) => {
12992
13291
  const storyboard = await readValidated(sbPath, storyboardSchema, "storyboard");
12993
- const sourcePath = resolve5(String(o.source));
13292
+ const sourcePath = resolve6(String(o.source));
12994
13293
  const source = await readValidated(sourcePath, sourceSchema, "source");
12995
13294
  assertRefsResolve(storyboard, source);
12996
13295
  if (o.bake && o.link) throw new Error("Choose --bake or --link, not both.");
@@ -13005,7 +13304,7 @@ voiceFlags(
13005
13304
  const prefer = o.link ? "link" : "bake";
13006
13305
  const plan = await planMedia(figureAssets(source, sourcePath, prefer));
13007
13306
  const files = { ...plan.files };
13008
- if (found && narration) Object.assign(files, await audioFiles(dirname4(found), narration));
13307
+ if (found && narration) Object.assign(files, await audioFiles(dirname5(found), narration));
13009
13308
  const pack2 = {
13010
13309
  version: PACK_VERSION,
13011
13310
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -13020,7 +13319,7 @@ voiceFlags(
13020
13319
  ...narration ? { narration } : {},
13021
13320
  media: plan.media
13022
13321
  };
13023
- const out = resolve5(String(o.out));
13322
+ const out = resolve6(String(o.out));
13024
13323
  const bytes = await writePack(pack2, files, out);
13025
13324
  if (plan.demoted.length) {
13026
13325
  step(`pack: kept as links, not bakeable \u2014 ${plan.demoted.join(", ")}`);
@@ -13034,19 +13333,19 @@ voiceFlags(
13034
13333
  step(`pack: ${size2(bytes)} \u2192 ${out}`);
13035
13334
  });
13036
13335
  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(resolve5(file));
13038
- const out = resolve5(o.out);
13039
- await writeJson(join16(out, "source.json"), pack2.source);
13040
- await writeJson(join16(out, "storyboard.json"), pack2.storyboard);
13041
- await writeJson(join16(out, "decksmith.config.json"), pack2.prefs);
13042
- if (pack2.narration) await writeJson(join16(out, AUDIO_DIR, NARRATION_FILE), pack2.narration);
13336
+ const { pack: pack2, files } = await readPack(resolve6(file));
13337
+ const out = resolve6(o.out);
13338
+ await writeJson(join17(out, "source.json"), pack2.source);
13339
+ await writeJson(join17(out, "storyboard.json"), pack2.storyboard);
13340
+ await writeJson(join17(out, "decksmith.config.json"), pack2.prefs);
13341
+ if (pack2.narration) await writeJson(join17(out, AUDIO_DIR, NARRATION_FILE), pack2.narration);
13043
13342
  const figures = new Map(pack2.source.figures.map((f) => [f.id, f.src]));
13044
13343
  for (const [path2, bytes] of Object.entries(files)) {
13045
13344
  const media = pack2.media.find((m) => m.path === path2);
13046
13345
  const src = media && figures.get(media.id);
13047
- const to = src ? join16("assets", src) : path2;
13048
- await mkdir10(dirname4(join16(out, to)), { recursive: true });
13049
- await writeFile12(join16(out, to), bytes);
13346
+ const to = src ? join17("assets", src) : path2;
13347
+ await mkdir10(dirname5(join17(out, to)), { recursive: true });
13348
+ await writeFile12(join17(out, to), bytes);
13050
13349
  }
13051
13350
  const linked = pack2.media.filter((m) => m.policy !== "bake");
13052
13351
  step(`unpack: "${pack2.title}", ${pack2.storyboard.beats.length} beats \u2192 ${out}`);
@@ -13056,7 +13355,7 @@ program.command("unpack").description("Open a .deck back into the files build re
13056
13355
  );
13057
13356
  }
13058
13357
  step(
13059
- `unpack: decksmith build ${join16(relative2(process.cwd(), out) || ".", "storyboard.json")} --source ${join16(relative2(process.cwd(), out) || ".", "source.json")} -o deck`
13358
+ `unpack: decksmith build ${join17(relative2(process.cwd(), out) || ".", "storyboard.json")} --source ${join17(relative2(process.cwd(), out) || ".", "source.json")} -o deck`
13060
13359
  );
13061
13360
  });
13062
13361
  program.parseAsync(process.argv).catch((err) => {
@@ -13077,7 +13376,7 @@ async function gate(dir, snapshots = false, storyboard, kept, fidelity2 = true,
13077
13376
  );
13078
13377
  const verdict = await verify(dir, { snapshots, fidelity: fidelity2 }, storyboard, kept, source);
13079
13378
  process.stdout.write(report(verdict));
13080
- if (snapshots) step(`verify: snapshots in ${join16(dir, "snapshots")}`);
13379
+ if (snapshots) step(`verify: snapshots in ${join17(dir, "snapshots")}`);
13081
13380
  if (!verdict.passed) process.exitCode = 1;
13082
13381
  }
13083
13382
  function report(v) {
@@ -13094,12 +13393,12 @@ function report(v) {
13094
13393
  async function findNarration(sbPath, flag) {
13095
13394
  if (flag === false) return void 0;
13096
13395
  if (typeof flag === "string") {
13097
- const path2 = resolve5(flag);
13396
+ const path2 = resolve6(flag);
13098
13397
  if (!await stat2(path2).catch(() => null)) throw new Error(`Cannot read narration ${flag}.`);
13099
13398
  return path2;
13100
13399
  }
13101
- const beside = dirname4(resolve5(sbPath));
13102
- for (const candidate of [join16(beside, AUDIO_DIR, NARRATION_FILE), join16(beside, NARRATION_FILE)]) {
13400
+ const beside = dirname5(resolve6(sbPath));
13401
+ for (const candidate of [join17(beside, AUDIO_DIR, NARRATION_FILE), join17(beside, NARRATION_FILE)]) {
13103
13402
  if (await stat2(candidate).catch(() => null)) {
13104
13403
  step(`narration: using ${candidate}`);
13105
13404
  return candidate;
@@ -13119,11 +13418,11 @@ function audioNames(narration) {
13119
13418
  ].sort();
13120
13419
  }
13121
13420
  async function copyAudio(from, narration, out) {
13122
- const dir = join16(out, AUDIO_DIR);
13421
+ const dir = join17(out, AUDIO_DIR);
13123
13422
  await mkdir10(dir, { recursive: true });
13124
13423
  const names = audioNames(narration);
13125
13424
  for (const name of names) {
13126
- await cp(join16(from, name), join16(dir, name)).catch(() => {
13425
+ await cp(join17(from, name), join17(dir, name)).catch(() => {
13127
13426
  throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
13128
13427
  });
13129
13428
  }
@@ -13132,7 +13431,7 @@ async function copyAudio(from, narration, out) {
13132
13431
  async function audioFiles(from, narration) {
13133
13432
  const files = {};
13134
13433
  for (const name of audioNames(narration)) {
13135
- const bytes = await readFile17(join16(from, name)).catch(() => {
13434
+ const bytes = await readFile17(join17(from, name)).catch(() => {
13136
13435
  throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
13137
13436
  });
13138
13437
  files[`${AUDIO_DIR}/${name}`] = new Uint8Array(bytes);
@@ -13154,9 +13453,9 @@ function wrapScript(text2, width) {
13154
13453
  async function writeTiming(out, input) {
13155
13454
  try {
13156
13455
  const timing = planTiming(input);
13157
- await writeJson(join16(out, TIMING_FILE), timing);
13456
+ await writeJson(join17(out, TIMING_FILE), timing);
13158
13457
  step(
13159
- `build: timing for ${timing.segments.length} narration segment(s) \u2192 ${join16(out, TIMING_FILE)}`
13458
+ `build: timing for ${timing.segments.length} narration segment(s) \u2192 ${join17(out, TIMING_FILE)}`
13160
13459
  );
13161
13460
  } catch (err) {
13162
13461
  step(
@@ -13165,10 +13464,10 @@ async function writeTiming(out, input) {
13165
13464
  }
13166
13465
  }
13167
13466
  function figureAssets(source, sourcePath, prefer) {
13168
- const assets = join16(dirname4(sourcePath), "assets");
13467
+ const assets = join17(dirname5(sourcePath), "assets");
13169
13468
  return source.figures.map((f) => ({
13170
13469
  id: f.id,
13171
- url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src : join16(assets, f.src),
13470
+ url: /^[a-z][a-z0-9+.-]*:/i.test(f.src) ? f.src : join17(assets, f.src),
13172
13471
  prefer
13173
13472
  }));
13174
13473
  }
@@ -13197,7 +13496,7 @@ function withMinWeight(format, raw2) {
13197
13496
  return { ...format, minWeight };
13198
13497
  }
13199
13498
  async function copyAssets(sourceDir, out, figures) {
13200
- const from = join16(sourceDir, "assets");
13499
+ const from = join17(sourceDir, "assets");
13201
13500
  if (!await stat2(from).catch(() => null)) {
13202
13501
  step(`build: no assets/ beside source.json, skipping`);
13203
13502
  return;
@@ -13205,19 +13504,19 @@ async function copyAssets(sourceDir, out, figures) {
13205
13504
  const wanted = new Set(
13206
13505
  figures.flatMap((f) => [f.src, f.poster]).filter((n3) => n3 !== void 0).map((n3) => n3.replace(/^\.?\//, ""))
13207
13506
  );
13208
- await mkdir10(join16(out, "assets"), { recursive: true });
13507
+ await mkdir10(join17(out, "assets"), { recursive: true });
13209
13508
  let copied = 0;
13210
13509
  for (const name of wanted) {
13211
- const src = resolve5(join16(from, name));
13212
- if (!src.startsWith(`${resolve5(from)}/`)) continue;
13510
+ const src = resolve6(join17(from, name));
13511
+ if (!src.startsWith(`${resolve6(from)}/`)) continue;
13213
13512
  if (!await stat2(src).catch(() => null)) continue;
13214
- await mkdir10(dirname4(join16(out, "assets", name)), { recursive: true });
13215
- await cp(src, join16(out, "assets", name));
13513
+ await mkdir10(dirname5(join17(out, "assets", name)), { recursive: true });
13514
+ await cp(src, join17(out, "assets", name));
13216
13515
  copied++;
13217
13516
  }
13218
- const fonts = join16(from, "fonts");
13517
+ const fonts = join17(from, "fonts");
13219
13518
  if (await stat2(fonts).catch(() => null)) {
13220
- await cp(fonts, join16(out, "assets", "fonts"), { recursive: true });
13519
+ await cp(fonts, join17(out, "assets", "fonts"), { recursive: true });
13221
13520
  }
13222
13521
  step(`build: copied ${copied} referenced figure(s)`);
13223
13522
  }
@@ -13226,7 +13525,7 @@ async function refreshFont(storyboard, source, out) {
13226
13525
  const bundle = await bundleFont(
13227
13526
  storyboard.lang,
13228
13527
  glyphs(source) + glyphs(storyboard),
13229
- join16(out, "assets", "fonts")
13528
+ join17(out, "assets", "fonts")
13230
13529
  );
13231
13530
  if (bundle) step(`build: font bundle covers ${bundle.family}`);
13232
13531
  return bundle?.css;
@@ -13241,7 +13540,7 @@ function glyphs(value) {
13241
13540
  return JSON.stringify(value);
13242
13541
  }
13243
13542
  async function readValidated(file, schema, label) {
13244
- const text2 = await readFile17(resolve5(file), "utf8").catch(() => {
13543
+ const text2 = await readFile17(resolve6(file), "utf8").catch(() => {
13245
13544
  throw new Error(`Cannot read ${label} file ${file}.`);
13246
13545
  });
13247
13546
  let json;
@@ -13259,8 +13558,8 @@ ${issues}`);
13259
13558
  return parsed.data;
13260
13559
  }
13261
13560
  async function writeJson(file, value) {
13262
- const path2 = resolve5(file);
13263
- await mkdir10(dirname4(path2), { recursive: true });
13561
+ const path2 = resolve6(file);
13562
+ await mkdir10(dirname5(path2), { recursive: true });
13264
13563
  await writeFile12(path2, `${JSON.stringify(value, null, 2)}
13265
13564
  `);
13266
13565
  }