@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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { cp, mkdir as mkdir10, readdir as readdir4, readFile as readFile17, stat as stat2, writeFile as writeFile12 } from "node:fs/promises";
|
|
3
|
-
import { createRequire as
|
|
4
|
-
import { dirname as
|
|
3
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
4
|
+
import { dirname as dirname5, join as join17, resolve as resolve6 } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
// src/pack/media.ts
|
|
@@ -414,15 +414,53 @@ var dataTableParamsSchema = z.object({
|
|
|
414
414
|
path: ["highlight"],
|
|
415
415
|
message: "every highlight must name a row that params.rows draws"
|
|
416
416
|
});
|
|
417
|
+
var chartPointSchema = z.object({ x: z.string(), y: z.number() });
|
|
417
418
|
var lineChartParamsSchema = z.object({
|
|
418
419
|
eyebrow: z.string().optional(),
|
|
419
420
|
headline: z.string(),
|
|
420
421
|
xLabel: z.string(),
|
|
421
422
|
yLabel: z.string(),
|
|
422
|
-
points: z.array(
|
|
423
|
+
points: z.array(chartPointSchema).min(2),
|
|
423
424
|
/** Inter-point annotations, e.g. per-step deltas. One fewer than `points`. */
|
|
424
425
|
deltas: z.array(z.string()).optional(),
|
|
425
|
-
readout: z.string().optional()
|
|
426
|
+
readout: z.string().optional(),
|
|
427
|
+
/**
|
|
428
|
+
* THE SAME MEASUREMENT UNDER A SECOND CONDITION — a baseline the main series
|
|
429
|
+
* is to be read against, where the point is the change in the SHAPE of the
|
|
430
|
+
* curve rather than two numbers.
|
|
431
|
+
*
|
|
432
|
+
* Drawn first and alone; then the curve lifts off it and reshapes into
|
|
433
|
+
* `points`, leaving this one behind as a ghost. `label` names the ghost and
|
|
434
|
+
* is drawn wherever the chart has room for it.
|
|
435
|
+
*
|
|
436
|
+
* KEEP IT SHORT — two or three words. A label wider than the plot is refused
|
|
437
|
+
* outright, and that refusal reaches `onBeatError` and costs the whole beat.
|
|
438
|
+
* One that fits but cannot be placed clear of the axis names, the tick and
|
|
439
|
+
* category labels, the values, the deltas and both curves is DROPPED
|
|
440
|
+
* instead, silently: an unnamed ghost is still legibly the fainter, earlier
|
|
441
|
+
* curve, where a name printed through a number is a defect in both of them.
|
|
442
|
+
* Nothing warns about that one — see the placement note in
|
|
443
|
+
* `src/emit/archetypes/line-chart.ts`.
|
|
444
|
+
*/
|
|
445
|
+
compare: z.object({ label: z.string(), points: z.array(chartPointSchema).min(2) }).optional()
|
|
446
|
+
}).superRefine((p, ctx) => {
|
|
447
|
+
if (!p.compare) return;
|
|
448
|
+
if (p.compare.points.length !== p.points.length) {
|
|
449
|
+
ctx.addIssue({
|
|
450
|
+
code: z.ZodIssueCode.custom,
|
|
451
|
+
path: ["compare", "points"],
|
|
452
|
+
message: `compare has ${p.compare.points.length} points against ${p.points.length}; a comparison is over the same x values`
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const off = p.compare.points.findIndex((c, i) => c.x !== p.points[i]?.x);
|
|
457
|
+
if (off >= 0) {
|
|
458
|
+
ctx.addIssue({
|
|
459
|
+
code: z.ZodIssueCode.custom,
|
|
460
|
+
path: ["compare", "points", off, "x"],
|
|
461
|
+
message: `compare point ${off} is "${p.compare.points[off]?.x}" where points has "${p.points[off]?.x}"`
|
|
462
|
+
});
|
|
463
|
+
}
|
|
426
464
|
});
|
|
427
465
|
var calloutParamsSchema = z.object({
|
|
428
466
|
eyebrow: z.string().optional(),
|
|
@@ -1205,8 +1243,8 @@ async function bundleFont(lang, glyphs, dir) {
|
|
|
1205
1243
|
${text2}`).digest("hex").slice(0, 16)} */`;
|
|
1206
1244
|
await mkdir(dir, { recursive: true });
|
|
1207
1245
|
const cssPath = join(dir, "fonts.css");
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1246
|
+
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1247
|
+
if (cached2.startsWith(stamp)) return { family, css: cached2, files: localNames(cached2) };
|
|
1210
1248
|
const res = await fetch(
|
|
1211
1249
|
`https://fonts.googleapis.com/css2?family=${family.replaceAll(" ", "+")}:wght@400;500;700&text=${encodeURIComponent(text2)}&display=block`,
|
|
1212
1250
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1419,6 +1457,21 @@ function nv(v) {
|
|
|
1419
1457
|
}
|
|
1420
1458
|
var DRAW_FROM = { drawSVG: "0%" };
|
|
1421
1459
|
var DRAW_TO = { drawSVG: "100%" };
|
|
1460
|
+
var SHAPE_INDEX = 0;
|
|
1461
|
+
function reshape(target, to, at, seconds, first) {
|
|
1462
|
+
if (!(seconds > 0)) throw new Error(`reshape ${target}: ${seconds}s is no time to reshape in`);
|
|
1463
|
+
return fromTo(
|
|
1464
|
+
target,
|
|
1465
|
+
{ morphSVG: { shape: target, shapeIndex: SHAPE_INDEX } },
|
|
1466
|
+
{
|
|
1467
|
+
morphSVG: { shape: to, shapeIndex: SHAPE_INDEX },
|
|
1468
|
+
duration: sec(seconds),
|
|
1469
|
+
ease: "power2.inOut",
|
|
1470
|
+
...first ? {} : { immediateRender: false }
|
|
1471
|
+
},
|
|
1472
|
+
sec(at)
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1422
1475
|
function travel(target, route, at, seconds) {
|
|
1423
1476
|
if (!(seconds > 0)) throw new Error(`travel ${target}: ${seconds}s is no time to travel in`);
|
|
1424
1477
|
const legs = [];
|
|
@@ -4100,6 +4153,12 @@ var CHART_GAP = 60;
|
|
|
4100
4153
|
var READOUT_W = 460;
|
|
4101
4154
|
var READOUT_LH = 1.35;
|
|
4102
4155
|
var TALL_ASPECT = 1.15;
|
|
4156
|
+
var RESHAPE_SECONDS = 1.2;
|
|
4157
|
+
var GHOST_OPACITY = 0.28;
|
|
4158
|
+
var DRAW_SECONDS = 1.8;
|
|
4159
|
+
var DRAW_FLOOR = 1;
|
|
4160
|
+
var SEPARATE = 0.5;
|
|
4161
|
+
var STEP_FLOOR = 0.15;
|
|
4103
4162
|
function chartScale(values) {
|
|
4104
4163
|
const lo = Math.min(...values);
|
|
4105
4164
|
const hi = Math.max(...values);
|
|
@@ -4130,7 +4189,8 @@ var lineChart = (beat, ctx) => {
|
|
|
4130
4189
|
const { sid, theme } = ctx;
|
|
4131
4190
|
const p = beat.params;
|
|
4132
4191
|
const face = faceOf(theme.fontStack);
|
|
4133
|
-
const
|
|
4192
|
+
const cmp = p.compare;
|
|
4193
|
+
const scale = chartScale([...p.points, ...cmp?.points ?? []].map((pt) => pt.y));
|
|
4134
4194
|
const box = contentW(ctx.format);
|
|
4135
4195
|
const tall = isPortrait(ctx.format);
|
|
4136
4196
|
const width = tall ? box : box - (p.readout ? CHART_GAP + READOUT_W : 0);
|
|
@@ -4173,11 +4233,15 @@ var lineChart = (beat, ctx) => {
|
|
|
4173
4233
|
const LABEL_SIZE3 = 40;
|
|
4174
4234
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4175
4235
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
4236
|
+
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
4237
|
+
const ghostW = (s) => textWidth(s, LABEL_SIZE3, 600, 0, false, face);
|
|
4176
4238
|
const shownX = fitIndices((i) => catW(p.points[i]?.x ?? ""), false);
|
|
4177
4239
|
const xLabels = p.points.map(
|
|
4178
4240
|
(pt, i) => shownX.has(i) ? `<text x="${n2(x(i))}" y="${H - PAD2.b + 56}">${esc(pt.x)}</text>` : ""
|
|
4179
4241
|
).join("");
|
|
4180
|
-
const
|
|
4242
|
+
const pathOf = (pts) => pts.map((pt, i) => `${i === 0 ? "M" : "L"}${n2(x(i))},${n2(y(pt.y))}`).join(" ");
|
|
4243
|
+
const path2 = pathOf(p.points);
|
|
4244
|
+
const basePath = cmp ? pathOf(cmp.points) : "";
|
|
4181
4245
|
const dots = p.points.map(
|
|
4182
4246
|
(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}" />`
|
|
4183
4247
|
).join("");
|
|
@@ -4213,6 +4277,75 @@ var lineChart = (beat, ctx) => {
|
|
|
4213
4277
|
}).join("");
|
|
4214
4278
|
const first = { x: x(0), y: y(p.points[0]?.y ?? 0) };
|
|
4215
4279
|
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" />` : "";
|
|
4280
|
+
const lineTag = (part, d, extra = "") => `<path class="chartline" id="${sid}-${part}" d="${d}" fill="none" stroke="${theme.accent}"${extra} />`;
|
|
4281
|
+
const chartlines = cmp ? [
|
|
4282
|
+
lineTag("base", basePath),
|
|
4283
|
+
lineTag("line", basePath, ' opacity="0"'),
|
|
4284
|
+
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
4285
|
+
].join("\n ") : lineTag("line", path2);
|
|
4286
|
+
const curveBand = (pts, x0, x1) => {
|
|
4287
|
+
const ys = [];
|
|
4288
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
4289
|
+
for (let i = 0; i < pts.length; i++) {
|
|
4290
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
4291
|
+
}
|
|
4292
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
4293
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
4294
|
+
for (const edge of [x0, x1]) {
|
|
4295
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
4296
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
4301
|
+
};
|
|
4302
|
+
const fixedBoxes = [
|
|
4303
|
+
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
4304
|
+
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
4305
|
+
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
4306
|
+
{ x: 0, y: 42 - LABEL_SIZE3, w: nameW(p.yLabel), h: LABEL_SIZE3 },
|
|
4307
|
+
{
|
|
4308
|
+
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
4309
|
+
y: H - 16 - LABEL_SIZE3,
|
|
4310
|
+
w: nameW(p.xLabel),
|
|
4311
|
+
h: LABEL_SIZE3
|
|
4312
|
+
},
|
|
4313
|
+
...ticks.map((v) => {
|
|
4314
|
+
const w = runW(v.toFixed(scale.decimals));
|
|
4315
|
+
return { x: PAD2.l - 22 - w, y: y(v) + 13 - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4316
|
+
}),
|
|
4317
|
+
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
4318
|
+
];
|
|
4319
|
+
const lastI = p.points.length - 1;
|
|
4320
|
+
const ghost = (() => {
|
|
4321
|
+
if (!cmp) return void 0;
|
|
4322
|
+
const w = ghostW(cmp.label);
|
|
4323
|
+
if (w > plotW) {
|
|
4324
|
+
throw new Error(
|
|
4325
|
+
`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.`
|
|
4326
|
+
);
|
|
4327
|
+
}
|
|
4328
|
+
const atEnd = (i, start) => {
|
|
4329
|
+
const cy = y(cmp.points[i]?.y ?? 0);
|
|
4330
|
+
const away = Math.max(44, cy - 26);
|
|
4331
|
+
const toward = Math.min(cy + 52, H - PAD2.b + 4);
|
|
4332
|
+
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
4333
|
+
return sides.map((by) => ({ i, start, by }));
|
|
4334
|
+
};
|
|
4335
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
4336
|
+
return [...atEnd(lastI, false), ...atEnd(0, true)].find((c) => {
|
|
4337
|
+
const box2 = { x: c.start ? x(c.i) : x(c.i) - w, y: c.by - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
4338
|
+
if (fixedBoxes.some((f) => overlaps(box2, f))) return false;
|
|
4339
|
+
if (valueBoxes.some((v) => overlaps(box2, v))) return false;
|
|
4340
|
+
if (drawnDeltas.some((d) => d !== null && overlaps(box2, d))) return false;
|
|
4341
|
+
return ![p.points, cmp.points].some((series) => {
|
|
4342
|
+
const [top, bottom] = curveBand(series, box2.x, box2.x + box2.w);
|
|
4343
|
+
return Math.min(box2.y + box2.h, bottom) - Math.max(box2.y, top) > 8;
|
|
4344
|
+
});
|
|
4345
|
+
});
|
|
4346
|
+
})();
|
|
4347
|
+
const ghostLabel = cmp && ghost ? `
|
|
4348
|
+
<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>` : "";
|
|
4216
4349
|
const readout = p.readout ? `
|
|
4217
4350
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
4218
4351
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -4225,7 +4358,7 @@ var lineChart = (beat, ctx) => {
|
|
|
4225
4358
|
the svg and the layout gate reports container_overflow. -->
|
|
4226
4359
|
<text class="axname" x="0" y="42">${esc(p.yLabel)}</text>
|
|
4227
4360
|
<text class="axname" x="${n2(PAD2.l + plotW / 2)}" y="${H - 16}" text-anchor="middle">${esc(p.xLabel)}</text>
|
|
4228
|
-
|
|
4361
|
+
${chartlines}${ghostLabel}
|
|
4229
4362
|
<g>${dots}</g>
|
|
4230
4363
|
${ring}
|
|
4231
4364
|
<g class="ptlab" text-anchor="middle">${values}</g>
|
|
@@ -4233,10 +4366,52 @@ var lineChart = (beat, ctx) => {
|
|
|
4233
4366
|
</svg>${readout}
|
|
4234
4367
|
</div>`;
|
|
4235
4368
|
const draw2 = 0.8;
|
|
4236
|
-
const
|
|
4369
|
+
const count = p.points.length;
|
|
4370
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count);
|
|
4371
|
+
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
4372
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count - 1) : 0;
|
|
4373
|
+
const tailAfter = (s) => Math.max(
|
|
4374
|
+
s * count + 0.4,
|
|
4375
|
+
// the ring: its walk, then the 0.4s it takes to leave
|
|
4376
|
+
0.3 + s * (count - 1),
|
|
4377
|
+
// the dots
|
|
4378
|
+
0.5 + s * (count - 1),
|
|
4379
|
+
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
4380
|
+
);
|
|
4381
|
+
const room = beat.seconds - spine;
|
|
4382
|
+
let drawCents = Math.round(DRAW_SECONDS * 100);
|
|
4383
|
+
let stepCents = Math.floor(idealStep * 100);
|
|
4384
|
+
if (cmp) {
|
|
4385
|
+
while (drawCents / 100 + tailAfter(stepCents / 100) > room) {
|
|
4386
|
+
if (drawCents > Math.round(DRAW_FLOOR * 100)) drawCents--;
|
|
4387
|
+
else if (stepCents > Math.round(STEP_FLOOR * 100)) stepCents--;
|
|
4388
|
+
else break;
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
const drawFor = cmp ? drawCents / 100 : DRAW_SECONDS;
|
|
4392
|
+
const step = cmp ? stepCents / 100 : idealStep;
|
|
4393
|
+
const lift2 = sec(draw2 + drawFor);
|
|
4394
|
+
const settled = cmp ? sec(lift2 + SEPARATE + RESHAPE_SECONDS) : draw2;
|
|
4237
4395
|
const tl = [
|
|
4238
4396
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
4239
|
-
tween(
|
|
4397
|
+
tween(
|
|
4398
|
+
`#${sid}-${cmp ? "base" : "line"}`,
|
|
4399
|
+
DRAW_FROM,
|
|
4400
|
+
{ ...DRAW_TO, duration: drawFor, ease: "none" },
|
|
4401
|
+
draw2
|
|
4402
|
+
)
|
|
4403
|
+
];
|
|
4404
|
+
if (cmp) {
|
|
4405
|
+
tl.push(
|
|
4406
|
+
...ghost ? [tween(`#${sid}-ghostlab`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, draw2 + 0.4)] : [],
|
|
4407
|
+
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
4408
|
+
// separating from itself rather than as a second line arriving.
|
|
4409
|
+
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
4410
|
+
tween(`#${sid}-base`, { opacity: 1 }, { opacity: GHOST_OPACITY, duration: SEPARATE }, lift2),
|
|
4411
|
+
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
4412
|
+
);
|
|
4413
|
+
}
|
|
4414
|
+
tl.push(
|
|
4240
4415
|
tween(
|
|
4241
4416
|
`#${sid} .dot`,
|
|
4242
4417
|
// Origin in both halves, or GSAP's smoothOrigin compensates the change with
|
|
@@ -4244,37 +4419,43 @@ var lineChart = (beat, ctx) => {
|
|
|
4244
4419
|
// polyline they mark, inside the frame and so invisible to every gate.
|
|
4245
4420
|
{ opacity: 0, scale: 0, transformOrigin: "center" },
|
|
4246
4421
|
{ opacity: 1, scale: 1, transformOrigin: "center", duration: 0.3, stagger: step },
|
|
4247
|
-
|
|
4422
|
+
settled
|
|
4248
4423
|
),
|
|
4249
|
-
tween(
|
|
4250
|
-
|
|
4424
|
+
tween(
|
|
4425
|
+
`#${sid} .pv`,
|
|
4426
|
+
{ opacity: 0 },
|
|
4427
|
+
{ opacity: 1, duration: 0.3, stagger: step },
|
|
4428
|
+
settled + 0.2
|
|
4429
|
+
)
|
|
4430
|
+
);
|
|
4251
4431
|
if (deltas) {
|
|
4252
4432
|
tl.push(
|
|
4253
4433
|
tween(
|
|
4254
4434
|
`#${sid} .dv`,
|
|
4255
4435
|
{ opacity: 0, y: -10 },
|
|
4256
4436
|
{ opacity: 1, y: 0, duration: 0.35, stagger: step },
|
|
4257
|
-
|
|
4437
|
+
settled + 0.6
|
|
4258
4438
|
)
|
|
4259
4439
|
);
|
|
4260
4440
|
}
|
|
4261
|
-
|
|
4441
|
+
const walk = cmp ? sec(step * count) : DRAW_SECONDS;
|
|
4442
|
+
if (count > 1) {
|
|
4262
4443
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
4263
4444
|
tl.push(
|
|
4264
|
-
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 },
|
|
4265
|
-
...travel(`#${sid}-ring`, route,
|
|
4445
|
+
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
4446
|
+
...travel(`#${sid}-ring`, route, settled, walk),
|
|
4266
4447
|
// And it leaves once the curve is whole: a marker parked on the last
|
|
4267
4448
|
// point for the rest of the beat reads as a defect, not as emphasis.
|
|
4268
4449
|
tween(
|
|
4269
4450
|
`#${sid}-ring`,
|
|
4270
4451
|
{ opacity: 1 },
|
|
4271
4452
|
{ opacity: 0, duration: 0.4, immediateRender: false },
|
|
4272
|
-
|
|
4453
|
+
settled + walk
|
|
4273
4454
|
)
|
|
4274
4455
|
);
|
|
4275
4456
|
}
|
|
4276
|
-
const drawn =
|
|
4277
|
-
const holds = [drawn];
|
|
4457
|
+
const drawn = settled + (cmp ? tailAfter(step) : step * count + 0.4);
|
|
4458
|
+
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
4278
4459
|
if (p.readout) {
|
|
4279
4460
|
tl.push(
|
|
4280
4461
|
// It enters from wherever it sits: from the right when it is beside the
|
|
@@ -4283,9 +4464,22 @@ var lineChart = (beat, ctx) => {
|
|
|
4283
4464
|
);
|
|
4284
4465
|
holds.push(drawn + 0.8);
|
|
4285
4466
|
}
|
|
4467
|
+
const end = holds[holds.length - 1] ?? 0;
|
|
4468
|
+
const need = sec(end + 0.15);
|
|
4469
|
+
if (cmp && need > beat.seconds + 1e-9) {
|
|
4470
|
+
return {
|
|
4471
|
+
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
4472
|
+
warnings: [
|
|
4473
|
+
`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.`
|
|
4474
|
+
]
|
|
4475
|
+
};
|
|
4476
|
+
}
|
|
4286
4477
|
return {
|
|
4287
4478
|
html,
|
|
4288
4479
|
tl,
|
|
4480
|
+
// Named only when a beat actually reshapes, which is what keeps MorphSVG's
|
|
4481
|
+
// 21,195 bytes off every deck that does not. See `PLUGINS` in composition.ts.
|
|
4482
|
+
...cmp ? { plugins: ["morphSVG"] } : {},
|
|
4289
4483
|
holds: holdsWithin(holds, beat.seconds),
|
|
4290
4484
|
css: [
|
|
4291
4485
|
chromeCss(theme),
|
|
@@ -4310,6 +4504,14 @@ var lineChart = (beat, ctx) => {
|
|
|
4310
4504
|
`.axname{font-size:40px;fill:${theme.muted};font-weight:500}`,
|
|
4311
4505
|
`.ptlab{font-size:40px;fill:${theme.fg};font-weight:600}`,
|
|
4312
4506
|
`.delta{font-size:40px;fill:${theme.tones.b};font-weight:600}`,
|
|
4507
|
+
// Only when a ghost was actually named — not merely when there is a ghost,
|
|
4508
|
+
// since the label is dropped where neither side of the baseline is clear.
|
|
4509
|
+
// An unconditional rule would move the stylesheet bytes of every line chart
|
|
4510
|
+
// ever built for a part they do not draw, which is the whole thing
|
|
4511
|
+
// `Scene.plugins` is careful about one level up. `muted`, not `dim`: it
|
|
4512
|
+
// names a series, so it is read, and the curve it names is the thing that
|
|
4513
|
+
// has been faded, not its label.
|
|
4514
|
+
...ghost ? [`.ghostlab{font-size:40px;fill:${theme.muted};font-weight:600}`] : [],
|
|
4313
4515
|
// 1.7 set the two lines of a wrapped readout 68px apart, which reads as two
|
|
4314
4516
|
// unrelated fragments rather than one sentence. 1.35 keeps it a paragraph.
|
|
4315
4517
|
`.readout{font-size:${BODY_SIZE}px;line-height:${READOUT_LH};color:${theme.muted};max-width:${READOUT_W}px}`,
|
|
@@ -5555,7 +5757,10 @@ function round4(n3) {
|
|
|
5555
5757
|
// src/emit/composition.ts
|
|
5556
5758
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5557
5759
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5558
|
-
var
|
|
5760
|
+
var PLUGINS = {
|
|
5761
|
+
dsMorph: { src: "./vendor/ds-morph.js", global: "DSMorphPlugin" },
|
|
5762
|
+
morphSVG: { src: "./vendor/MorphSVGPlugin.min.js", global: "MorphSVGPlugin" }
|
|
5763
|
+
};
|
|
5559
5764
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5560
5765
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5561
5766
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5663,7 +5868,15 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5663
5868
|
}
|
|
5664
5869
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5665
5870
|
if (scene.measure?.length) builds = true;
|
|
5666
|
-
for (const
|
|
5871
|
+
for (const w of cut2.scene.warnings ?? []) opts.onBeatWarning?.(beat.id, w);
|
|
5872
|
+
for (const p of scene.plugins ?? []) {
|
|
5873
|
+
if (!Object.hasOwn(PLUGINS, p)) {
|
|
5874
|
+
throw new Error(
|
|
5875
|
+
`${beat.archetype} ${beat.id}: no vendored plugin named "${p}" \u2014 Scene.plugins takes ${Object.keys(PLUGINS).join(" or ")}`
|
|
5876
|
+
);
|
|
5877
|
+
}
|
|
5878
|
+
plugins.add(p);
|
|
5879
|
+
}
|
|
5667
5880
|
scenes.push(
|
|
5668
5881
|
sceneHtml(
|
|
5669
5882
|
sid,
|
|
@@ -5783,9 +5996,11 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5783
5996
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5784
5997
|
const island = format.navigable ? `
|
|
5785
5998
|
${emitIsland(slides)}` : "";
|
|
5786
|
-
const
|
|
5787
|
-
|
|
5788
|
-
<script
|
|
5999
|
+
const plugins = Object.entries(PLUGINS).filter(([name]) => laid.plugins.has(name)).map(
|
|
6000
|
+
([, p]) => `
|
|
6001
|
+
<script src="${p.src}"></script>
|
|
6002
|
+
<script>gsap.registerPlugin(${p.global});</script>`
|
|
6003
|
+
).join("");
|
|
5789
6004
|
return `<!doctype html>
|
|
5790
6005
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5791
6006
|
<head>
|
|
@@ -5794,7 +6009,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5794
6009
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5795
6010
|
<script src="${GSAP_SRC}"></script>
|
|
5796
6011
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5797
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${
|
|
6012
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${plugins}
|
|
5798
6013
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5799
6014
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5800
6015
|
<style>
|
|
@@ -6669,8 +6884,8 @@ async function send(url, target, headers, signal, opts) {
|
|
|
6669
6884
|
...secure && isIP(host) === 0 ? { servername: host } : {}
|
|
6670
6885
|
};
|
|
6671
6886
|
try {
|
|
6672
|
-
return await new Promise((
|
|
6673
|
-
const req = (secure ? httpsRequest : httpRequest)(options,
|
|
6887
|
+
return await new Promise((resolve7, reject) => {
|
|
6888
|
+
const req = (secure ? httpsRequest : httpRequest)(options, resolve7);
|
|
6674
6889
|
req.on("error", reject);
|
|
6675
6890
|
req.end();
|
|
6676
6891
|
});
|
|
@@ -6758,18 +6973,26 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
6758
6973
|
import { homedir } from "node:os";
|
|
6759
6974
|
import { join as join2 } from "node:path";
|
|
6760
6975
|
import { pathToFileURL } from "node:url";
|
|
6761
|
-
async function
|
|
6762
|
-
const
|
|
6763
|
-
if (
|
|
6976
|
+
async function resolveChrome(need = "open the deck with") {
|
|
6977
|
+
const env = process.env.HYPERFRAMES_BROWSER_PATH || process.env.DECKSMITH_CHROME || process.env.CHROME_PATH;
|
|
6978
|
+
if (env) return { path: env, source: "env" };
|
|
6764
6979
|
const { getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
6765
|
-
const
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6980
|
+
const pick = async (cacheDir) => {
|
|
6981
|
+
const installed2 = await getInstalledBrowsers({ cacheDir }).catch(() => []);
|
|
6982
|
+
const ordered = [...installed2].sort((a, b) => b.buildId.localeCompare(a.buildId, "en"));
|
|
6983
|
+
return ordered.find((b) => b.browser === "chrome-headless-shell") ?? ordered.find((b) => b.browser === "chrome");
|
|
6984
|
+
};
|
|
6985
|
+
const hf = await pick(join2(homedir(), ".cache", "hyperframes", "chrome"));
|
|
6986
|
+
if (hf) return { path: hf.executablePath, source: "hyperframes" };
|
|
6987
|
+
const pup = await pick(process.env.PUPPETEER_CACHE_DIR || join2(homedir(), ".cache", "puppeteer"));
|
|
6988
|
+
if (pup) return { path: pup.executablePath, source: "puppeteer" };
|
|
6769
6989
|
throw new Error(
|
|
6770
6990
|
`no Chrome to ${need} \u2014 run \`npx puppeteer browsers install chrome\`, or set DECKSMITH_CHROME to a Chrome binary.`
|
|
6771
6991
|
);
|
|
6772
6992
|
}
|
|
6993
|
+
async function chromePath(need = "open the deck with") {
|
|
6994
|
+
return (await resolveChrome(need)).path;
|
|
6995
|
+
}
|
|
6773
6996
|
function runtimePath() {
|
|
6774
6997
|
return createRequire2(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
6775
6998
|
}
|
|
@@ -6786,12 +7009,24 @@ async function openDeck(dir, opts = {}) {
|
|
|
6786
7009
|
if (!width || !height) throw new Error("index.html declares no canvas size");
|
|
6787
7010
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
6788
7011
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
7012
|
+
const chrome2 = await resolveChrome();
|
|
6789
7013
|
const browser = await puppeteer.launch({
|
|
6790
|
-
executablePath:
|
|
7014
|
+
executablePath: chrome2.path,
|
|
6791
7015
|
headless: true,
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
7016
|
+
args: [
|
|
7017
|
+
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
7018
|
+
// renderer's.
|
|
7019
|
+
"--force-device-scale-factor=1",
|
|
7020
|
+
"--hide-scrollbars",
|
|
7021
|
+
// WITHOUT THESE A WebGL CANVAS GETS NO CONTEXT AND SCREENSHOTS EMPTY. The
|
|
7022
|
+
// renderer passes its own GL arguments; this path passed none, so it drew
|
|
7023
|
+
// a blank rectangle and reported success. SwiftShader rather than the host
|
|
7024
|
+
// GPU on purpose: it is the one backend available on every machine, and a
|
|
7025
|
+
// gate that quietly follows the host's hardware is measuring the host.
|
|
7026
|
+
"--use-gl=angle",
|
|
7027
|
+
"--use-angle=swiftshader",
|
|
7028
|
+
"--enable-unsafe-swiftshader"
|
|
7029
|
+
]
|
|
6795
7030
|
});
|
|
6796
7031
|
try {
|
|
6797
7032
|
const page = await browser.newPage();
|
|
@@ -6807,6 +7042,18 @@ async function openDeck(dir, opts = {}) {
|
|
|
6807
7042
|
{ timeout }
|
|
6808
7043
|
);
|
|
6809
7044
|
await page.evaluate(() => document.fonts.ready);
|
|
7045
|
+
const canvas = await page.evaluate(() => {
|
|
7046
|
+
const el = document.querySelector("canvas");
|
|
7047
|
+
if (!el) return { present: false, gl: true };
|
|
7048
|
+
const probe2 = document.createElement("canvas");
|
|
7049
|
+
const gl = !!(probe2.getContext("webgl2") ?? probe2.getContext("webgl"));
|
|
7050
|
+
return { present: true, gl };
|
|
7051
|
+
});
|
|
7052
|
+
if (canvas.present && !canvas.gl) {
|
|
7053
|
+
throw new Error(
|
|
7054
|
+
`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.`
|
|
7055
|
+
);
|
|
7056
|
+
}
|
|
6810
7057
|
const cdp = await page.createCDPSession();
|
|
6811
7058
|
return {
|
|
6812
7059
|
width,
|
|
@@ -6839,7 +7086,7 @@ import { z as z2 } from "zod";
|
|
|
6839
7086
|
async function fetchFigures(source, dir, warnings) {
|
|
6840
7087
|
const drops = warnings ?? [];
|
|
6841
7088
|
await mkdir3(dir, { recursive: true });
|
|
6842
|
-
const
|
|
7089
|
+
const cached2 = await readdir(dir).catch(() => []);
|
|
6843
7090
|
const figures = [];
|
|
6844
7091
|
for (const figure of source.figures) {
|
|
6845
7092
|
if (figure.kind === "clip") {
|
|
@@ -6847,7 +7094,7 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6847
7094
|
continue;
|
|
6848
7095
|
}
|
|
6849
7096
|
try {
|
|
6850
|
-
figures.push(figureSchema.parse(await localize(figure, dir,
|
|
7097
|
+
figures.push(figureSchema.parse(await localize(figure, dir, cached2)));
|
|
6851
7098
|
} catch (err) {
|
|
6852
7099
|
drops.push(`figure ${figure.id} was left out: ${reason(err)}`);
|
|
6853
7100
|
}
|
|
@@ -6861,15 +7108,15 @@ async function fetchFigures(source, dir, warnings) {
|
|
|
6861
7108
|
}
|
|
6862
7109
|
return parsed.data;
|
|
6863
7110
|
}
|
|
6864
|
-
async function localize(figure, dir,
|
|
7111
|
+
async function localize(figure, dir, cached2) {
|
|
6865
7112
|
const stem = assetStem(figure.id, figure.src);
|
|
6866
|
-
const hit =
|
|
7113
|
+
const hit = cached2.find((name) => name.startsWith(`${stem}.`));
|
|
6867
7114
|
const bytes = hit ? await readFile4(join3(dir, hit)) : await load(figure.src);
|
|
6868
7115
|
const size2 = imageSize(bytes);
|
|
6869
7116
|
if (hit) return { ...figure, src: hit, ...size2 };
|
|
6870
7117
|
const src = `${stem}${assetExt(bytes, figure.src)}`;
|
|
6871
7118
|
await writeFile3(join3(dir, src), bytes);
|
|
6872
|
-
|
|
7119
|
+
cached2.push(src);
|
|
6873
7120
|
return { ...figure, src, ...size2 };
|
|
6874
7121
|
}
|
|
6875
7122
|
function assetStem(id2, src) {
|
|
@@ -7455,7 +7702,7 @@ ${tail3}`);
|
|
|
7455
7702
|
}
|
|
7456
7703
|
}
|
|
7457
7704
|
function runLive(file, args) {
|
|
7458
|
-
return new Promise((
|
|
7705
|
+
return new Promise((resolve7, reject) => {
|
|
7459
7706
|
const child = spawn(file, args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
7460
7707
|
child.on("error", (err) => {
|
|
7461
7708
|
reject(
|
|
@@ -7463,7 +7710,7 @@ function runLive(file, args) {
|
|
|
7463
7710
|
);
|
|
7464
7711
|
});
|
|
7465
7712
|
child.on("close", (code, signal) => {
|
|
7466
|
-
if (code === 0)
|
|
7713
|
+
if (code === 0) resolve7();
|
|
7467
7714
|
else if (signal) {
|
|
7468
7715
|
reject(
|
|
7469
7716
|
new Error(
|
|
@@ -7790,7 +8037,11 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
7790
8037
|
"--disable-background-networking",
|
|
7791
8038
|
"--disable-extensions",
|
|
7792
8039
|
"--no-default-browser-check",
|
|
7793
|
-
"--no-first-run"
|
|
8040
|
+
"--no-first-run",
|
|
8041
|
+
// The floor under interception — see the note above. Not a hardening
|
|
8042
|
+
// nicety: without it the open-proxy property holds only as fast as CDP
|
|
8043
|
+
// happens to be that run.
|
|
8044
|
+
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
7794
8045
|
]
|
|
7795
8046
|
});
|
|
7796
8047
|
try {
|
|
@@ -8842,7 +9093,10 @@ var REVEALS = {
|
|
|
8842
9093
|
"equation-walk": "one per term",
|
|
8843
9094
|
"equation-morph": "2",
|
|
8844
9095
|
"data-table": "one per highlighted row, plus 1",
|
|
8845
|
-
"
|
|
9096
|
+
// "long enough" is not a hedge: below the emitter's own floor the comparison
|
|
9097
|
+
// is dropped and the chart stops twice becoming a chart that stops once. See
|
|
9098
|
+
// the line-chart entry under THE THIRTEEN ARCHETYPES for what that costs.
|
|
9099
|
+
"line-chart": "1, or 2 when compare is given and the beat is long enough for it",
|
|
8846
9100
|
callout: "one per panel",
|
|
8847
9101
|
pipeline: "one per stage",
|
|
8848
9102
|
"annotated-figure": "one per note, plus 1",
|
|
@@ -8965,6 +9219,29 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
8965
9219
|
scale, over training. Points come from the source's numbers;
|
|
8966
9220
|
deltas, if given, are the steps between consecutive points, so
|
|
8967
9221
|
there is always one fewer.
|
|
9222
|
+
\`compare\` adds a SECOND CONDITION over the same axis \u2014 the tell
|
|
9223
|
+
there is THE SAME QUANTITY MEASURED TWO WAYS, a baseline and the
|
|
9224
|
+
result, where what the source is claiming is the change in the
|
|
9225
|
+
SHAPE of the curve and not two numbers. Its \`label\` names the
|
|
9226
|
+
baseline ("without pretraining", "FP32") and is drawn on the
|
|
9227
|
+
slide where the chart has room for it. Keep it SHORT \u2014 two or
|
|
9228
|
+
three words. A label wider than the plot is refused and the
|
|
9229
|
+
whole slide goes with it, and one that cannot be placed clear of
|
|
9230
|
+
the axis names, the values and both curves is dropped without a
|
|
9231
|
+
word, leaving the baseline unnamed. A \`readout\` takes the plot
|
|
9232
|
+
down to about two thirds of its width, so a beat that has one
|
|
9233
|
+
has room for a shorter label still.
|
|
9234
|
+
\`compare.points\` must carry the SAME x values, in the
|
|
9235
|
+
same order, as \`points\`; the baseline is drawn first, then the
|
|
9236
|
+
curve reshapes into \`points\` and leaves the baseline behind. Do
|
|
9237
|
+
not use it for two unrelated series that happen to share an
|
|
9238
|
+
axis \u2014 nothing reshapes into something it is not a version of.
|
|
9239
|
+
A compare beat also costs TIME \u2014 the baseline draws, is held,
|
|
9240
|
+
then reshapes \u2014 so give it \`seconds\` of 7, or 8 with a
|
|
9241
|
+
\`readout\`. Give it too few and the emitter draws the chart
|
|
9242
|
+
without the comparison rather than stopping on a half-drawn one:
|
|
9243
|
+
the slide is still there, but the point about the baseline is
|
|
9244
|
+
gone.
|
|
8968
9245
|
|
|
8969
9246
|
DESCRIBING ARCHETYPES \u2014 the fallbacks
|
|
8970
9247
|
|
|
@@ -9749,7 +10026,7 @@ function codexCommand(args) {
|
|
|
9749
10026
|
}
|
|
9750
10027
|
function runCodex(args) {
|
|
9751
10028
|
const { argv, env } = codexCommand(args);
|
|
9752
|
-
return new Promise((
|
|
10029
|
+
return new Promise((resolve7, reject) => {
|
|
9753
10030
|
const child = spawn2("codex", argv, {
|
|
9754
10031
|
stdio: ["pipe", "ignore", "pipe"],
|
|
9755
10032
|
...env === void 0 ? {} : { env }
|
|
@@ -9772,7 +10049,7 @@ function runCodex(args) {
|
|
|
9772
10049
|
});
|
|
9773
10050
|
child.on("close", (code) => {
|
|
9774
10051
|
clearTimeout(timer);
|
|
9775
|
-
if (code === 0) return
|
|
10052
|
+
if (code === 0) return resolve7();
|
|
9776
10053
|
reject(
|
|
9777
10054
|
new Error(`codex exec exited ${code}.
|
|
9778
10055
|
${stderr.trim().split("\n").slice(-8).join("\n")}`)
|
|
@@ -10243,7 +10520,7 @@ async function find2(can) {
|
|
|
10243
10520
|
throw new Error(MISSING);
|
|
10244
10521
|
}
|
|
10245
10522
|
function runArgv(cmd, args) {
|
|
10246
|
-
return new Promise((
|
|
10523
|
+
return new Promise((resolve7) => {
|
|
10247
10524
|
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10248
10525
|
let stdout = "";
|
|
10249
10526
|
let stderr = "";
|
|
@@ -10253,8 +10530,8 @@ function runArgv(cmd, args) {
|
|
|
10253
10530
|
child.stderr.on("data", (b) => {
|
|
10254
10531
|
stderr += b.toString();
|
|
10255
10532
|
});
|
|
10256
|
-
child.on("error", (e) =>
|
|
10257
|
-
child.on("close", (code) =>
|
|
10533
|
+
child.on("error", (e) => resolve7({ code: -1, stderr: String(e), stdout }));
|
|
10534
|
+
child.on("close", (code) => resolve7({ code: code ?? -1, stderr, stdout }));
|
|
10258
10535
|
});
|
|
10259
10536
|
}
|
|
10260
10537
|
var edgeTts = {
|
|
@@ -10353,8 +10630,8 @@ async function synthesize(text2, opts) {
|
|
|
10353
10630
|
const file = `${key}.mp3`;
|
|
10354
10631
|
const audio = join8(opts.dir, file);
|
|
10355
10632
|
const sidecar = join8(opts.dir, `${key}.json`);
|
|
10356
|
-
const
|
|
10357
|
-
if (
|
|
10633
|
+
const cached2 = await readSidecar(sidecar);
|
|
10634
|
+
if (cached2) return { audio, file, seconds: cached2.seconds, cues: cached2.cues };
|
|
10358
10635
|
await mkdir6(opts.dir, { recursive: true });
|
|
10359
10636
|
const spoken = await provider.speak({ text: text2, voice: opts.voice, rate, pitch, audio });
|
|
10360
10637
|
const { cues, seconds } = spoken;
|
|
@@ -12489,13 +12766,61 @@ ${issues}`);
|
|
|
12489
12766
|
return parsed.data;
|
|
12490
12767
|
}
|
|
12491
12768
|
|
|
12769
|
+
// src/tmpdir.ts
|
|
12770
|
+
import { realpathSync } from "node:fs";
|
|
12771
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
12772
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
12773
|
+
import { basename as basename4, dirname as dirname4, join as join16, resolve as resolve5, sep as sep2 } from "node:path";
|
|
12774
|
+
var cached;
|
|
12775
|
+
function real(dir) {
|
|
12776
|
+
let at = resolve5(dir);
|
|
12777
|
+
const tail3 = [];
|
|
12778
|
+
for (; ; ) {
|
|
12779
|
+
try {
|
|
12780
|
+
return join16(realpathSync(at), ...tail3);
|
|
12781
|
+
} catch {
|
|
12782
|
+
const up = dirname4(at);
|
|
12783
|
+
if (up === at) return resolve5(dir);
|
|
12784
|
+
tail3.unshift(basename4(at));
|
|
12785
|
+
at = up;
|
|
12786
|
+
}
|
|
12787
|
+
}
|
|
12788
|
+
}
|
|
12789
|
+
function packageRoot() {
|
|
12790
|
+
cached ??= real(dirname4(createRequire3(import.meta.url).resolve("../package.json")));
|
|
12791
|
+
return cached;
|
|
12792
|
+
}
|
|
12793
|
+
function insideRoot(dir) {
|
|
12794
|
+
const root = packageRoot();
|
|
12795
|
+
const at = real(dir);
|
|
12796
|
+
return at === root || at.startsWith(root + sep2);
|
|
12797
|
+
}
|
|
12798
|
+
function guardTmpdir() {
|
|
12799
|
+
const before = tmpdir4();
|
|
12800
|
+
if (!insideRoot(before)) return;
|
|
12801
|
+
delete process.env.TMPDIR;
|
|
12802
|
+
const after = tmpdir4();
|
|
12803
|
+
if (insideRoot(after)) {
|
|
12804
|
+
throw new Error(
|
|
12805
|
+
`tmpdir: ${after} is still inside ${packageRoot()}. Set TMPDIR to a real temp directory.`
|
|
12806
|
+
);
|
|
12807
|
+
}
|
|
12808
|
+
const worker = process.env.VITEST_WORKER_ID;
|
|
12809
|
+
if (worker === void 0 || worker === "1") {
|
|
12810
|
+
process.stderr.write(
|
|
12811
|
+
`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.
|
|
12812
|
+
`
|
|
12813
|
+
);
|
|
12814
|
+
}
|
|
12815
|
+
}
|
|
12816
|
+
|
|
12492
12817
|
// src/index.ts
|
|
12493
12818
|
async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
12494
12819
|
const format = opts.format ?? FORMATS["deck-16x9"];
|
|
12495
12820
|
if (!format) throw new Error("no deck-16x9 format");
|
|
12496
12821
|
const step = opts.onStep ?? (() => {
|
|
12497
12822
|
});
|
|
12498
|
-
const out =
|
|
12823
|
+
const out = resolve6(outDir);
|
|
12499
12824
|
await mkdir10(out, { recursive: true });
|
|
12500
12825
|
const speed = opts.speed ?? 1;
|
|
12501
12826
|
const fontCss = await refreshFont(storyboard, source, out, step);
|
|
@@ -12504,12 +12829,13 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12504
12829
|
...opts.theme ? { theme: opts.theme } : {},
|
|
12505
12830
|
...opts.narration ? { narration: opts.narration } : {},
|
|
12506
12831
|
...opts.onBeatError ? { onBeatError: opts.onBeatError } : {},
|
|
12832
|
+
...opts.onBeatWarning ? { onBeatWarning: opts.onBeatWarning } : {},
|
|
12507
12833
|
...fontCss ? { fontCss } : {}
|
|
12508
12834
|
});
|
|
12509
12835
|
const files = [];
|
|
12510
12836
|
const write = async (name, text2) => {
|
|
12511
|
-
await writeFile12(
|
|
12512
|
-
files.push(
|
|
12837
|
+
await writeFile12(join17(out, name), text2);
|
|
12838
|
+
files.push(join17(out, name));
|
|
12513
12839
|
};
|
|
12514
12840
|
await write("index.html", deck.composition);
|
|
12515
12841
|
await write("hyperframes.json", HYPERFRAMES_JSON);
|
|
@@ -12538,8 +12864,8 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12538
12864
|
}
|
|
12539
12865
|
if (deck.page) {
|
|
12540
12866
|
await write(DECK_PAGE, deck.page);
|
|
12541
|
-
await cp(playerBundle(),
|
|
12542
|
-
files.push(
|
|
12867
|
+
await cp(playerBundle(), join17(out, PLAYER_FILE));
|
|
12868
|
+
files.push(join17(out, PLAYER_FILE));
|
|
12543
12869
|
}
|
|
12544
12870
|
files.push(...await vendorKatex(out));
|
|
12545
12871
|
if (opts.assetsFrom) files.push(...await copyAssets(opts.assetsFrom, out));
|
|
@@ -12548,7 +12874,7 @@ async function buildDeck(storyboard, source, outDir, opts = {}) {
|
|
|
12548
12874
|
}
|
|
12549
12875
|
const of = deck.cut.kept.length === storyboard.beats.length ? "" : ` of ${storyboard.beats.length}`;
|
|
12550
12876
|
step(
|
|
12551
|
-
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${
|
|
12877
|
+
`build: ${deck.cut.kept.length}${of} beats at ${format.width}\xD7${format.height} \u2192 ${join17(out, "index.html")}`
|
|
12552
12878
|
);
|
|
12553
12879
|
for (const d of deck.cut.dropped) step(`build: cut ${d.beat.id} \u2014 ${d.reason}`);
|
|
12554
12880
|
for (const d of deck.cut.dangling) step(`build: check the wording \u2014 ${d.reason}`);
|
|
@@ -12572,40 +12898,40 @@ async function deckRuntime() {
|
|
|
12572
12898
|
}
|
|
12573
12899
|
}
|
|
12574
12900
|
function playerBundle() {
|
|
12575
|
-
const require2 =
|
|
12901
|
+
const require2 = createRequire4(import.meta.url);
|
|
12576
12902
|
try {
|
|
12577
|
-
return
|
|
12903
|
+
return join17(dirname5(require2.resolve("hyperframes/package.json")), "dist", PLAYER_FILE);
|
|
12578
12904
|
} catch {
|
|
12579
12905
|
throw new Error('Cannot locate the hyperframes player. Run "npm install".');
|
|
12580
12906
|
}
|
|
12581
12907
|
}
|
|
12582
12908
|
async function vendorKatex(out) {
|
|
12583
|
-
const require2 =
|
|
12584
|
-
const dist =
|
|
12585
|
-
const css = await readFile17(
|
|
12909
|
+
const require2 = createRequire4(import.meta.url);
|
|
12910
|
+
const dist = join17(dirname5(require2.resolve("katex/package.json")), "dist");
|
|
12911
|
+
const css = await readFile17(join17(dist, "katex.min.css"), "utf8");
|
|
12586
12912
|
const written = [];
|
|
12587
|
-
await mkdir10(
|
|
12588
|
-
for (const file of await readdir4(
|
|
12913
|
+
await mkdir10(join17(out, "katex/fonts"), { recursive: true });
|
|
12914
|
+
for (const file of await readdir4(join17(dist, "fonts"))) {
|
|
12589
12915
|
if (!file.endsWith(".woff2")) continue;
|
|
12590
|
-
await cp(
|
|
12591
|
-
written.push(
|
|
12916
|
+
await cp(join17(dist, "fonts", file), join17(out, "katex/fonts", file));
|
|
12917
|
+
written.push(join17(out, "katex/fonts", file));
|
|
12592
12918
|
}
|
|
12593
12919
|
const woff2Only = css.replace(/src:([^;}]*)/g, (whole, list) => {
|
|
12594
12920
|
const kept = list.split(",").filter((part) => part.includes(".woff2")).join(",");
|
|
12595
12921
|
return kept ? `src:${kept}` : whole;
|
|
12596
12922
|
});
|
|
12597
|
-
await writeFile12(
|
|
12598
|
-
written.push(
|
|
12923
|
+
await writeFile12(join17(out, "katex/katex.min.css"), woff2Only);
|
|
12924
|
+
written.push(join17(out, "katex/katex.min.css"));
|
|
12599
12925
|
return written;
|
|
12600
12926
|
}
|
|
12601
12927
|
async function copyAssets(sourceDir, out) {
|
|
12602
|
-
const from =
|
|
12928
|
+
const from = join17(resolve6(sourceDir), "assets");
|
|
12603
12929
|
if (!await stat2(from).catch(() => null)) return [];
|
|
12604
|
-
await cp(from,
|
|
12605
|
-
return [
|
|
12930
|
+
await cp(from, join17(out, "assets"), { recursive: true });
|
|
12931
|
+
return [join17(out, "assets")];
|
|
12606
12932
|
}
|
|
12607
12933
|
async function copyAudio(from, narration, out) {
|
|
12608
|
-
const dir =
|
|
12934
|
+
const dir = join17(out, narration.dir);
|
|
12609
12935
|
await mkdir10(dir, { recursive: true });
|
|
12610
12936
|
const names = [
|
|
12611
12937
|
...new Set(
|
|
@@ -12613,18 +12939,18 @@ async function copyAudio(from, narration, out) {
|
|
|
12613
12939
|
)
|
|
12614
12940
|
].sort();
|
|
12615
12941
|
for (const name of names) {
|
|
12616
|
-
await cp(
|
|
12942
|
+
await cp(join17(resolve6(from), name), join17(dir, name)).catch(() => {
|
|
12617
12943
|
throw new Error(`Narration names ${name}, but it is not in ${from}. Re-run \`narrate\`.`);
|
|
12618
12944
|
});
|
|
12619
12945
|
}
|
|
12620
|
-
return names.map((n3) =>
|
|
12946
|
+
return names.map((n3) => join17(dir, n3));
|
|
12621
12947
|
}
|
|
12622
12948
|
async function refreshFont(storyboard, source, out, step) {
|
|
12623
12949
|
try {
|
|
12624
12950
|
const bundle = await bundleFont(
|
|
12625
12951
|
storyboard.lang,
|
|
12626
12952
|
JSON.stringify(source) + JSON.stringify(storyboard),
|
|
12627
|
-
|
|
12953
|
+
join17(out, "assets", "fonts")
|
|
12628
12954
|
);
|
|
12629
12955
|
if (bundle) step(`build: font bundle covers ${bundle.family}`);
|
|
12630
12956
|
return bundle?.css;
|
|
@@ -12694,6 +13020,7 @@ export {
|
|
|
12694
13020
|
framePlan,
|
|
12695
13021
|
gradeOverprint,
|
|
12696
13022
|
gridParamsSchema,
|
|
13023
|
+
guardTmpdir,
|
|
12697
13024
|
harvest,
|
|
12698
13025
|
hasIllustrations,
|
|
12699
13026
|
illustrate,
|