@jokerized/decksmith 0.7.0 → 0.8.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/README.md +23 -4
- package/dist/cli.js +599 -179
- package/dist/index.js +593 -176
- package/dist/mcp.js +534 -166
- package/dist/types/emit/archetypes/equation-morph.d.ts +40 -0
- package/dist/types/emit/archetypes/equation-walk.d.ts +60 -1
- package/dist/types/emit/tex.d.ts +39 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/plan/prompt.d.ts +0 -12
- package/dist/types/render/capture.d.ts +24 -0
- package/dist/types/render/timing.d.ts +16 -0
- package/dist/types/verify/budget.d.ts +8 -1
- package/dist/types/verify/fidelity.d.ts +19 -0
- package/dist/types/verify/index.d.ts +17 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -869,35 +869,35 @@ function elbow(sid, from, to, o) {
|
|
|
869
869
|
}
|
|
870
870
|
return path(d, { ...strokeAttrs(o), "marker-end": `url(#${headId(sid, o.stroke)})` });
|
|
871
871
|
}
|
|
872
|
-
function tracks(width,
|
|
873
|
-
const w =
|
|
874
|
-
return Array.from({ length:
|
|
872
|
+
function tracks(width, count2, gap, x0 = 0) {
|
|
873
|
+
const w = count2 > 1 ? (width - gap * (count2 - 1)) / count2 : width;
|
|
874
|
+
return Array.from({ length: count2 }, (_, i) => ({ x: x0 + i * (w + gap), w }));
|
|
875
875
|
}
|
|
876
876
|
function fitBoxes(req) {
|
|
877
|
-
const
|
|
877
|
+
const count2 = req.labels.length;
|
|
878
878
|
const minGap = req.minGap ?? 24;
|
|
879
879
|
const padEm = req.padEm ?? 0.8;
|
|
880
880
|
const weight = req.weight ?? 600;
|
|
881
881
|
const x0 = req.x0 ?? 0;
|
|
882
882
|
const unit = Math.max(1, ...req.labels.map((l) => textWidth(l, 1, weight) + 2 * padEm));
|
|
883
|
-
const sizeAt = (gap) =>
|
|
883
|
+
const sizeAt = (gap) => count2 < 1 ? req.size : (req.width - gap * (count2 - 1)) / count2 / unit;
|
|
884
884
|
if (sizeAt(req.gap) >= req.size) {
|
|
885
|
-
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width,
|
|
885
|
+
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width, count2, req.gap, x0) };
|
|
886
886
|
}
|
|
887
|
-
if (
|
|
888
|
-
const gap = (req.width -
|
|
889
|
-
return { ok: true, size: req.size, gap, boxes: tracks(req.width,
|
|
887
|
+
if (count2 > 1 && sizeAt(minGap) >= req.size) {
|
|
888
|
+
const gap = (req.width - count2 * req.size * unit) / (count2 - 1);
|
|
889
|
+
return { ok: true, size: req.size, gap, boxes: tracks(req.width, count2, gap, x0) };
|
|
890
890
|
}
|
|
891
891
|
const size3 = sizeAt(minGap);
|
|
892
892
|
if (size3 >= MIN_FONT) {
|
|
893
|
-
return { ok: true, size: size3, gap: minGap, boxes: tracks(req.width,
|
|
893
|
+
return { ok: true, size: size3, gap: minGap, boxes: tracks(req.width, count2, minGap, x0) };
|
|
894
894
|
}
|
|
895
895
|
return {
|
|
896
896
|
ok: false,
|
|
897
897
|
size: MIN_FONT,
|
|
898
898
|
gap: minGap,
|
|
899
|
-
boxes: tracks(req.width,
|
|
900
|
-
needed:
|
|
899
|
+
boxes: tracks(req.width, count2, minGap, x0),
|
|
900
|
+
needed: count2 * MIN_FONT * unit + minGap * (count2 - 1)
|
|
901
901
|
};
|
|
902
902
|
}
|
|
903
903
|
|
|
@@ -1230,8 +1230,8 @@ var MID_GAP = 56;
|
|
|
1230
1230
|
var BAND_GAP = 34;
|
|
1231
1231
|
var COL_GAP = 28;
|
|
1232
1232
|
var MIN_COL = 250;
|
|
1233
|
-
function lineBudget(
|
|
1234
|
-
const perSide = Math.max(1, Math.ceil(
|
|
1233
|
+
function lineBudget(count2, stageH) {
|
|
1234
|
+
const perSide = Math.max(1, Math.ceil(count2 / 2));
|
|
1235
1235
|
const room = (stageH - (perSide - 1) * LAB_GAP) / perSide;
|
|
1236
1236
|
const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
|
|
1237
1237
|
return Math.min(LINE_CEIL, Math.max(1, lines));
|
|
@@ -1642,7 +1642,7 @@ var HEAD_GAP = 14;
|
|
|
1642
1642
|
var barCompare = (beat, ctx) => {
|
|
1643
1643
|
const { sid, theme } = ctx;
|
|
1644
1644
|
const p = beat.params;
|
|
1645
|
-
const
|
|
1645
|
+
const count2 = p.bars.length;
|
|
1646
1646
|
const W = contentW(ctx.format);
|
|
1647
1647
|
const face = faceOf(ctx.theme.fontStack);
|
|
1648
1648
|
const tall = isPortrait(ctx.format);
|
|
@@ -1656,7 +1656,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1656
1656
|
0,
|
|
1657
1657
|
face
|
|
1658
1658
|
);
|
|
1659
|
-
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (
|
|
1659
|
+
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count2 + GAP_RATIO * (count2 - 1)));
|
|
1660
1660
|
let pitch = bar * (1 + GAP_RATIO);
|
|
1661
1661
|
const gutterMax = tall ? 0 : share(GUTTER_MAX, W);
|
|
1662
1662
|
const gutterInner = tall ? W : gutterMax - GUTTER_PAD;
|
|
@@ -1678,7 +1678,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1678
1678
|
if (tall) {
|
|
1679
1679
|
bar = Math.min(
|
|
1680
1680
|
BAR_MAX,
|
|
1681
|
-
(avail - unitBand - FOOT -
|
|
1681
|
+
(avail - unitBand - FOOT - count2 * head) / (count2 + GAP_RATIO * (count2 - 1))
|
|
1682
1682
|
);
|
|
1683
1683
|
pitch = head + bar * (1 + GAP_RATIO);
|
|
1684
1684
|
} else {
|
|
@@ -1689,7 +1689,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1689
1689
|
}
|
|
1690
1690
|
}
|
|
1691
1691
|
const valueSize = Math.max(MIN_FONT, Math.min(VALUE_MAX, bar * 0.9));
|
|
1692
|
-
const barsH = (
|
|
1692
|
+
const barsH = (count2 - 1) * pitch + head + bar;
|
|
1693
1693
|
const foot = Math.max(
|
|
1694
1694
|
0,
|
|
1695
1695
|
Math.ceil(
|
|
@@ -1702,7 +1702,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1702
1702
|
const H = barsH + unitBand + foot;
|
|
1703
1703
|
if (H > avail) {
|
|
1704
1704
|
throw new Error(
|
|
1705
|
-
`bar-compare ${beat.id}: ${
|
|
1705
|
+
`bar-compare ${beat.id}: ${count2} bars with labels this long need ${Math.ceil(H)}px of the ${Math.floor(avail)}px this slide has. Shorten the labels or split the beat.`
|
|
1706
1706
|
);
|
|
1707
1707
|
}
|
|
1708
1708
|
const gutter = tall ? 0 : Math.min(
|
|
@@ -1855,7 +1855,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1855
1855
|
</div>${note2}`;
|
|
1856
1856
|
const railsAt = 0.6;
|
|
1857
1857
|
const barsAt = 0.95;
|
|
1858
|
-
const step2 = Math.min(0.4, 2.4 /
|
|
1858
|
+
const step2 = Math.min(0.4, 2.4 / count2);
|
|
1859
1859
|
const grow = 0.85;
|
|
1860
1860
|
const tl = [
|
|
1861
1861
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
@@ -1905,11 +1905,11 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1905
1905
|
);
|
|
1906
1906
|
}
|
|
1907
1907
|
}
|
|
1908
|
-
const settled = barsAt + (
|
|
1908
|
+
const settled = barsAt + (count2 - 1) * step2 + grow + 0.05;
|
|
1909
1909
|
const holds = [settled + 0.2];
|
|
1910
1910
|
const focal = rows.reduce((best, r) => Math.abs(r.value) > Math.abs(best.value) ? r : best);
|
|
1911
1911
|
const spot = spotlighter(sid, ".bc-bar");
|
|
1912
|
-
if (
|
|
1912
|
+
if (count2 > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
|
|
1913
1913
|
const tailAt = settled + 0.3;
|
|
1914
1914
|
if (p.unit) {
|
|
1915
1915
|
tl.push(tween(`#${id(sid, "unit")}`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, tailAt));
|
|
@@ -2402,6 +2402,90 @@ var dataTable = (beat, ctx) => {
|
|
|
2402
2402
|
};
|
|
2403
2403
|
};
|
|
2404
2404
|
|
|
2405
|
+
// src/emit/tex.ts
|
|
2406
|
+
import katex from "katex";
|
|
2407
|
+
var UNFIT_ATTR = "data-ds-unfit";
|
|
2408
|
+
var trust = (c) => c.command === "\\htmlClass";
|
|
2409
|
+
function texError(tex, display) {
|
|
2410
|
+
try {
|
|
2411
|
+
katex.renderToString(tex, {
|
|
2412
|
+
displayMode: display,
|
|
2413
|
+
throwOnError: true,
|
|
2414
|
+
strict: false,
|
|
2415
|
+
trust,
|
|
2416
|
+
output: "html"
|
|
2417
|
+
});
|
|
2418
|
+
return null;
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
return err instanceof Error ? err.message : String(err);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
var LEFT = /\\left(?![a-zA-Z])/g;
|
|
2424
|
+
var RIGHT = /\\right(?![a-zA-Z])/g;
|
|
2425
|
+
var NO_INK = /\\label\{[^{}]*\}|\\nonumber(?![a-zA-Z])|\\notag(?![a-zA-Z])/g;
|
|
2426
|
+
function braceBalance(tex) {
|
|
2427
|
+
let depth = 0;
|
|
2428
|
+
for (let i = 0; i < tex.length; i++) {
|
|
2429
|
+
const c = tex[i];
|
|
2430
|
+
if (c === "\\") {
|
|
2431
|
+
i++;
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (c === "{") depth++;
|
|
2435
|
+
else if (c === "}") depth--;
|
|
2436
|
+
}
|
|
2437
|
+
return depth;
|
|
2438
|
+
}
|
|
2439
|
+
var count = (tex, re) => (tex.match(re) ?? []).length;
|
|
2440
|
+
function repairTex(input, display) {
|
|
2441
|
+
const repairs = [];
|
|
2442
|
+
let tex = input;
|
|
2443
|
+
let error = texError(tex, display);
|
|
2444
|
+
if (error === null) return { tex, repairs, error };
|
|
2445
|
+
const gone = tex.match(NO_INK) ?? [];
|
|
2446
|
+
if (gone.length) {
|
|
2447
|
+
tex = tex.replace(NO_INK, "").trim();
|
|
2448
|
+
repairs.push(`removed ${gone.join(", ")}, which draw nothing`);
|
|
2449
|
+
error = texError(tex, display);
|
|
2450
|
+
}
|
|
2451
|
+
const lefts = count(tex, LEFT);
|
|
2452
|
+
const rights = count(tex, RIGHT);
|
|
2453
|
+
if (error !== null && lefts !== rights) {
|
|
2454
|
+
const n3 = Math.abs(lefts - rights);
|
|
2455
|
+
tex = lefts > rights ? `${tex} ${"\\right.".repeat(n3)}` : `${"\\left.".repeat(n3)} ${tex}`;
|
|
2456
|
+
repairs.push(
|
|
2457
|
+
`${lefts} \\left against ${rights} \\right: added ${n3} invisible ${lefts > rights ? "\\right." : "\\left."} to balance them`
|
|
2458
|
+
);
|
|
2459
|
+
error = texError(tex, display);
|
|
2460
|
+
}
|
|
2461
|
+
const depth = braceBalance(tex);
|
|
2462
|
+
if (error !== null && depth !== 0) {
|
|
2463
|
+
const n3 = Math.abs(depth);
|
|
2464
|
+
tex = depth > 0 ? `${tex}${"}".repeat(n3)}` : `${"{".repeat(n3)}${tex}`;
|
|
2465
|
+
repairs.push(
|
|
2466
|
+
`${n3} unclosed brace group(s): ${depth > 0 ? "closed at the end" : "opened at the start"}`
|
|
2467
|
+
);
|
|
2468
|
+
error = texError(tex, display);
|
|
2469
|
+
}
|
|
2470
|
+
const undefinedNames = [];
|
|
2471
|
+
for (let round7 = 0; error !== null && round7 < 12; round7++) {
|
|
2472
|
+
const m = /Undefined control sequence: \\([a-zA-Z]+)/.exec(error);
|
|
2473
|
+
if (!m) break;
|
|
2474
|
+
const name = m[1];
|
|
2475
|
+
const next = tex.replace(new RegExp(`\\\\${name}(?![a-zA-Z])`, "g"), `\\operatorname{${name}}`);
|
|
2476
|
+
if (next === tex) break;
|
|
2477
|
+
tex = next;
|
|
2478
|
+
undefinedNames.push(`\\${name}`);
|
|
2479
|
+
error = texError(tex, display);
|
|
2480
|
+
}
|
|
2481
|
+
if (undefinedNames.length) {
|
|
2482
|
+
repairs.push(
|
|
2483
|
+
`${undefinedNames.join(", ")} ${undefinedNames.length === 1 ? "is" : "are"} never defined in the source \u2014 drawn as ${undefinedNames.length === 1 ? "its name" : "their names"} in upright type`
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
return { tex, repairs, error };
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2405
2489
|
// src/emit/archetypes/equation-walk.ts
|
|
2406
2490
|
var TRUST = 'function (c) { return c.command === "\\\\htmlClass"; }';
|
|
2407
2491
|
var OPTS = `{ displayMode: true, trust: ${TRUST}, strict: false, output: "html" }`;
|
|
@@ -2416,6 +2500,9 @@ var SYNONYMS = [
|
|
|
2416
2500
|
function normalise(tex) {
|
|
2417
2501
|
let out = "";
|
|
2418
2502
|
const map = [];
|
|
2503
|
+
const ends = [];
|
|
2504
|
+
const lead = [];
|
|
2505
|
+
let hint = null;
|
|
2419
2506
|
for (let i = 0; i < tex.length; ) {
|
|
2420
2507
|
if (/\s/.test(tex[i])) {
|
|
2421
2508
|
i++;
|
|
@@ -2427,19 +2514,19 @@ function normalise(tex) {
|
|
|
2427
2514
|
const m = new RegExp(`^(?:${re.source})`).exec(rest);
|
|
2428
2515
|
return m ? { len: m[0].length, to } : null;
|
|
2429
2516
|
}).find(Boolean);
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
i
|
|
2436
|
-
|
|
2517
|
+
const len = hit ? hit.len : 1;
|
|
2518
|
+
const chars = hit ? hit.to : tex[i];
|
|
2519
|
+
if (chars === "") hint ??= i;
|
|
2520
|
+
for (const ch of chars) {
|
|
2521
|
+
out += ch;
|
|
2522
|
+
map.push(i);
|
|
2523
|
+
ends.push(i + len);
|
|
2524
|
+
lead.push(hint ?? i);
|
|
2525
|
+
hint = null;
|
|
2437
2526
|
}
|
|
2438
|
-
|
|
2439
|
-
map.push(i);
|
|
2440
|
-
i++;
|
|
2527
|
+
i += len;
|
|
2441
2528
|
}
|
|
2442
|
-
return { text: out, map };
|
|
2529
|
+
return { text: out, map, ends, lead };
|
|
2443
2530
|
}
|
|
2444
2531
|
function locate(tex, term) {
|
|
2445
2532
|
const hay = normalise(tex);
|
|
@@ -2447,58 +2534,131 @@ function locate(tex, term) {
|
|
|
2447
2534
|
if (needle === "") return null;
|
|
2448
2535
|
const at = hay.text.indexOf(needle);
|
|
2449
2536
|
if (at < 0) return null;
|
|
2450
|
-
const start = hay.
|
|
2451
|
-
const
|
|
2452
|
-
const lastOrig = hay.map[lastNorm];
|
|
2453
|
-
let end = lastOrig + 1;
|
|
2454
|
-
while (end < tex.length && (hay.map[lastNorm + 1] ?? tex.length) > end && /\s/.test(tex[end])) {
|
|
2455
|
-
end++;
|
|
2456
|
-
}
|
|
2457
|
-
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
2537
|
+
const start = hay.lead[at];
|
|
2538
|
+
const end = hay.ends[at + needle.length - 1];
|
|
2458
2539
|
return { start, end: Math.min(end, tex.length) };
|
|
2459
2540
|
}
|
|
2460
2541
|
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
2542
|
+
const checked = texError(tex, true) === null;
|
|
2461
2543
|
let parts = [{ text: tex, raw: true }];
|
|
2462
2544
|
const used = [];
|
|
2463
2545
|
const missing = [];
|
|
2546
|
+
const broken = [];
|
|
2547
|
+
const joined = (ps) => ps.map((p) => p.text).join("");
|
|
2464
2548
|
for (const term of terms) {
|
|
2465
2549
|
let placed = false;
|
|
2550
|
+
let found = false;
|
|
2466
2551
|
for (let i = 0; i < parts.length && !placed; i++) {
|
|
2467
2552
|
const part = parts[i];
|
|
2468
2553
|
if (!part.raw) continue;
|
|
2469
2554
|
const at = locate(part.text, term.tex);
|
|
2470
2555
|
if (!at) continue;
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
raw:
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2556
|
+
found = true;
|
|
2557
|
+
const body = `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`;
|
|
2558
|
+
for (const text2 of [body, `{${body}}`]) {
|
|
2559
|
+
const next = parts.toSpliced(
|
|
2560
|
+
i,
|
|
2561
|
+
1,
|
|
2562
|
+
{ text: part.text.slice(0, at.start), raw: true },
|
|
2563
|
+
{ text: text2, raw: false },
|
|
2564
|
+
{ text: part.text.slice(at.end), raw: true }
|
|
2565
|
+
);
|
|
2566
|
+
if (checked && texError(joined(next), true) !== null) continue;
|
|
2567
|
+
parts = next;
|
|
2568
|
+
used.push(term);
|
|
2569
|
+
placed = true;
|
|
2570
|
+
break;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
if (!placed) {
|
|
2574
|
+
missing.push(term);
|
|
2575
|
+
if (found) broken.push(term);
|
|
2483
2576
|
}
|
|
2484
|
-
if (!placed) missing.push(term);
|
|
2485
2577
|
}
|
|
2486
2578
|
if (used.length === 0) {
|
|
2487
2579
|
throw new Error(
|
|
2488
|
-
`equation-walk ${beatId}: none of its ${terms.length} term(s) occur in the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(tex)}`
|
|
2580
|
+
`equation-walk ${beatId}: none of its ${terms.length} term(s) ${broken.length ? "can be highlighted in" : "occur in"} the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(tex)}`
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
return { tex: joined(parts), used, missing, broken };
|
|
2584
|
+
}
|
|
2585
|
+
function droppedTerms(eqId, missing, broken) {
|
|
2586
|
+
return missing.map(
|
|
2587
|
+
(t2) => broken.includes(t2) ? `term ${JSON.stringify(t2.tex)} is in ${eqId} but no span around it parses, so it is not highlighted and its legend row ("${t2.label}") is left out` : `term ${JSON.stringify(t2.tex)} does not occur in ${eqId}, so its legend row ("${t2.label}") is left out`
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
function prepareWalk(eq, terms, beatId) {
|
|
2591
|
+
const fixed = repairTex(eq.tex, true);
|
|
2592
|
+
const notes = fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`);
|
|
2593
|
+
const repairedTerms = terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
2594
|
+
if (fixed.error !== null) {
|
|
2595
|
+
notes.push(
|
|
2596
|
+
`${eq.id} does not parse even after repair (${fixed.error}), so it is shown as its TeX source in plain text \u2014 ${JSON.stringify(eq.tex)}`
|
|
2489
2597
|
);
|
|
2598
|
+
const parts = segment(eq.tex, [...terms]);
|
|
2599
|
+
if (parts.used.length === 0) {
|
|
2600
|
+
throw new Error(
|
|
2601
|
+
`equation-walk ${beatId}: none of its ${terms.length} term(s) occur in the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(eq.tex)}`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
notes.push(...droppedTerms(eq.id, parts.missing, []));
|
|
2605
|
+
return {
|
|
2606
|
+
raw: eq.tex,
|
|
2607
|
+
plain: { error: fixed.error },
|
|
2608
|
+
used: parts.used,
|
|
2609
|
+
missing: parts.missing,
|
|
2610
|
+
notes
|
|
2611
|
+
};
|
|
2490
2612
|
}
|
|
2491
|
-
|
|
2613
|
+
const walk = wrapTerms(fixed.tex, repairedTerms, beatId);
|
|
2614
|
+
const back = (ts) => ts.map((t2) => terms[repairedTerms.indexOf(t2)]);
|
|
2615
|
+
const used = back(walk.used);
|
|
2616
|
+
const missing = back(walk.missing);
|
|
2617
|
+
notes.push(...droppedTerms(eq.id, missing, back(walk.broken)));
|
|
2618
|
+
return { tex: walk.tex, raw: fixed.tex, used, missing, notes };
|
|
2619
|
+
}
|
|
2620
|
+
function segment(tex, terms) {
|
|
2621
|
+
let parts = [{ text: tex }];
|
|
2622
|
+
const used = [];
|
|
2623
|
+
const missing = [];
|
|
2624
|
+
for (const term of terms) {
|
|
2625
|
+
const i = parts.findIndex((p) => !p.term && locate(p.text, term.tex));
|
|
2626
|
+
const part = parts[i];
|
|
2627
|
+
const at = part && locate(part.text, term.tex);
|
|
2628
|
+
if (!part || !at) {
|
|
2629
|
+
missing.push(term);
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
parts = parts.toSpliced(
|
|
2633
|
+
i,
|
|
2634
|
+
1,
|
|
2635
|
+
{ text: part.text.slice(0, at.start) },
|
|
2636
|
+
{ text: part.text.slice(at.start, at.end), term },
|
|
2637
|
+
{ text: part.text.slice(at.end) }
|
|
2638
|
+
);
|
|
2639
|
+
used.push(term);
|
|
2640
|
+
}
|
|
2641
|
+
return { parts, used, missing };
|
|
2642
|
+
}
|
|
2643
|
+
function plainHtml(tex, terms) {
|
|
2644
|
+
return segment(tex, terms).parts.map(
|
|
2645
|
+
(p) => p.term ? `<span class="term t-${p.term.tone}">${esc(p.text)}</span>` : esc(p.text)
|
|
2646
|
+
).join("");
|
|
2647
|
+
}
|
|
2648
|
+
function chipSetup(sid, term) {
|
|
2649
|
+
const fixed = repairTex(term.tex, false);
|
|
2650
|
+
const el = `document.getElementById("${sid}-chip-${term.tone}")`;
|
|
2651
|
+
return fixed.error === null ? `katex.render('${js(fixed.tex)}', ${el}, ${INLINE_OPTS});` : `${el}.textContent = '${js(term.tex)}';`;
|
|
2492
2652
|
}
|
|
2493
2653
|
function equationSize(tex) {
|
|
2494
2654
|
return tex.length > 120 ? 68 : tex.length > 90 ? 80 : tex.length > 55 ? 92 : 108;
|
|
2495
2655
|
}
|
|
2496
|
-
var
|
|
2656
|
+
var NO_INK2 = /\\(math[a-z]+|text[a-z]*|bm|boldsymbol|displaystyle|textstyle|scriptstyle|left|right|operatorname|limits|nolimits|htmlClass|hspace|phantom|[,;:!])/g;
|
|
2497
2657
|
var GLYPH_EM = 0.9;
|
|
2498
2658
|
function texUnits(tex) {
|
|
2499
2659
|
let ems = 0;
|
|
2500
2660
|
for (const m of tex.matchAll(/\\qquad|\\quad/g)) ems += m[0] === "\\qquad" ? 2 : 1;
|
|
2501
|
-
const glyphs2 = tex.replace(/\\q?quad/g, "").replace(
|
|
2661
|
+
const glyphs2 = tex.replace(/\\q?quad/g, "").replace(NO_INK2, "").replace(/[{}\s_^]/g, "");
|
|
2502
2662
|
return ems + glyphs2.length * GLYPH_EM;
|
|
2503
2663
|
}
|
|
2504
2664
|
function statements(tex, stacked) {
|
|
@@ -2506,6 +2666,100 @@ function statements(tex, stacked) {
|
|
|
2506
2666
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
2507
2667
|
return parts.length > 0 ? parts : [tex];
|
|
2508
2668
|
}
|
|
2669
|
+
var PLAIN_FONT = 48;
|
|
2670
|
+
function mathFit(sid, targets, shown, raw2, want, floor) {
|
|
2671
|
+
return [
|
|
2672
|
+
`var dsEqFit = (function () {
|
|
2673
|
+
var box = document.getElementById("${sid}-eq");
|
|
2674
|
+
var slide = box && box.parentNode;
|
|
2675
|
+
var bw = box ? box.getBoundingClientRect().width : 0;
|
|
2676
|
+
if (!(bw > 0) || !slide) return "unmeasured";
|
|
2677
|
+
var targets = [${targets.map((id2) => `"${id2}"`).join(", ")}].map(function (id) { return document.getElementById(id); });
|
|
2678
|
+
var TEX = [${shown.map((s) => `'${js(s)}'`).join(", ")}];
|
|
2679
|
+
var spill0 = slide.scrollHeight - slide.clientHeight;
|
|
2680
|
+
function extent(each) {
|
|
2681
|
+
var w = 0;
|
|
2682
|
+
targets.forEach(function (t) {
|
|
2683
|
+
var lo = Infinity, hi = -Infinity;
|
|
2684
|
+
Array.prototype.forEach.call(t.querySelectorAll(".katex-html > .base"), function (b) {
|
|
2685
|
+
var r = b.getBoundingClientRect();
|
|
2686
|
+
if (each) w = Math.max(w, r.width);
|
|
2687
|
+
else { lo = Math.min(lo, r.left); hi = Math.max(hi, r.right); }
|
|
2688
|
+
});
|
|
2689
|
+
if (!each && hi > lo) w = Math.max(w, hi - lo);
|
|
2690
|
+
});
|
|
2691
|
+
return w;
|
|
2692
|
+
}
|
|
2693
|
+
function fits(each) {
|
|
2694
|
+
return extent(each) <= bw + 0.5 && slide.scrollHeight - slide.clientHeight <= Math.max(1, spill0 + 1);
|
|
2695
|
+
}
|
|
2696
|
+
function settle(each, size) {
|
|
2697
|
+
size = Math.max(${floor}, Math.min(${want}, Math.floor(size)));
|
|
2698
|
+
box.style.fontSize = size + "px";
|
|
2699
|
+
while (!fits(each) && size > ${floor}) {
|
|
2700
|
+
size = Math.max(${floor}, size - 2);
|
|
2701
|
+
box.style.fontSize = size + "px";
|
|
2702
|
+
}
|
|
2703
|
+
return fits(each);
|
|
2704
|
+
}
|
|
2705
|
+
if (fits(false)) return "fits";
|
|
2706
|
+
var size = parseFloat(box.style.fontSize);
|
|
2707
|
+
if (settle(false, (size * bw) / extent(false))) return "scaled";
|
|
2708
|
+
targets.forEach(function (t, i) { katex.render("\\\\displaystyle " + TEX[i], t, ${INLINE_OPTS}); });
|
|
2709
|
+
box.className += " eq-broken";
|
|
2710
|
+
box.style.fontSize = "${want}px";
|
|
2711
|
+
var wb = extent(true);
|
|
2712
|
+
if (settle(true, wb > bw ? (${want} * bw) / wb : ${want})) return "broken";
|
|
2713
|
+
box.setAttribute("${UNFIT_ATTR}", '${js(raw2)}');
|
|
2714
|
+
return "unfit";
|
|
2715
|
+
})()`
|
|
2716
|
+
];
|
|
2717
|
+
}
|
|
2718
|
+
function chipFit(sid, terms) {
|
|
2719
|
+
if (terms.length === 0) return [];
|
|
2720
|
+
return [
|
|
2721
|
+
`[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2722
|
+
var chip = document.getElementById("${sid}-chip-" + tone);
|
|
2723
|
+
var r = chip && chip.getBoundingClientRect();
|
|
2724
|
+
if (!r || !(r.height > 0)) return;
|
|
2725
|
+
var k = chip.offsetHeight / r.height;
|
|
2726
|
+
var top = r.top, bottom = r.bottom;
|
|
2727
|
+
Array.prototype.forEach.call(chip.querySelectorAll("span"), function (el) {
|
|
2728
|
+
for (var n = el.firstChild; n; n = n.nextSibling) {
|
|
2729
|
+
if (n.nodeType === 3 && n.nodeValue.trim()) {
|
|
2730
|
+
var q = el.getBoundingClientRect();
|
|
2731
|
+
top = Math.min(top, q.top);
|
|
2732
|
+
bottom = Math.max(bottom, q.bottom);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
var cs = getComputedStyle(chip);
|
|
2738
|
+
if (top < r.top) chip.style.paddingTop = parseFloat(cs.paddingTop) + Math.ceil((r.top - top) * k) + "px";
|
|
2739
|
+
if (bottom > r.bottom) chip.style.paddingBottom = parseFloat(cs.paddingBottom) + Math.ceil((bottom - r.bottom) * k) + "px";
|
|
2740
|
+
})`
|
|
2741
|
+
];
|
|
2742
|
+
}
|
|
2743
|
+
var SWELL = 1.16;
|
|
2744
|
+
var SWELL_EM = 0.06;
|
|
2745
|
+
function swellFit(sid, terms) {
|
|
2746
|
+
return [
|
|
2747
|
+
`var dsSwell = (function () {
|
|
2748
|
+
var out = {};
|
|
2749
|
+
var box = document.getElementById("${sid}-eq");
|
|
2750
|
+
var k = box && box.offsetWidth ? box.getBoundingClientRect().width / box.offsetWidth : 1;
|
|
2751
|
+
var em = box ? parseFloat(getComputedStyle(box).fontSize) : 0;
|
|
2752
|
+
[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2753
|
+
var w = 0;
|
|
2754
|
+
Array.prototype.forEach.call(document.querySelectorAll("#${sid} .t-" + tone), function (el) {
|
|
2755
|
+
w = Math.max(w, el.getBoundingClientRect().width / k);
|
|
2756
|
+
});
|
|
2757
|
+
out[tone] = w > 0 && em > 0 ? Math.min(${SWELL}, Math.round((1 + (2 * ${SWELL_EM} * em) / w) * 1000) / 1000) : ${SWELL};
|
|
2758
|
+
});
|
|
2759
|
+
return out;
|
|
2760
|
+
})()`
|
|
2761
|
+
];
|
|
2762
|
+
}
|
|
2509
2763
|
function legendRows(sid, terms, theme) {
|
|
2510
2764
|
return terms.map(
|
|
2511
2765
|
(t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
|
|
@@ -2521,7 +2775,10 @@ function legendCss(theme) {
|
|
|
2521
2775
|
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
2522
2776
|
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
2523
2777
|
// are one symbol each and their natural widths differ by a few pixels.
|
|
2524
|
-
|
|
2778
|
+
// `flex:none`: beside a long label the chip used to SHRINK below its own
|
|
2779
|
+
// nowrap TeX, which then ran out of the painted box into the label — on a
|
|
2780
|
+
// HypePaper deck, "pixelcoord" 91px past the chip's right edge.
|
|
2781
|
+
`.chip{display:inline-block;flex:none;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
|
|
2525
2782
|
].join("\n");
|
|
2526
2783
|
}
|
|
2527
2784
|
var equationWalk = (beat, ctx) => {
|
|
@@ -2533,37 +2790,53 @@ var equationWalk = (beat, ctx) => {
|
|
|
2533
2790
|
`equation-walk ${beat.id}: no equation "${p.equationId}" in source ${ctx.source.id}`
|
|
2534
2791
|
);
|
|
2535
2792
|
}
|
|
2536
|
-
const walk =
|
|
2793
|
+
const walk = prepareWalk(eq, p.terms, beat.id);
|
|
2537
2794
|
const terms = walk.used;
|
|
2538
2795
|
const legend = legendRows(sid, terms, theme);
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2796
|
+
const chips = terms.map((t2) => chipSetup(sid, t2));
|
|
2797
|
+
let eqHtml;
|
|
2798
|
+
let setup;
|
|
2799
|
+
let measure4;
|
|
2800
|
+
if (walk.plain) {
|
|
2801
|
+
eqHtml = `<div class="eq eq-plain" id="${sid}-eq" style="font-size:${PLAIN_FONT}px">${plainHtml(eq.tex, terms)}</div>`;
|
|
2802
|
+
setup = chips;
|
|
2803
|
+
measure4 = [...chipFit(sid, terms), ...swellFit(sid, terms)];
|
|
2804
|
+
} else {
|
|
2805
|
+
const wrapped = walk.tex;
|
|
2806
|
+
const split = statements(wrapped, isPortrait(ctx.format));
|
|
2807
|
+
const stacked = split.length > 1 && split.every((s) => texError(s, true) === null);
|
|
2808
|
+
const raw2 = statements(walk.raw, stacked);
|
|
2809
|
+
const shown = stacked ? split : [wrapped];
|
|
2810
|
+
const longest = raw2.reduce((a, b) => b.length > a.length ? b : a);
|
|
2811
|
+
const want = equationSize(longest);
|
|
2812
|
+
const size3 = Math.max(
|
|
2813
|
+
MIN_FONT,
|
|
2814
|
+
Math.min(want, Math.floor(contentW(ctx.format) / Math.max(...raw2.map(texUnits))))
|
|
2815
|
+
);
|
|
2816
|
+
const body = shown.length === 1 ? "" : shown.map((_, i) => `<div id="${sid}-eq${i}"></div>`).join("\n ");
|
|
2817
|
+
eqHtml = `<div class="eq${shown.length === 1 ? "" : " eqstack"}" id="${sid}-eq" style="font-size:${size3}px">${body}</div>`;
|
|
2818
|
+
const targets = shown.map((_, i) => `${sid}-eq${shown.length === 1 ? "" : i}`);
|
|
2819
|
+
setup = [
|
|
2820
|
+
`var OPTS = ${OPTS};`,
|
|
2821
|
+
...shown.map(
|
|
2822
|
+
(part, i) => `katex.render('${js(part)}', document.getElementById("${targets[i]}"), OPTS);`
|
|
2823
|
+
),
|
|
2824
|
+
...chips
|
|
2825
|
+
];
|
|
2826
|
+
measure4 = [
|
|
2827
|
+
...chipFit(sid, terms),
|
|
2828
|
+
...mathFit(sid, targets, shown, walk.raw, want, MIN_FONT),
|
|
2829
|
+
// Last: it measures each term at the size the fit settled on.
|
|
2830
|
+
...swellFit(sid, terms)
|
|
2831
|
+
];
|
|
2832
|
+
}
|
|
2551
2833
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
2552
2834
|
<div class="eqslide">
|
|
2553
|
-
|
|
2835
|
+
${eqHtml}
|
|
2554
2836
|
<div class="legend">
|
|
2555
2837
|
${legend}
|
|
2556
2838
|
</div>
|
|
2557
2839
|
</div>`;
|
|
2558
|
-
const setup = [
|
|
2559
|
-
`var OPTS = ${OPTS};`,
|
|
2560
|
-
...shown.map(
|
|
2561
|
-
(part, i) => `katex.render('${js(part)}', document.getElementById("${sid}-eq${shown.length === 1 ? "" : i}"), OPTS);`
|
|
2562
|
-
),
|
|
2563
|
-
...terms.map(
|
|
2564
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2565
|
-
)
|
|
2566
|
-
];
|
|
2567
2840
|
const tl = [
|
|
2568
2841
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
2569
2842
|
tween(`#${sid}-eq`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8)
|
|
@@ -2575,6 +2848,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2575
2848
|
);
|
|
2576
2849
|
const holds = [];
|
|
2577
2850
|
const spot = spotlighter(sid, ".term");
|
|
2851
|
+
const swell = (t2) => raw(`dsSwell.${t2.tone}`);
|
|
2578
2852
|
terms.forEach((term, i) => {
|
|
2579
2853
|
const at = first + i * step2;
|
|
2580
2854
|
const colour = theme.tones[term.tone];
|
|
@@ -2591,13 +2865,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2591
2865
|
tween(
|
|
2592
2866
|
`#${sid} .t-${term.tone}`,
|
|
2593
2867
|
{ color: theme.fg, scale: 1 },
|
|
2594
|
-
{ color: colour, scale:
|
|
2868
|
+
{ color: colour, scale: swell(term), duration: 0.5 },
|
|
2595
2869
|
at
|
|
2596
2870
|
)
|
|
2597
2871
|
);
|
|
2598
2872
|
const prev = terms[i - 1];
|
|
2599
2873
|
if (prev) {
|
|
2600
|
-
tl.push(
|
|
2874
|
+
tl.push(
|
|
2875
|
+
tween(`#${sid} .t-${prev.tone}`, { scale: swell(prev) }, { scale: 1, duration: 0.4 }, at)
|
|
2876
|
+
);
|
|
2601
2877
|
}
|
|
2602
2878
|
holds.push(at + 0.6);
|
|
2603
2879
|
});
|
|
@@ -2605,7 +2881,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2605
2881
|
if (last) {
|
|
2606
2882
|
const at = first + terms.length * step2;
|
|
2607
2883
|
tl.push(
|
|
2608
|
-
tween(`#${sid} .t-${last.tone}`, { scale:
|
|
2884
|
+
tween(`#${sid} .t-${last.tone}`, { scale: swell(last) }, { scale: 1, duration: 0.4 }, at),
|
|
2609
2885
|
...spot.restore(at)
|
|
2610
2886
|
);
|
|
2611
2887
|
}
|
|
@@ -2613,6 +2889,8 @@ var equationWalk = (beat, ctx) => {
|
|
|
2613
2889
|
html,
|
|
2614
2890
|
tl,
|
|
2615
2891
|
setup,
|
|
2892
|
+
measure: measure4,
|
|
2893
|
+
...walk.notes.length ? { warnings: walk.notes } : {},
|
|
2616
2894
|
holds: holdsWithin(holds, beat.seconds),
|
|
2617
2895
|
css: [
|
|
2618
2896
|
chromeCss(theme),
|
|
@@ -2632,6 +2910,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2632
2910
|
// has its margin zeroed above, so without an explicit gap the two lines
|
|
2633
2911
|
// would touch and read as one equation broken mid-expression.
|
|
2634
2912
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
2913
|
+
// Set by `mathFit` when one line will not fit at the floor: the display is
|
|
2914
|
+
// re-rendered inline, where KaTeX breaks after a top-level relation or
|
|
2915
|
+
// operator, and the lines it breaks into are centred and spaced as lines.
|
|
2916
|
+
".eq-broken{line-height:1.5}",
|
|
2917
|
+
// An equation KaTeX refuses, shown as its source. Wraps anywhere, because
|
|
2918
|
+
// TeX source has few spaces and must not run off the slide either. The
|
|
2919
|
+
// deck's own face, not a monospace one: a family the bundle does not
|
|
2920
|
+
// declare falls back silently (invariant 9).
|
|
2921
|
+
".eq-plain{white-space:pre-wrap;overflow-wrap:anywhere}",
|
|
2635
2922
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
2636
2923
|
".term{display:inline-block}",
|
|
2637
2924
|
legendCss(theme),
|
|
@@ -2646,26 +2933,67 @@ var equationWalk = (beat, ctx) => {
|
|
|
2646
2933
|
|
|
2647
2934
|
// src/emit/archetypes/equation-morph.ts
|
|
2648
2935
|
var MORPH_SECONDS = 1.6;
|
|
2649
|
-
|
|
2650
|
-
const { sid, theme } = ctx;
|
|
2936
|
+
function prepareMorph(beat, source) {
|
|
2651
2937
|
const p = beat.params;
|
|
2652
2938
|
const find3 = (id2) => {
|
|
2653
|
-
const eq =
|
|
2939
|
+
const eq = source.equations.find((e) => e.id === id2);
|
|
2654
2940
|
if (!eq)
|
|
2655
|
-
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${
|
|
2941
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${source.id}`);
|
|
2656
2942
|
return eq;
|
|
2657
2943
|
};
|
|
2658
|
-
const
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2944
|
+
const notes = [];
|
|
2945
|
+
const drawable = (eq) => {
|
|
2946
|
+
const fixed = repairTex(eq.tex, true);
|
|
2947
|
+
if (fixed.error !== null) {
|
|
2948
|
+
throw new Error(
|
|
2949
|
+
`equation-morph ${beat.id}: ${eq.id} does not parse even after repair (${fixed.error}), and a morph cannot be drawn from plain text. Formula: ${JSON.stringify(eq.tex)}`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
notes.push(...fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`));
|
|
2953
|
+
return { id: eq.id, tex: fixed.tex };
|
|
2954
|
+
};
|
|
2955
|
+
const a = drawable(find3(p.fromId));
|
|
2956
|
+
const b = drawable(find3(p.toId));
|
|
2957
|
+
const repaired = p.terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
2958
|
+
const original = (t2) => p.terms[repaired.indexOf(t2)];
|
|
2959
|
+
let keyed = repaired.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
2960
|
+
if (keyed.length === 0) {
|
|
2662
2961
|
throw new Error(
|
|
2663
2962
|
`equation-morph ${beat.id}: none of its ${p.terms.length} term(s) occur in both equations. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. From: ${JSON.stringify(a.tex)}. To: ${JSON.stringify(b.tex)}`
|
|
2664
2963
|
);
|
|
2665
2964
|
}
|
|
2965
|
+
notes.push(
|
|
2966
|
+
...repaired.filter((t2) => !keyed.includes(t2)).map(
|
|
2967
|
+
(t2) => `term ${JSON.stringify(original(t2).tex)} does not occur in both ${a.id} and ${b.id}, so it does not travel and its legend row ("${t2.label}") is left out`
|
|
2968
|
+
)
|
|
2969
|
+
);
|
|
2666
2970
|
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
2667
|
-
|
|
2668
|
-
|
|
2971
|
+
let wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
2972
|
+
let wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
2973
|
+
const paired = keyed.filter((t2) => wa.used.includes(t2) && wb.used.includes(t2));
|
|
2974
|
+
if (paired.length < keyed.length) {
|
|
2975
|
+
const unpaired = keyed.filter((t2) => !paired.includes(t2));
|
|
2976
|
+
notes.push(
|
|
2977
|
+
...unpaired.map(
|
|
2978
|
+
(t2) => `term ${JSON.stringify(original(t2).tex)} cannot be marked on both lines without breaking one, so it does not travel and its legend row ("${t2.label}") is left out`
|
|
2979
|
+
)
|
|
2980
|
+
);
|
|
2981
|
+
if (paired.length === 0) {
|
|
2982
|
+
throw new Error(
|
|
2983
|
+
`equation-morph ${beat.id}: no term can be marked on both lines without breaking one. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}`
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
keyed = paired;
|
|
2987
|
+
wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
2988
|
+
wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
2989
|
+
}
|
|
2990
|
+
const both = keyed.map(original);
|
|
2991
|
+
return { a, b, wa: wa.tex, wb: wb.tex, both, notes };
|
|
2992
|
+
}
|
|
2993
|
+
var equationMorph = (beat, ctx) => {
|
|
2994
|
+
const { sid, theme } = ctx;
|
|
2995
|
+
const p = beat.params;
|
|
2996
|
+
const { a, b, wa, wb, both, notes } = prepareMorph(beat, ctx.source);
|
|
2669
2997
|
const size3 = Math.max(
|
|
2670
2998
|
MIN_FONT,
|
|
2671
2999
|
Math.min(
|
|
@@ -2687,9 +3015,7 @@ var equationMorph = (beat, ctx) => {
|
|
|
2687
3015
|
`var OPTS = ${OPTS};`,
|
|
2688
3016
|
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
2689
3017
|
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
2690
|
-
...both.map(
|
|
2691
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2692
|
-
)
|
|
3018
|
+
...both.map((t2) => chipSetup(sid, t2))
|
|
2693
3019
|
];
|
|
2694
3020
|
const first = 1.8;
|
|
2695
3021
|
const at = Math.round(
|
|
@@ -2722,8 +3048,11 @@ var equationMorph = (beat, ctx) => {
|
|
|
2722
3048
|
setup,
|
|
2723
3049
|
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
2724
3050
|
// the ready gate, and the plugin tween above finds it on the host.
|
|
2725
|
-
|
|
3051
|
+
// The chips are fitted before the plan is built: growing one moves the
|
|
3052
|
+
// legend, and nothing the morph measures may move after it has measured.
|
|
3053
|
+
measure: [...chipFit(sid, both), `DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
2726
3054
|
plugins: ["dsMorph"],
|
|
3055
|
+
...notes.length ? { warnings: notes } : {},
|
|
2727
3056
|
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
2728
3057
|
css: [
|
|
2729
3058
|
chromeCss(theme),
|
|
@@ -3304,16 +3633,16 @@ var lineChart = (beat, ctx) => {
|
|
|
3304
3633
|
</svg>${readout}
|
|
3305
3634
|
</div>`;
|
|
3306
3635
|
const draw2 = 0.8;
|
|
3307
|
-
const
|
|
3308
|
-
const idealStep = Math.min(0.45, DRAW_SECONDS /
|
|
3636
|
+
const count2 = p.points.length;
|
|
3637
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count2);
|
|
3309
3638
|
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
3310
|
-
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length,
|
|
3639
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count2 - 1) : 0;
|
|
3311
3640
|
const tailAfter = (s) => Math.max(
|
|
3312
|
-
s *
|
|
3641
|
+
s * count2 + 0.4,
|
|
3313
3642
|
// the ring: its walk, then the 0.4s it takes to leave
|
|
3314
|
-
0.3 + s * (
|
|
3643
|
+
0.3 + s * (count2 - 1),
|
|
3315
3644
|
// the dots
|
|
3316
|
-
0.5 + s * (
|
|
3645
|
+
0.5 + s * (count2 - 1),
|
|
3317
3646
|
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
3318
3647
|
);
|
|
3319
3648
|
const room = beat.seconds - spine;
|
|
@@ -3376,8 +3705,8 @@ var lineChart = (beat, ctx) => {
|
|
|
3376
3705
|
)
|
|
3377
3706
|
);
|
|
3378
3707
|
}
|
|
3379
|
-
const walk = cmp ? sec(step2 *
|
|
3380
|
-
if (
|
|
3708
|
+
const walk = cmp ? sec(step2 * count2) : DRAW_SECONDS;
|
|
3709
|
+
if (count2 > 1) {
|
|
3381
3710
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
3382
3711
|
tl.push(
|
|
3383
3712
|
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
@@ -3392,7 +3721,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3392
3721
|
)
|
|
3393
3722
|
);
|
|
3394
3723
|
}
|
|
3395
|
-
const drawn = settled + (cmp ? tailAfter(step2) : step2 *
|
|
3724
|
+
const drawn = settled + (cmp ? tailAfter(step2) : step2 * count2 + 0.4);
|
|
3396
3725
|
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
3397
3726
|
if (p.readout) {
|
|
3398
3727
|
tl.push(
|
|
@@ -3408,7 +3737,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3408
3737
|
return {
|
|
3409
3738
|
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
3410
3739
|
warnings: [
|
|
3411
|
-
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${
|
|
3740
|
+
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${count2} 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.`
|
|
3412
3741
|
]
|
|
3413
3742
|
};
|
|
3414
3743
|
}
|
|
@@ -3488,7 +3817,7 @@ var V_BOX_H = 268;
|
|
|
3488
3817
|
var V_LOOP_SIDE = 56;
|
|
3489
3818
|
var V_LOOP_PAD = 18;
|
|
3490
3819
|
function columnLayout(stageW, stageH, stages, face, loop) {
|
|
3491
|
-
const
|
|
3820
|
+
const count2 = stages.length;
|
|
3492
3821
|
const loopW = loop ? Math.max(240, Math.round(stageW * 0.32)) : 0;
|
|
3493
3822
|
const boxX = M;
|
|
3494
3823
|
const boxW = stageW - 2 * M - (loop ? V_LOOP_SIDE + V_LOOP_PAD + loopW : 0);
|
|
@@ -3505,11 +3834,11 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3505
3834
|
return Math.ceil(2 * PAD_Y2 + label + (note2 > 0 ? NOTE_TOP + note2 * NOTE * NOTE_LH : 0));
|
|
3506
3835
|
})
|
|
3507
3836
|
);
|
|
3508
|
-
const room = (stageH - 2 * M - (
|
|
3837
|
+
const room = (stageH - 2 * M - (count2 - 1) * V_GAP) / count2;
|
|
3509
3838
|
const boxH = Math.max(need, Math.min(room, V_BOX_H));
|
|
3510
3839
|
return {
|
|
3511
3840
|
size: size3,
|
|
3512
|
-
boxes: Array.from({ length:
|
|
3841
|
+
boxes: Array.from({ length: count2 }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
|
|
3513
3842
|
boxH,
|
|
3514
3843
|
boxX,
|
|
3515
3844
|
boxW,
|
|
@@ -3521,7 +3850,7 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3521
3850
|
loopLines: loop ? wrap(loop.label, NOTE, loopW, 500, 0, face) : [],
|
|
3522
3851
|
loopX: boxX + boxW + V_LOOP_SIDE,
|
|
3523
3852
|
loopW,
|
|
3524
|
-
svgH: 2 * M +
|
|
3853
|
+
svgH: 2 * M + count2 * boxH + (count2 - 1) * V_GAP
|
|
3525
3854
|
};
|
|
3526
3855
|
}
|
|
3527
3856
|
function balance(label, k, face) {
|
|
@@ -4239,8 +4568,8 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4239
4568
|
if (inline2.fits) return inline2;
|
|
4240
4569
|
return inline2.wide && inline2.blockH < stacked.blockH ? inline2 : stacked;
|
|
4241
4570
|
}
|
|
4242
|
-
function labelWeight(i,
|
|
4243
|
-
return i ===
|
|
4571
|
+
function labelWeight(i, count2) {
|
|
4572
|
+
return i === count2 - 1 ? 700 : 600;
|
|
4244
4573
|
}
|
|
4245
4574
|
function floorFor(p, format) {
|
|
4246
4575
|
if (!p.tilt) return MIN_FONT;
|
|
@@ -4250,7 +4579,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4250
4579
|
const width = contentW(format);
|
|
4251
4580
|
const boxH = contentH(format);
|
|
4252
4581
|
const floor = floorFor(p, format);
|
|
4253
|
-
const
|
|
4582
|
+
const count2 = p.layers.length;
|
|
4254
4583
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4255
4584
|
const riseMax = RISE_MAX[k];
|
|
4256
4585
|
const syMax = SY_MAX[k];
|
|
@@ -4258,7 +4587,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4258
4587
|
const noteW = (l) => inline2 && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4259
4588
|
const want = Math.max(
|
|
4260
4589
|
...p.layers.map(
|
|
4261
|
-
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i,
|
|
4590
|
+
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count2), 0, false, face) + noteW(l)
|
|
4262
4591
|
)
|
|
4263
4592
|
);
|
|
4264
4593
|
const colCap = inline2 ? width * 0.56 : width * 0.5;
|
|
@@ -4267,7 +4596,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4267
4596
|
const labelX = width - colW;
|
|
4268
4597
|
const labelRoom = Math.min(
|
|
4269
4598
|
...p.layers.map(
|
|
4270
|
-
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i,
|
|
4599
|
+
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count2), 0, false, face))
|
|
4271
4600
|
)
|
|
4272
4601
|
);
|
|
4273
4602
|
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
@@ -4275,7 +4604,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4275
4604
|
const nw = noteW(l);
|
|
4276
4605
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
4277
4606
|
return {
|
|
4278
|
-
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i,
|
|
4607
|
+
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count2), 0, face),
|
|
4279
4608
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4280
4609
|
// short line" — and wrapping one would put its second line under the label.
|
|
4281
4610
|
note: l.note === void 0 ? [] : inline2 ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
@@ -4293,13 +4622,13 @@ function solve2(p, format, inline2, face) {
|
|
|
4293
4622
|
const noteH = noteHeight(p.note, noteWidth(format), 28, face);
|
|
4294
4623
|
const free = boxH - chromeH - 20 - noteH;
|
|
4295
4624
|
const avail = Math.max(300, free);
|
|
4296
|
-
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1,
|
|
4625
|
+
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count2 - 1);
|
|
4297
4626
|
const rise = clamp(room, RISE_MIN, riseMax);
|
|
4298
4627
|
const sy = clamp(rise * 0.62, 46, syMax);
|
|
4299
4628
|
const t2 = clamp(rise * 0.14, 10, tMax);
|
|
4300
4629
|
const span = width - NUM_W - GAP3 - colW;
|
|
4301
4630
|
const sx = clamp(span * 0.2, 110, 220);
|
|
4302
|
-
const height = 2 * pad + (
|
|
4631
|
+
const height = 2 * pad + (count2 - 1) * rise + sy + t2;
|
|
4303
4632
|
return {
|
|
4304
4633
|
// A rise shorter than a label block prints the note through the label below.
|
|
4305
4634
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
@@ -4324,7 +4653,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4324
4653
|
labelSize,
|
|
4325
4654
|
blockH,
|
|
4326
4655
|
pad,
|
|
4327
|
-
yBase: pad + (
|
|
4656
|
+
yBase: pad + (count2 - 1) * rise + sy,
|
|
4328
4657
|
lines
|
|
4329
4658
|
};
|
|
4330
4659
|
}
|
|
@@ -4341,11 +4670,11 @@ var stack = (beat, ctx) => {
|
|
|
4341
4670
|
const p = beat.params;
|
|
4342
4671
|
const face = faceOf(ctx.theme.fontStack);
|
|
4343
4672
|
const L = stackLayout(p, ctx.format, face);
|
|
4344
|
-
const
|
|
4345
|
-
const last =
|
|
4673
|
+
const count2 = p.layers.length;
|
|
4674
|
+
const last = count2 - 1;
|
|
4346
4675
|
if (!L.fits) {
|
|
4347
4676
|
throw new Error(
|
|
4348
|
-
`stack ${beat.id}: ${
|
|
4677
|
+
`stack ${beat.id}: ${count2} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${Math.round(L.floor)}px floor and with the notes already moved beside their labels. Nothing here may be set smaller, so the lever is upstream of this beat: drop a layer, or shorten the notes.`
|
|
4349
4678
|
);
|
|
4350
4679
|
}
|
|
4351
4680
|
const parts = {};
|
|
@@ -4373,7 +4702,7 @@ var stack = (beat, ctx) => {
|
|
|
4373
4702
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
4374
4703
|
{
|
|
4375
4704
|
size: L.labelSize,
|
|
4376
|
-
weight: labelWeight(i,
|
|
4705
|
+
weight: labelWeight(i, count2),
|
|
4377
4706
|
fill: top ? theme.tones.b : theme.fg,
|
|
4378
4707
|
maxWidth: block.labelMaxW,
|
|
4379
4708
|
lineHeight: 1.16,
|
|
@@ -4427,10 +4756,10 @@ var stack = (beat, ctx) => {
|
|
|
4427
4756
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4428
4757
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4429
4758
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
|
|
4430
|
-
const centre2 = (
|
|
4759
|
+
const centre2 = (count2 - 1) / 2;
|
|
4431
4760
|
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4432
4761
|
const first = 0.9;
|
|
4433
|
-
const step2 = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) /
|
|
4762
|
+
const step2 = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count2));
|
|
4434
4763
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
4435
4764
|
const holds = [];
|
|
4436
4765
|
const spot = spotlighter(sid);
|
|
@@ -4462,11 +4791,11 @@ var stack = (beat, ctx) => {
|
|
|
4462
4791
|
);
|
|
4463
4792
|
}
|
|
4464
4793
|
if (p.note) {
|
|
4465
|
-
const at = first +
|
|
4794
|
+
const at = first + count2 * step2;
|
|
4466
4795
|
tl.push(tween(`#${sid}-note`, { opacity: 0 }, { opacity: 1, duration: 0.6 }, at));
|
|
4467
4796
|
holds.push(at + 0.7);
|
|
4468
4797
|
}
|
|
4469
|
-
if (
|
|
4798
|
+
if (count2 > 1) tl.push(...spot.restore(first + count2 * step2));
|
|
4470
4799
|
return {
|
|
4471
4800
|
html,
|
|
4472
4801
|
parts,
|
|
@@ -5789,8 +6118,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5789
6118
|
const text2 = beat.narration?.trim();
|
|
5790
6119
|
if (!text2) continue;
|
|
5791
6120
|
const segments2 = [];
|
|
5792
|
-
const
|
|
5793
|
-
const plan = planSegments(text2, Math.min(
|
|
6121
|
+
const count2 = stopsFor(beat, source, format, `s${i + 1}`, theme);
|
|
6122
|
+
const plan = planSegments(text2, Math.min(count2, paced.speakingStops));
|
|
5794
6123
|
for (const [stop, line2] of plan.entries()) {
|
|
5795
6124
|
if (!line2) continue;
|
|
5796
6125
|
const speech = await synthesize(line2, {
|
|
@@ -5812,7 +6141,7 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5812
6141
|
}
|
|
5813
6142
|
if (segments2.length > 0) {
|
|
5814
6143
|
beats[beat.id] = segments2;
|
|
5815
|
-
staged[beat.id] =
|
|
6144
|
+
staged[beat.id] = count2;
|
|
5816
6145
|
}
|
|
5817
6146
|
}
|
|
5818
6147
|
return {
|
|
@@ -6707,11 +7036,11 @@ function stageScene(scene, speed) {
|
|
|
6707
7036
|
function speechPlan(open, holds, segments2) {
|
|
6708
7037
|
const starts = [];
|
|
6709
7038
|
let at = open;
|
|
6710
|
-
for (const [i,
|
|
6711
|
-
const hold = holds[Math.min(
|
|
7039
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7040
|
+
const hold = holds[Math.min(segment2.stop, holds.length - 1)] ?? 0;
|
|
6712
7041
|
if (i > 0) at = Math.max(at, hold);
|
|
6713
7042
|
starts.push(at);
|
|
6714
|
-
at +=
|
|
7043
|
+
at += segment2.seconds;
|
|
6715
7044
|
}
|
|
6716
7045
|
return { starts, end: at };
|
|
6717
7046
|
}
|
|
@@ -8206,8 +8535,13 @@ function renderSource(source) {
|
|
|
8206
8535
|
}
|
|
8207
8536
|
}
|
|
8208
8537
|
out.push("", "== EQUATIONS ==");
|
|
8209
|
-
for (const e of source.equations)
|
|
8210
|
-
|
|
8538
|
+
for (const e of source.equations) {
|
|
8539
|
+
const fixed = repairTex(e.tex, true);
|
|
8540
|
+
const note2 = fixed.error === null ? "" : " (KaTeX cannot draw this one: a walk shows it as plain source, a morph cannot use it)";
|
|
8541
|
+
out.push(
|
|
8542
|
+
`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${fixed.error === null ? fixed.tex : e.tex}${note2}`
|
|
8543
|
+
);
|
|
8544
|
+
}
|
|
8211
8545
|
if (!source.equations.length)
|
|
8212
8546
|
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
8213
8547
|
out.push("", "== TABLES ==");
|
|
@@ -8771,10 +9105,10 @@ function drawSvg(req) {
|
|
|
8771
9105
|
const seed = createHash5("sha256").update([req.prompt, req.style, req.aspect].join("|")).digest();
|
|
8772
9106
|
const rand = mulberry32(seed.readUInt32BE(0));
|
|
8773
9107
|
const accent = ACCENTS[Math.floor(rand() * ACCENTS.length)];
|
|
8774
|
-
const
|
|
9108
|
+
const count2 = 6 + Math.floor(rand() * 5);
|
|
8775
9109
|
const unit = Math.min(W, H);
|
|
8776
9110
|
const parts = [`<rect width="${W}" height="${H}" fill="${PAPER}"/>`];
|
|
8777
|
-
for (let i = 0; i <
|
|
9111
|
+
for (let i = 0; i < count2; i++) {
|
|
8778
9112
|
const kind = Math.floor(rand() * 3);
|
|
8779
9113
|
const cx = Math.round(W * (0.15 + 0.7 * rand()));
|
|
8780
9114
|
const cy = Math.round(H * (0.15 + 0.7 * rand()));
|
|
@@ -9199,6 +9533,22 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
9199
9533
|
async function chromePath(need = "open the deck with") {
|
|
9200
9534
|
return (await resolveChrome(need)).path;
|
|
9201
9535
|
}
|
|
9536
|
+
function sandboxMissing(err) {
|
|
9537
|
+
return /No usable sandbox/.test(err instanceof Error ? err.message : String(err));
|
|
9538
|
+
}
|
|
9539
|
+
var SANDBOX_FIX = "`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`, or give Chrome an AppArmor profile";
|
|
9540
|
+
async function launchOwnPage(launch, args, what) {
|
|
9541
|
+
try {
|
|
9542
|
+
return await launch(args);
|
|
9543
|
+
} catch (err) {
|
|
9544
|
+
if (!sandboxMissing(err)) throw err;
|
|
9545
|
+
process.stderr.write(
|
|
9546
|
+
`chrome: this host gives Chrome no sandbox, so ${what} runs without one, as hyperframes' render always does. To keep the sandbox: ${SANDBOX_FIX}.
|
|
9547
|
+
`
|
|
9548
|
+
);
|
|
9549
|
+
return launch([...args, "--no-sandbox"]);
|
|
9550
|
+
}
|
|
9551
|
+
}
|
|
9202
9552
|
function runtimePath() {
|
|
9203
9553
|
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
9204
9554
|
}
|
|
@@ -9216,10 +9566,9 @@ async function openDeck(dir, opts = {}) {
|
|
|
9216
9566
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
9217
9567
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
9218
9568
|
const chrome2 = await resolveChrome();
|
|
9219
|
-
const browser = await
|
|
9220
|
-
executablePath: chrome2.path,
|
|
9221
|
-
|
|
9222
|
-
args: [
|
|
9569
|
+
const browser = await launchOwnPage(
|
|
9570
|
+
(args) => puppeteer.launch({ executablePath: chrome2.path, headless: true, args }),
|
|
9571
|
+
[
|
|
9223
9572
|
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
9224
9573
|
// renderer's.
|
|
9225
9574
|
"--force-device-scale-factor=1",
|
|
@@ -9232,8 +9581,9 @@ async function openDeck(dir, opts = {}) {
|
|
|
9232
9581
|
"--use-gl=angle",
|
|
9233
9582
|
"--use-angle=swiftshader",
|
|
9234
9583
|
"--enable-unsafe-swiftshader"
|
|
9235
|
-
]
|
|
9236
|
-
|
|
9584
|
+
],
|
|
9585
|
+
"the gates' page"
|
|
9586
|
+
);
|
|
9237
9587
|
try {
|
|
9238
9588
|
const page = await browser.newPage();
|
|
9239
9589
|
await page.evaluateOnNewDocument("self.__name = self.__name || ((fn) => fn);");
|
|
@@ -9336,10 +9686,10 @@ function pack(words2, width) {
|
|
|
9336
9686
|
function splitCue(cue, max = CUE_MAX_CHARS) {
|
|
9337
9687
|
if (cue.text.length <= max) return [cue];
|
|
9338
9688
|
const words2 = cue.text.split(/\s+/).filter(Boolean);
|
|
9339
|
-
const
|
|
9689
|
+
const count2 = pack(words2, max).length;
|
|
9340
9690
|
let width = max;
|
|
9341
|
-
for (let w = Math.ceil(cue.text.length /
|
|
9342
|
-
if (pack(words2, w).length <=
|
|
9691
|
+
for (let w = Math.ceil(cue.text.length / count2); w < max; w++) {
|
|
9692
|
+
if (pack(words2, w).length <= count2) {
|
|
9343
9693
|
width = w;
|
|
9344
9694
|
break;
|
|
9345
9695
|
}
|
|
@@ -9431,17 +9781,17 @@ function place(scenes, spoken) {
|
|
|
9431
9781
|
);
|
|
9432
9782
|
}
|
|
9433
9783
|
const { starts } = speechPlan(scene.open, scene.holds, segments2);
|
|
9434
|
-
for (const [i,
|
|
9435
|
-
const index = Math.min(
|
|
9784
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
9785
|
+
const index = Math.min(segment2.stop, scene.holds.length - 1);
|
|
9436
9786
|
out.push({
|
|
9437
|
-
id: `${scene.id}.${
|
|
9787
|
+
id: `${scene.id}.${segment2.stop}`,
|
|
9438
9788
|
scene: scene.id,
|
|
9439
|
-
stop:
|
|
9440
|
-
audio:
|
|
9789
|
+
stop: segment2.stop,
|
|
9790
|
+
audio: segment2.audio,
|
|
9441
9791
|
hold: round5(scene.start + scene.holds[index]),
|
|
9442
9792
|
start: round5(scene.start + starts[i]),
|
|
9443
|
-
duration:
|
|
9444
|
-
cues:
|
|
9793
|
+
duration: segment2.seconds,
|
|
9794
|
+
cues: segment2.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
|
|
9445
9795
|
});
|
|
9446
9796
|
}
|
|
9447
9797
|
}
|
|
@@ -9518,6 +9868,15 @@ function planTiming(input) {
|
|
|
9518
9868
|
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
9519
9869
|
// disagree with the box the archetypes actually drew into.
|
|
9520
9870
|
captionReserve: format.captionReserve ?? 0,
|
|
9871
|
+
// An unbudgeted format's limit is Infinity, which JSON writes as `null`.
|
|
9872
|
+
// Left out instead: recorded as a format with no limit, never with a limit
|
|
9873
|
+
// of null or zero.
|
|
9874
|
+
format: {
|
|
9875
|
+
id: format.id,
|
|
9876
|
+
minWeight: format.minWeight,
|
|
9877
|
+
...Number.isFinite(format.maxSeconds) && { maxSeconds: format.maxSeconds },
|
|
9878
|
+
...format.warnSeconds !== void 0 && { warnSeconds: format.warnSeconds }
|
|
9879
|
+
},
|
|
9521
9880
|
scenes,
|
|
9522
9881
|
segments: segments2
|
|
9523
9882
|
};
|
|
@@ -9544,15 +9903,15 @@ function framePlan(timing, fps) {
|
|
|
9544
9903
|
);
|
|
9545
9904
|
}
|
|
9546
9905
|
pieces.push({ from: first, motion: last - first, freeze: 0 });
|
|
9547
|
-
for (const
|
|
9548
|
-
const at = f(
|
|
9906
|
+
for (const segment2 of segments2) {
|
|
9907
|
+
const at = f(segment2.start);
|
|
9549
9908
|
audio.push({
|
|
9550
|
-
id:
|
|
9551
|
-
audio:
|
|
9909
|
+
id: segment2.id,
|
|
9910
|
+
audio: segment2.audio,
|
|
9552
9911
|
startFrame: at,
|
|
9553
9912
|
delayMs: Math.round(at / fps * 1e3)
|
|
9554
9913
|
});
|
|
9555
|
-
for (const cue of
|
|
9914
|
+
for (const cue of segment2.cues) {
|
|
9556
9915
|
const start = at / fps + cue.start;
|
|
9557
9916
|
const end = at / fps + cue.end;
|
|
9558
9917
|
if (end > start) cues.push({ start, end, text: cue.text });
|
|
@@ -9671,7 +10030,7 @@ ${bands}
|
|
|
9671
10030
|
</div>
|
|
9672
10031
|
`;
|
|
9673
10032
|
}
|
|
9674
|
-
function measure3(
|
|
10033
|
+
function measure3(count2) {
|
|
9675
10034
|
const doc = document;
|
|
9676
10035
|
const probe2 = doc.getElementById("c0");
|
|
9677
10036
|
if (probe2) {
|
|
@@ -9684,7 +10043,7 @@ function measure3(count) {
|
|
|
9684
10043
|
probe2.classList.remove("on");
|
|
9685
10044
|
}
|
|
9686
10045
|
const rects = [];
|
|
9687
|
-
for (let i = 0; i <
|
|
10046
|
+
for (let i = 0; i < count2; i++) {
|
|
9688
10047
|
const el = doc.getElementById(`c${i}`);
|
|
9689
10048
|
if (!el) continue;
|
|
9690
10049
|
el.classList.add("on");
|
|
@@ -9712,11 +10071,12 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
9712
10071
|
await mkdir8(work, { recursive: true });
|
|
9713
10072
|
await writeFile10(page, captionPage(cues, style, href));
|
|
9714
10073
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
9715
|
-
const
|
|
9716
|
-
|
|
9717
|
-
headless: true,
|
|
9718
|
-
|
|
9719
|
-
|
|
10074
|
+
const executablePath = await chromePath(CAPTION_NEED);
|
|
10075
|
+
const browser = await launchOwnPage(
|
|
10076
|
+
(args) => puppeteer.launch({ executablePath, headless: true, args }),
|
|
10077
|
+
["--force-device-scale-factor=1", "--hide-scrollbars"],
|
|
10078
|
+
"the caption band"
|
|
10079
|
+
);
|
|
9720
10080
|
try {
|
|
9721
10081
|
const tab = await browser.newPage();
|
|
9722
10082
|
await tab.setViewport({ width: style.width, height: style.height, deviceScaleFactor: 1 });
|
|
@@ -10633,6 +10993,14 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
10633
10993
|
// happens to be that run.
|
|
10634
10994
|
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
10635
10995
|
]
|
|
10996
|
+
// No `--no-sandbox` fallback here, unlike `launchOwnPage`: this is a page
|
|
10997
|
+
// from the web, and a renderer bug it triggers is exactly what the sandbox
|
|
10998
|
+
// contains. Refused, with the fix, rather than Chrome's raw FATAL.
|
|
10999
|
+
}).catch((err) => {
|
|
11000
|
+
if (!sandboxMissing(err)) throw err;
|
|
11001
|
+
throw new Error(
|
|
11002
|
+
`Chrome has no usable sandbox on this host, and a page from the web is not opened without one. ${SANDBOX_FIX}, then ingest again \u2014 or save the page and ingest the file.`
|
|
11003
|
+
);
|
|
10636
11004
|
});
|
|
10637
11005
|
try {
|
|
10638
11006
|
const page = await browser.newPage();
|
|
@@ -11666,10 +12034,10 @@ function readCanvas(html) {
|
|
|
11666
12034
|
function profilesFor(width, height, formats = FORMATS) {
|
|
11667
12035
|
return Object.values(formats).filter((f) => f.width === width && f.height === height);
|
|
11668
12036
|
}
|
|
11669
|
-
function scanBudget(html, storyboard, kept, formats = FORMATS) {
|
|
12037
|
+
function scanBudget(html, storyboard, kept, formats = FORMATS, recorded) {
|
|
11670
12038
|
const canvas = readCanvas(html);
|
|
11671
12039
|
if (!canvas) return [];
|
|
11672
|
-
const profiles = profilesFor(canvas.width, canvas.height, formats);
|
|
12040
|
+
const profiles = recorded && recorded.width === canvas.width && recorded.height === canvas.height ? [recorded] : profilesFor(canvas.width, canvas.height, formats);
|
|
11673
12041
|
if (profiles.length === 0) {
|
|
11674
12042
|
return [
|
|
11675
12043
|
{
|
|
@@ -12394,6 +12762,20 @@ function captionBottom(sid, selector, fallbackPx) {
|
|
|
12394
12762
|
}
|
|
12395
12763
|
return bottom > 0 ? bottom : fallbackPx;
|
|
12396
12764
|
}
|
|
12765
|
+
function collectUnfit(attr) {
|
|
12766
|
+
return Array.from(document.querySelectorAll(`[${attr}]`)).map((el) => ({
|
|
12767
|
+
sid: el.closest("[data-composition-id]")?.getAttribute("data-composition-id") ?? "?",
|
|
12768
|
+
tex: el.getAttribute(attr) ?? ""
|
|
12769
|
+
}));
|
|
12770
|
+
}
|
|
12771
|
+
function gradeUnfit(rows) {
|
|
12772
|
+
return rows.map((r) => ({
|
|
12773
|
+
severity: "error",
|
|
12774
|
+
gate: "fidelity",
|
|
12775
|
+
rule: "math_unfit",
|
|
12776
|
+
message: `${r.sid}: the equation does not fit its box even at the ${TYPE_FLOOR_PX}px floor broken across lines \u2014 ${JSON.stringify(r.tex)}. Shorten it in the source, or walk it across two beats.`
|
|
12777
|
+
}));
|
|
12778
|
+
}
|
|
12397
12779
|
async function fidelity(dir, opts = {}) {
|
|
12398
12780
|
const started = Date.now();
|
|
12399
12781
|
const floor = opts.floor ?? INK_FLOOR;
|
|
@@ -12417,6 +12799,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
12417
12799
|
try {
|
|
12418
12800
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
12419
12801
|
const { page, height } = deck;
|
|
12802
|
+
await page.waitForFunction("window.__hfTimelinesBuilding === false", {
|
|
12803
|
+
timeout: opts.timeoutMs ?? 6e4
|
|
12804
|
+
});
|
|
12805
|
+
const unfit = await page.evaluate(collectUnfit, UNFIT_ATTR);
|
|
12420
12806
|
const measured = [];
|
|
12421
12807
|
const collided = [];
|
|
12422
12808
|
const apparent = [];
|
|
@@ -12460,7 +12846,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
12460
12846
|
...gradeFidelity(measured, floor),
|
|
12461
12847
|
...gradeReserve(measured),
|
|
12462
12848
|
...gradeOverprint(collided),
|
|
12463
|
-
...gradeApparent(apparent)
|
|
12849
|
+
...gradeApparent(apparent),
|
|
12850
|
+
...gradeUnfit(unfit)
|
|
12464
12851
|
],
|
|
12465
12852
|
elapsedMs: Date.now() - started
|
|
12466
12853
|
};
|
|
@@ -12807,9 +13194,13 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12807
13194
|
await readFile17(join17(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12808
13195
|
await listFiles(dir)
|
|
12809
13196
|
);
|
|
12810
|
-
const
|
|
13197
|
+
const timing = await readTiming2(dir);
|
|
13198
|
+
const recorded = typeof timing?.format?.id === "string" ? { ...timing.format, width: timing.width, height: timing.height } : void 0;
|
|
13199
|
+
const budget3 = html.flatMap(
|
|
13200
|
+
([, text2]) => scanBudget(text2, storyboard, kept, void 0, recorded)
|
|
13201
|
+
);
|
|
12811
13202
|
const type = html.flatMap(([file, text2]) => scanTypeFloor(text2, file));
|
|
12812
|
-
const lead = kept
|
|
13203
|
+
const lead = kept && timing ? scanNarrationLead(kept, timing) : [];
|
|
12813
13204
|
const ours = [...determinism, ...narration, ...budget3, ...type, ...lead];
|
|
12814
13205
|
const stops = await declaredStops(dir);
|
|
12815
13206
|
const [verdict, frames2] = await Promise.all([
|
|
@@ -12998,6 +13389,31 @@ function scanUnusedFigures(storyboard, source) {
|
|
|
12998
13389
|
}
|
|
12999
13390
|
];
|
|
13000
13391
|
}
|
|
13392
|
+
function scanMath(storyboard, source) {
|
|
13393
|
+
const out = [];
|
|
13394
|
+
const say = (rule, message2) => {
|
|
13395
|
+
out.push({ severity: "warning", gate: "storyboard", rule, message: message2 });
|
|
13396
|
+
};
|
|
13397
|
+
for (const beat of storyboard.beats) {
|
|
13398
|
+
try {
|
|
13399
|
+
let notes = [];
|
|
13400
|
+
if (beat.archetype === "equation-walk") {
|
|
13401
|
+
const params = beat.params;
|
|
13402
|
+
const eq = source.equations.find((e) => e.id === params.equationId);
|
|
13403
|
+
if (eq) notes = prepareWalk(eq, params.terms, beat.id).notes;
|
|
13404
|
+
} else if (beat.archetype === "equation-morph") {
|
|
13405
|
+
notes = prepareMorph(beat, source).notes;
|
|
13406
|
+
}
|
|
13407
|
+
for (const n3 of notes) say("tex_repaired", `${beat.id}: ${n3}`);
|
|
13408
|
+
} catch (err) {
|
|
13409
|
+
say(
|
|
13410
|
+
"tex_unrenderable",
|
|
13411
|
+
`${beat.id}: ${err instanceof Error ? err.message : String(err)} \u2014 build will leave this beat out`
|
|
13412
|
+
);
|
|
13413
|
+
}
|
|
13414
|
+
}
|
|
13415
|
+
return out;
|
|
13416
|
+
}
|
|
13001
13417
|
function figuresShown2(params) {
|
|
13002
13418
|
const sides = ["left", "right"].map(
|
|
13003
13419
|
(side) => params[side]?.figureId
|
|
@@ -13079,14 +13495,14 @@ function scanNarrationLead(beats, timing) {
|
|
|
13079
13495
|
return findings;
|
|
13080
13496
|
}
|
|
13081
13497
|
function firstMention(segments2, label) {
|
|
13082
|
-
for (const
|
|
13083
|
-
for (const cue of
|
|
13498
|
+
for (const segment2 of segments2) {
|
|
13499
|
+
for (const cue of segment2.cues) {
|
|
13084
13500
|
const words2 = cue.text.split(/\s+/);
|
|
13085
13501
|
let at = 0;
|
|
13086
13502
|
for (const word of words2) {
|
|
13087
13503
|
if (mentions(word, label)) {
|
|
13088
13504
|
const share2 = cue.text.length > 0 ? at / cue.text.length : 0;
|
|
13089
|
-
return
|
|
13505
|
+
return segment2.start + cue.start + share2 * (cue.end - cue.start);
|
|
13090
13506
|
}
|
|
13091
13507
|
at += word.length + 1;
|
|
13092
13508
|
}
|
|
@@ -13354,7 +13770,11 @@ imageFlags(
|
|
|
13354
13770
|
...scanRepeatedObject(storyboard),
|
|
13355
13771
|
// A figure the plan ignored is cheapest to fix here, where the answer is one
|
|
13356
13772
|
// more beat in a file the author already has open.
|
|
13357
|
-
...scanUnusedFigures(storyboard, source)
|
|
13773
|
+
...scanUnusedFigures(storyboard, source),
|
|
13774
|
+
// Every formula the deck will draw, parsed here by the KaTeX it vendors. A
|
|
13775
|
+
// formula the browser could not parse used to be found at `verify`, after
|
|
13776
|
+
// the build and the narration had been spent on it.
|
|
13777
|
+
...scanMath(storyboard, source)
|
|
13358
13778
|
])
|
|
13359
13779
|
step(`plan: ${f.message}`);
|
|
13360
13780
|
const script = storyboard.beats.map((b) => b.narration?.trim()).filter(Boolean).join(" ");
|
|
@@ -13700,12 +14120,12 @@ async function gate(dir, snapshots = false, storyboard, kept, fidelity2 = true,
|
|
|
13700
14120
|
if (!verdict.passed) process.exitCode = 1;
|
|
13701
14121
|
}
|
|
13702
14122
|
function report(v) {
|
|
13703
|
-
const
|
|
14123
|
+
const count2 = (s) => v.findings.filter((f) => f.severity === s).length;
|
|
13704
14124
|
const lines = v.findings.map(
|
|
13705
14125
|
(f) => ` ${f.severity.padEnd(7)} ${f.gate.padEnd(11)} ${f.rule} ${f.message}`
|
|
13706
14126
|
);
|
|
13707
14127
|
lines.push(
|
|
13708
|
-
`${v.passed ? "PASS" : "FAIL"} \u2014 ${
|
|
14128
|
+
`${v.passed ? "PASS" : "FAIL"} \u2014 ${count2("error")} error(s), ${count2("warning")} warning(s)`
|
|
13709
14129
|
);
|
|
13710
14130
|
return `${lines.join("\n")}
|
|
13711
14131
|
`;
|