@jokerized/decksmith 0.6.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 +29 -8
- package/dist/cli.js +619 -179
- package/dist/index.js +613 -176
- package/dist/mcp.js +554 -166
- package/dist/types/build/files.d.ts +2 -0
- 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/composition.d.ts +3 -2
- 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/source/fonts.d.ts +1 -1
- 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 +2 -1
package/dist/mcp.js
CHANGED
|
@@ -218,6 +218,11 @@ async function copyAudio(from, narration, out, step) {
|
|
|
218
218
|
return names.map((n3) => join3(dir, n3));
|
|
219
219
|
}
|
|
220
220
|
async function refreshFont(storyboard, source, out, step) {
|
|
221
|
+
if (familyFor(storyboard.lang) === null) {
|
|
222
|
+
const css = await vendorInter(join3(out, "assets", "fonts"));
|
|
223
|
+
step("build: vendored Inter beside the deck");
|
|
224
|
+
return css;
|
|
225
|
+
}
|
|
221
226
|
try {
|
|
222
227
|
const bundle = await bundleFont(
|
|
223
228
|
storyboard.lang,
|
|
@@ -233,6 +238,21 @@ async function refreshFont(storyboard, source, out, step) {
|
|
|
233
238
|
return void 0;
|
|
234
239
|
}
|
|
235
240
|
}
|
|
241
|
+
async function vendorInter(dir) {
|
|
242
|
+
const pkg = dirname2(createRequire3(import.meta.url).resolve("@fontsource-variable/inter"));
|
|
243
|
+
const css = await readFile2(join3(pkg, "index.css"), "utf8");
|
|
244
|
+
await mkdir2(dir, { recursive: true });
|
|
245
|
+
for (const [, name] of css.matchAll(/url\(\.\/files\/([^)]+)\)/g)) {
|
|
246
|
+
await cp(join3(pkg, "files", name ?? ""), join3(dir, name ?? ""));
|
|
247
|
+
}
|
|
248
|
+
const ours = css.replaceAll("font-family: 'Inter Variable'", "font-family: 'Inter'").replaceAll("font-display: swap", "font-display: block").replace(/url\(\.\/files\/([^)]+)\) format\('woff2-variations'\)/g, "url($1) format('woff2')");
|
|
249
|
+
if (/Inter Variable|\.\/files\/|swap/.test(ours) || !ours.includes("font-family: 'Inter';")) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`@fontsource-variable/inter's index.css changed shape; update vendorInter (${pkg})`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return ours;
|
|
255
|
+
}
|
|
236
256
|
|
|
237
257
|
// src/emit/kit.ts
|
|
238
258
|
var PAD_X = 110;
|
|
@@ -910,35 +930,35 @@ function elbow(sid, from, to, o) {
|
|
|
910
930
|
}
|
|
911
931
|
return path(d, { ...strokeAttrs(o), "marker-end": `url(#${headId(sid, o.stroke)})` });
|
|
912
932
|
}
|
|
913
|
-
function tracks(width,
|
|
914
|
-
const w =
|
|
915
|
-
return Array.from({ length:
|
|
933
|
+
function tracks(width, count2, gap, x0 = 0) {
|
|
934
|
+
const w = count2 > 1 ? (width - gap * (count2 - 1)) / count2 : width;
|
|
935
|
+
return Array.from({ length: count2 }, (_, i) => ({ x: x0 + i * (w + gap), w }));
|
|
916
936
|
}
|
|
917
937
|
function fitBoxes(req) {
|
|
918
|
-
const
|
|
938
|
+
const count2 = req.labels.length;
|
|
919
939
|
const minGap = req.minGap ?? 24;
|
|
920
940
|
const padEm = req.padEm ?? 0.8;
|
|
921
941
|
const weight = req.weight ?? 600;
|
|
922
942
|
const x0 = req.x0 ?? 0;
|
|
923
943
|
const unit = Math.max(1, ...req.labels.map((l) => textWidth(l, 1, weight) + 2 * padEm));
|
|
924
|
-
const sizeAt = (gap) =>
|
|
944
|
+
const sizeAt = (gap) => count2 < 1 ? req.size : (req.width - gap * (count2 - 1)) / count2 / unit;
|
|
925
945
|
if (sizeAt(req.gap) >= req.size) {
|
|
926
|
-
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width,
|
|
946
|
+
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width, count2, req.gap, x0) };
|
|
927
947
|
}
|
|
928
|
-
if (
|
|
929
|
-
const gap = (req.width -
|
|
930
|
-
return { ok: true, size: req.size, gap, boxes: tracks(req.width,
|
|
948
|
+
if (count2 > 1 && sizeAt(minGap) >= req.size) {
|
|
949
|
+
const gap = (req.width - count2 * req.size * unit) / (count2 - 1);
|
|
950
|
+
return { ok: true, size: req.size, gap, boxes: tracks(req.width, count2, gap, x0) };
|
|
931
951
|
}
|
|
932
952
|
const size = sizeAt(minGap);
|
|
933
953
|
if (size >= MIN_FONT) {
|
|
934
|
-
return { ok: true, size, gap: minGap, boxes: tracks(req.width,
|
|
954
|
+
return { ok: true, size, gap: minGap, boxes: tracks(req.width, count2, minGap, x0) };
|
|
935
955
|
}
|
|
936
956
|
return {
|
|
937
957
|
ok: false,
|
|
938
958
|
size: MIN_FONT,
|
|
939
959
|
gap: minGap,
|
|
940
|
-
boxes: tracks(req.width,
|
|
941
|
-
needed:
|
|
960
|
+
boxes: tracks(req.width, count2, minGap, x0),
|
|
961
|
+
needed: count2 * MIN_FONT * unit + minGap * (count2 - 1)
|
|
942
962
|
};
|
|
943
963
|
}
|
|
944
964
|
|
|
@@ -1271,8 +1291,8 @@ var MID_GAP = 56;
|
|
|
1271
1291
|
var BAND_GAP = 34;
|
|
1272
1292
|
var COL_GAP = 28;
|
|
1273
1293
|
var MIN_COL = 250;
|
|
1274
|
-
function lineBudget(
|
|
1275
|
-
const perSide = Math.max(1, Math.ceil(
|
|
1294
|
+
function lineBudget(count2, stageH) {
|
|
1295
|
+
const perSide = Math.max(1, Math.ceil(count2 / 2));
|
|
1276
1296
|
const room = (stageH - (perSide - 1) * LAB_GAP) / perSide;
|
|
1277
1297
|
const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
|
|
1278
1298
|
return Math.min(LINE_CEIL, Math.max(1, lines));
|
|
@@ -1683,7 +1703,7 @@ var HEAD_GAP = 14;
|
|
|
1683
1703
|
var barCompare = (beat, ctx) => {
|
|
1684
1704
|
const { sid, theme } = ctx;
|
|
1685
1705
|
const p = beat.params;
|
|
1686
|
-
const
|
|
1706
|
+
const count2 = p.bars.length;
|
|
1687
1707
|
const W = contentW(ctx.format);
|
|
1688
1708
|
const face = faceOf(ctx.theme.fontStack);
|
|
1689
1709
|
const tall = isPortrait(ctx.format);
|
|
@@ -1697,7 +1717,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1697
1717
|
0,
|
|
1698
1718
|
face
|
|
1699
1719
|
);
|
|
1700
|
-
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (
|
|
1720
|
+
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count2 + GAP_RATIO * (count2 - 1)));
|
|
1701
1721
|
let pitch = bar * (1 + GAP_RATIO);
|
|
1702
1722
|
const gutterMax = tall ? 0 : share(GUTTER_MAX, W);
|
|
1703
1723
|
const gutterInner = tall ? W : gutterMax - GUTTER_PAD;
|
|
@@ -1719,7 +1739,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1719
1739
|
if (tall) {
|
|
1720
1740
|
bar = Math.min(
|
|
1721
1741
|
BAR_MAX,
|
|
1722
|
-
(avail - unitBand - FOOT -
|
|
1742
|
+
(avail - unitBand - FOOT - count2 * head) / (count2 + GAP_RATIO * (count2 - 1))
|
|
1723
1743
|
);
|
|
1724
1744
|
pitch = head + bar * (1 + GAP_RATIO);
|
|
1725
1745
|
} else {
|
|
@@ -1730,7 +1750,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1730
1750
|
}
|
|
1731
1751
|
}
|
|
1732
1752
|
const valueSize = Math.max(MIN_FONT, Math.min(VALUE_MAX, bar * 0.9));
|
|
1733
|
-
const barsH = (
|
|
1753
|
+
const barsH = (count2 - 1) * pitch + head + bar;
|
|
1734
1754
|
const foot = Math.max(
|
|
1735
1755
|
0,
|
|
1736
1756
|
Math.ceil(
|
|
@@ -1743,7 +1763,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1743
1763
|
const H = barsH + unitBand + foot;
|
|
1744
1764
|
if (H > avail) {
|
|
1745
1765
|
throw new Error(
|
|
1746
|
-
`bar-compare ${beat.id}: ${
|
|
1766
|
+
`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.`
|
|
1747
1767
|
);
|
|
1748
1768
|
}
|
|
1749
1769
|
const gutter = tall ? 0 : Math.min(
|
|
@@ -1896,7 +1916,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1896
1916
|
</div>${note}`;
|
|
1897
1917
|
const railsAt = 0.6;
|
|
1898
1918
|
const barsAt = 0.95;
|
|
1899
|
-
const step = Math.min(0.4, 2.4 /
|
|
1919
|
+
const step = Math.min(0.4, 2.4 / count2);
|
|
1900
1920
|
const grow = 0.85;
|
|
1901
1921
|
const tl = [
|
|
1902
1922
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
@@ -1946,11 +1966,11 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1946
1966
|
);
|
|
1947
1967
|
}
|
|
1948
1968
|
}
|
|
1949
|
-
const settled = barsAt + (
|
|
1969
|
+
const settled = barsAt + (count2 - 1) * step + grow + 0.05;
|
|
1950
1970
|
const holds = [settled + 0.2];
|
|
1951
1971
|
const focal = rows.reduce((best, r) => Math.abs(r.value) > Math.abs(best.value) ? r : best);
|
|
1952
1972
|
const spot = spotlighter(sid, ".bc-bar");
|
|
1953
|
-
if (
|
|
1973
|
+
if (count2 > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
|
|
1954
1974
|
const tailAt = settled + 0.3;
|
|
1955
1975
|
if (p.unit) {
|
|
1956
1976
|
tl.push(tween(`#${id(sid, "unit")}`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, tailAt));
|
|
@@ -2443,6 +2463,90 @@ var dataTable = (beat, ctx) => {
|
|
|
2443
2463
|
};
|
|
2444
2464
|
};
|
|
2445
2465
|
|
|
2466
|
+
// src/emit/tex.ts
|
|
2467
|
+
import katex from "katex";
|
|
2468
|
+
var UNFIT_ATTR = "data-ds-unfit";
|
|
2469
|
+
var trust = (c) => c.command === "\\htmlClass";
|
|
2470
|
+
function texError(tex, display) {
|
|
2471
|
+
try {
|
|
2472
|
+
katex.renderToString(tex, {
|
|
2473
|
+
displayMode: display,
|
|
2474
|
+
throwOnError: true,
|
|
2475
|
+
strict: false,
|
|
2476
|
+
trust,
|
|
2477
|
+
output: "html"
|
|
2478
|
+
});
|
|
2479
|
+
return null;
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
return err instanceof Error ? err.message : String(err);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
var LEFT = /\\left(?![a-zA-Z])/g;
|
|
2485
|
+
var RIGHT = /\\right(?![a-zA-Z])/g;
|
|
2486
|
+
var NO_INK = /\\label\{[^{}]*\}|\\nonumber(?![a-zA-Z])|\\notag(?![a-zA-Z])/g;
|
|
2487
|
+
function braceBalance(tex) {
|
|
2488
|
+
let depth = 0;
|
|
2489
|
+
for (let i = 0; i < tex.length; i++) {
|
|
2490
|
+
const c = tex[i];
|
|
2491
|
+
if (c === "\\") {
|
|
2492
|
+
i++;
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
if (c === "{") depth++;
|
|
2496
|
+
else if (c === "}") depth--;
|
|
2497
|
+
}
|
|
2498
|
+
return depth;
|
|
2499
|
+
}
|
|
2500
|
+
var count = (tex, re) => (tex.match(re) ?? []).length;
|
|
2501
|
+
function repairTex(input, display) {
|
|
2502
|
+
const repairs = [];
|
|
2503
|
+
let tex = input;
|
|
2504
|
+
let error = texError(tex, display);
|
|
2505
|
+
if (error === null) return { tex, repairs, error };
|
|
2506
|
+
const gone = tex.match(NO_INK) ?? [];
|
|
2507
|
+
if (gone.length) {
|
|
2508
|
+
tex = tex.replace(NO_INK, "").trim();
|
|
2509
|
+
repairs.push(`removed ${gone.join(", ")}, which draw nothing`);
|
|
2510
|
+
error = texError(tex, display);
|
|
2511
|
+
}
|
|
2512
|
+
const lefts = count(tex, LEFT);
|
|
2513
|
+
const rights = count(tex, RIGHT);
|
|
2514
|
+
if (error !== null && lefts !== rights) {
|
|
2515
|
+
const n3 = Math.abs(lefts - rights);
|
|
2516
|
+
tex = lefts > rights ? `${tex} ${"\\right.".repeat(n3)}` : `${"\\left.".repeat(n3)} ${tex}`;
|
|
2517
|
+
repairs.push(
|
|
2518
|
+
`${lefts} \\left against ${rights} \\right: added ${n3} invisible ${lefts > rights ? "\\right." : "\\left."} to balance them`
|
|
2519
|
+
);
|
|
2520
|
+
error = texError(tex, display);
|
|
2521
|
+
}
|
|
2522
|
+
const depth = braceBalance(tex);
|
|
2523
|
+
if (error !== null && depth !== 0) {
|
|
2524
|
+
const n3 = Math.abs(depth);
|
|
2525
|
+
tex = depth > 0 ? `${tex}${"}".repeat(n3)}` : `${"{".repeat(n3)}${tex}`;
|
|
2526
|
+
repairs.push(
|
|
2527
|
+
`${n3} unclosed brace group(s): ${depth > 0 ? "closed at the end" : "opened at the start"}`
|
|
2528
|
+
);
|
|
2529
|
+
error = texError(tex, display);
|
|
2530
|
+
}
|
|
2531
|
+
const undefinedNames = [];
|
|
2532
|
+
for (let round6 = 0; error !== null && round6 < 12; round6++) {
|
|
2533
|
+
const m = /Undefined control sequence: \\([a-zA-Z]+)/.exec(error);
|
|
2534
|
+
if (!m) break;
|
|
2535
|
+
const name = m[1];
|
|
2536
|
+
const next = tex.replace(new RegExp(`\\\\${name}(?![a-zA-Z])`, "g"), `\\operatorname{${name}}`);
|
|
2537
|
+
if (next === tex) break;
|
|
2538
|
+
tex = next;
|
|
2539
|
+
undefinedNames.push(`\\${name}`);
|
|
2540
|
+
error = texError(tex, display);
|
|
2541
|
+
}
|
|
2542
|
+
if (undefinedNames.length) {
|
|
2543
|
+
repairs.push(
|
|
2544
|
+
`${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`
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
return { tex, repairs, error };
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2446
2550
|
// src/emit/archetypes/equation-walk.ts
|
|
2447
2551
|
var TRUST = 'function (c) { return c.command === "\\\\htmlClass"; }';
|
|
2448
2552
|
var OPTS = `{ displayMode: true, trust: ${TRUST}, strict: false, output: "html" }`;
|
|
@@ -2457,6 +2561,9 @@ var SYNONYMS = [
|
|
|
2457
2561
|
function normalise(tex) {
|
|
2458
2562
|
let out = "";
|
|
2459
2563
|
const map = [];
|
|
2564
|
+
const ends = [];
|
|
2565
|
+
const lead = [];
|
|
2566
|
+
let hint = null;
|
|
2460
2567
|
for (let i = 0; i < tex.length; ) {
|
|
2461
2568
|
if (/\s/.test(tex[i])) {
|
|
2462
2569
|
i++;
|
|
@@ -2468,19 +2575,19 @@ function normalise(tex) {
|
|
|
2468
2575
|
const m = new RegExp(`^(?:${re.source})`).exec(rest);
|
|
2469
2576
|
return m ? { len: m[0].length, to } : null;
|
|
2470
2577
|
}).find(Boolean);
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
i
|
|
2477
|
-
|
|
2578
|
+
const len = hit ? hit.len : 1;
|
|
2579
|
+
const chars = hit ? hit.to : tex[i];
|
|
2580
|
+
if (chars === "") hint ??= i;
|
|
2581
|
+
for (const ch of chars) {
|
|
2582
|
+
out += ch;
|
|
2583
|
+
map.push(i);
|
|
2584
|
+
ends.push(i + len);
|
|
2585
|
+
lead.push(hint ?? i);
|
|
2586
|
+
hint = null;
|
|
2478
2587
|
}
|
|
2479
|
-
|
|
2480
|
-
map.push(i);
|
|
2481
|
-
i++;
|
|
2588
|
+
i += len;
|
|
2482
2589
|
}
|
|
2483
|
-
return { text: out, map };
|
|
2590
|
+
return { text: out, map, ends, lead };
|
|
2484
2591
|
}
|
|
2485
2592
|
function locate(tex, term) {
|
|
2486
2593
|
const hay = normalise(tex);
|
|
@@ -2488,58 +2595,131 @@ function locate(tex, term) {
|
|
|
2488
2595
|
if (needle === "") return null;
|
|
2489
2596
|
const at = hay.text.indexOf(needle);
|
|
2490
2597
|
if (at < 0) return null;
|
|
2491
|
-
const start = hay.
|
|
2492
|
-
const
|
|
2493
|
-
const lastOrig = hay.map[lastNorm];
|
|
2494
|
-
let end = lastOrig + 1;
|
|
2495
|
-
while (end < tex.length && (hay.map[lastNorm + 1] ?? tex.length) > end && /\s/.test(tex[end])) {
|
|
2496
|
-
end++;
|
|
2497
|
-
}
|
|
2498
|
-
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
2598
|
+
const start = hay.lead[at];
|
|
2599
|
+
const end = hay.ends[at + needle.length - 1];
|
|
2499
2600
|
return { start, end: Math.min(end, tex.length) };
|
|
2500
2601
|
}
|
|
2501
2602
|
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
2603
|
+
const checked = texError(tex, true) === null;
|
|
2502
2604
|
let parts = [{ text: tex, raw: true }];
|
|
2503
2605
|
const used = [];
|
|
2504
2606
|
const missing = [];
|
|
2607
|
+
const broken = [];
|
|
2608
|
+
const joined = (ps) => ps.map((p) => p.text).join("");
|
|
2505
2609
|
for (const term of terms) {
|
|
2506
2610
|
let placed = false;
|
|
2611
|
+
let found = false;
|
|
2507
2612
|
for (let i = 0; i < parts.length && !placed; i++) {
|
|
2508
2613
|
const part = parts[i];
|
|
2509
2614
|
if (!part.raw) continue;
|
|
2510
2615
|
const at = locate(part.text, term.tex);
|
|
2511
2616
|
if (!at) continue;
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
raw:
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2617
|
+
found = true;
|
|
2618
|
+
const body = `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`;
|
|
2619
|
+
for (const text2 of [body, `{${body}}`]) {
|
|
2620
|
+
const next = parts.toSpliced(
|
|
2621
|
+
i,
|
|
2622
|
+
1,
|
|
2623
|
+
{ text: part.text.slice(0, at.start), raw: true },
|
|
2624
|
+
{ text: text2, raw: false },
|
|
2625
|
+
{ text: part.text.slice(at.end), raw: true }
|
|
2626
|
+
);
|
|
2627
|
+
if (checked && texError(joined(next), true) !== null) continue;
|
|
2628
|
+
parts = next;
|
|
2629
|
+
used.push(term);
|
|
2630
|
+
placed = true;
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (!placed) {
|
|
2635
|
+
missing.push(term);
|
|
2636
|
+
if (found) broken.push(term);
|
|
2524
2637
|
}
|
|
2525
|
-
if (!placed) missing.push(term);
|
|
2526
2638
|
}
|
|
2527
2639
|
if (used.length === 0) {
|
|
2528
2640
|
throw new Error(
|
|
2529
|
-
`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)}`
|
|
2641
|
+
`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)}`
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
return { tex: joined(parts), used, missing, broken };
|
|
2645
|
+
}
|
|
2646
|
+
function droppedTerms(eqId, missing, broken) {
|
|
2647
|
+
return missing.map(
|
|
2648
|
+
(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`
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
function prepareWalk(eq, terms, beatId) {
|
|
2652
|
+
const fixed = repairTex(eq.tex, true);
|
|
2653
|
+
const notes = fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`);
|
|
2654
|
+
const repairedTerms = terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
2655
|
+
if (fixed.error !== null) {
|
|
2656
|
+
notes.push(
|
|
2657
|
+
`${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)}`
|
|
2530
2658
|
);
|
|
2659
|
+
const parts = segment(eq.tex, [...terms]);
|
|
2660
|
+
if (parts.used.length === 0) {
|
|
2661
|
+
throw new Error(
|
|
2662
|
+
`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)}`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
notes.push(...droppedTerms(eq.id, parts.missing, []));
|
|
2666
|
+
return {
|
|
2667
|
+
raw: eq.tex,
|
|
2668
|
+
plain: { error: fixed.error },
|
|
2669
|
+
used: parts.used,
|
|
2670
|
+
missing: parts.missing,
|
|
2671
|
+
notes
|
|
2672
|
+
};
|
|
2531
2673
|
}
|
|
2532
|
-
|
|
2674
|
+
const walk = wrapTerms(fixed.tex, repairedTerms, beatId);
|
|
2675
|
+
const back = (ts) => ts.map((t2) => terms[repairedTerms.indexOf(t2)]);
|
|
2676
|
+
const used = back(walk.used);
|
|
2677
|
+
const missing = back(walk.missing);
|
|
2678
|
+
notes.push(...droppedTerms(eq.id, missing, back(walk.broken)));
|
|
2679
|
+
return { tex: walk.tex, raw: fixed.tex, used, missing, notes };
|
|
2680
|
+
}
|
|
2681
|
+
function segment(tex, terms) {
|
|
2682
|
+
let parts = [{ text: tex }];
|
|
2683
|
+
const used = [];
|
|
2684
|
+
const missing = [];
|
|
2685
|
+
for (const term of terms) {
|
|
2686
|
+
const i = parts.findIndex((p) => !p.term && locate(p.text, term.tex));
|
|
2687
|
+
const part = parts[i];
|
|
2688
|
+
const at = part && locate(part.text, term.tex);
|
|
2689
|
+
if (!part || !at) {
|
|
2690
|
+
missing.push(term);
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
parts = parts.toSpliced(
|
|
2694
|
+
i,
|
|
2695
|
+
1,
|
|
2696
|
+
{ text: part.text.slice(0, at.start) },
|
|
2697
|
+
{ text: part.text.slice(at.start, at.end), term },
|
|
2698
|
+
{ text: part.text.slice(at.end) }
|
|
2699
|
+
);
|
|
2700
|
+
used.push(term);
|
|
2701
|
+
}
|
|
2702
|
+
return { parts, used, missing };
|
|
2703
|
+
}
|
|
2704
|
+
function plainHtml(tex, terms) {
|
|
2705
|
+
return segment(tex, terms).parts.map(
|
|
2706
|
+
(p) => p.term ? `<span class="term t-${p.term.tone}">${esc(p.text)}</span>` : esc(p.text)
|
|
2707
|
+
).join("");
|
|
2708
|
+
}
|
|
2709
|
+
function chipSetup(sid, term) {
|
|
2710
|
+
const fixed = repairTex(term.tex, false);
|
|
2711
|
+
const el = `document.getElementById("${sid}-chip-${term.tone}")`;
|
|
2712
|
+
return fixed.error === null ? `katex.render('${js(fixed.tex)}', ${el}, ${INLINE_OPTS});` : `${el}.textContent = '${js(term.tex)}';`;
|
|
2533
2713
|
}
|
|
2534
2714
|
function equationSize(tex) {
|
|
2535
2715
|
return tex.length > 120 ? 68 : tex.length > 90 ? 80 : tex.length > 55 ? 92 : 108;
|
|
2536
2716
|
}
|
|
2537
|
-
var
|
|
2717
|
+
var NO_INK2 = /\\(math[a-z]+|text[a-z]*|bm|boldsymbol|displaystyle|textstyle|scriptstyle|left|right|operatorname|limits|nolimits|htmlClass|hspace|phantom|[,;:!])/g;
|
|
2538
2718
|
var GLYPH_EM = 0.9;
|
|
2539
2719
|
function texUnits(tex) {
|
|
2540
2720
|
let ems = 0;
|
|
2541
2721
|
for (const m of tex.matchAll(/\\qquad|\\quad/g)) ems += m[0] === "\\qquad" ? 2 : 1;
|
|
2542
|
-
const glyphs = tex.replace(/\\q?quad/g, "").replace(
|
|
2722
|
+
const glyphs = tex.replace(/\\q?quad/g, "").replace(NO_INK2, "").replace(/[{}\s_^]/g, "");
|
|
2543
2723
|
return ems + glyphs.length * GLYPH_EM;
|
|
2544
2724
|
}
|
|
2545
2725
|
function statements(tex, stacked) {
|
|
@@ -2547,6 +2727,100 @@ function statements(tex, stacked) {
|
|
|
2547
2727
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
2548
2728
|
return parts.length > 0 ? parts : [tex];
|
|
2549
2729
|
}
|
|
2730
|
+
var PLAIN_FONT = 48;
|
|
2731
|
+
function mathFit(sid, targets, shown, raw2, want, floor) {
|
|
2732
|
+
return [
|
|
2733
|
+
`var dsEqFit = (function () {
|
|
2734
|
+
var box = document.getElementById("${sid}-eq");
|
|
2735
|
+
var slide = box && box.parentNode;
|
|
2736
|
+
var bw = box ? box.getBoundingClientRect().width : 0;
|
|
2737
|
+
if (!(bw > 0) || !slide) return "unmeasured";
|
|
2738
|
+
var targets = [${targets.map((id2) => `"${id2}"`).join(", ")}].map(function (id) { return document.getElementById(id); });
|
|
2739
|
+
var TEX = [${shown.map((s) => `'${js(s)}'`).join(", ")}];
|
|
2740
|
+
var spill0 = slide.scrollHeight - slide.clientHeight;
|
|
2741
|
+
function extent(each) {
|
|
2742
|
+
var w = 0;
|
|
2743
|
+
targets.forEach(function (t) {
|
|
2744
|
+
var lo = Infinity, hi = -Infinity;
|
|
2745
|
+
Array.prototype.forEach.call(t.querySelectorAll(".katex-html > .base"), function (b) {
|
|
2746
|
+
var r = b.getBoundingClientRect();
|
|
2747
|
+
if (each) w = Math.max(w, r.width);
|
|
2748
|
+
else { lo = Math.min(lo, r.left); hi = Math.max(hi, r.right); }
|
|
2749
|
+
});
|
|
2750
|
+
if (!each && hi > lo) w = Math.max(w, hi - lo);
|
|
2751
|
+
});
|
|
2752
|
+
return w;
|
|
2753
|
+
}
|
|
2754
|
+
function fits(each) {
|
|
2755
|
+
return extent(each) <= bw + 0.5 && slide.scrollHeight - slide.clientHeight <= Math.max(1, spill0 + 1);
|
|
2756
|
+
}
|
|
2757
|
+
function settle(each, size) {
|
|
2758
|
+
size = Math.max(${floor}, Math.min(${want}, Math.floor(size)));
|
|
2759
|
+
box.style.fontSize = size + "px";
|
|
2760
|
+
while (!fits(each) && size > ${floor}) {
|
|
2761
|
+
size = Math.max(${floor}, size - 2);
|
|
2762
|
+
box.style.fontSize = size + "px";
|
|
2763
|
+
}
|
|
2764
|
+
return fits(each);
|
|
2765
|
+
}
|
|
2766
|
+
if (fits(false)) return "fits";
|
|
2767
|
+
var size = parseFloat(box.style.fontSize);
|
|
2768
|
+
if (settle(false, (size * bw) / extent(false))) return "scaled";
|
|
2769
|
+
targets.forEach(function (t, i) { katex.render("\\\\displaystyle " + TEX[i], t, ${INLINE_OPTS}); });
|
|
2770
|
+
box.className += " eq-broken";
|
|
2771
|
+
box.style.fontSize = "${want}px";
|
|
2772
|
+
var wb = extent(true);
|
|
2773
|
+
if (settle(true, wb > bw ? (${want} * bw) / wb : ${want})) return "broken";
|
|
2774
|
+
box.setAttribute("${UNFIT_ATTR}", '${js(raw2)}');
|
|
2775
|
+
return "unfit";
|
|
2776
|
+
})()`
|
|
2777
|
+
];
|
|
2778
|
+
}
|
|
2779
|
+
function chipFit(sid, terms) {
|
|
2780
|
+
if (terms.length === 0) return [];
|
|
2781
|
+
return [
|
|
2782
|
+
`[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2783
|
+
var chip = document.getElementById("${sid}-chip-" + tone);
|
|
2784
|
+
var r = chip && chip.getBoundingClientRect();
|
|
2785
|
+
if (!r || !(r.height > 0)) return;
|
|
2786
|
+
var k = chip.offsetHeight / r.height;
|
|
2787
|
+
var top = r.top, bottom = r.bottom;
|
|
2788
|
+
Array.prototype.forEach.call(chip.querySelectorAll("span"), function (el) {
|
|
2789
|
+
for (var n = el.firstChild; n; n = n.nextSibling) {
|
|
2790
|
+
if (n.nodeType === 3 && n.nodeValue.trim()) {
|
|
2791
|
+
var q = el.getBoundingClientRect();
|
|
2792
|
+
top = Math.min(top, q.top);
|
|
2793
|
+
bottom = Math.max(bottom, q.bottom);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
var cs = getComputedStyle(chip);
|
|
2799
|
+
if (top < r.top) chip.style.paddingTop = parseFloat(cs.paddingTop) + Math.ceil((r.top - top) * k) + "px";
|
|
2800
|
+
if (bottom > r.bottom) chip.style.paddingBottom = parseFloat(cs.paddingBottom) + Math.ceil((bottom - r.bottom) * k) + "px";
|
|
2801
|
+
})`
|
|
2802
|
+
];
|
|
2803
|
+
}
|
|
2804
|
+
var SWELL = 1.16;
|
|
2805
|
+
var SWELL_EM = 0.06;
|
|
2806
|
+
function swellFit(sid, terms) {
|
|
2807
|
+
return [
|
|
2808
|
+
`var dsSwell = (function () {
|
|
2809
|
+
var out = {};
|
|
2810
|
+
var box = document.getElementById("${sid}-eq");
|
|
2811
|
+
var k = box && box.offsetWidth ? box.getBoundingClientRect().width / box.offsetWidth : 1;
|
|
2812
|
+
var em = box ? parseFloat(getComputedStyle(box).fontSize) : 0;
|
|
2813
|
+
[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2814
|
+
var w = 0;
|
|
2815
|
+
Array.prototype.forEach.call(document.querySelectorAll("#${sid} .t-" + tone), function (el) {
|
|
2816
|
+
w = Math.max(w, el.getBoundingClientRect().width / k);
|
|
2817
|
+
});
|
|
2818
|
+
out[tone] = w > 0 && em > 0 ? Math.min(${SWELL}, Math.round((1 + (2 * ${SWELL_EM} * em) / w) * 1000) / 1000) : ${SWELL};
|
|
2819
|
+
});
|
|
2820
|
+
return out;
|
|
2821
|
+
})()`
|
|
2822
|
+
];
|
|
2823
|
+
}
|
|
2550
2824
|
function legendRows(sid, terms, theme) {
|
|
2551
2825
|
return terms.map(
|
|
2552
2826
|
(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>`
|
|
@@ -2562,7 +2836,10 @@ function legendCss(theme) {
|
|
|
2562
2836
|
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
2563
2837
|
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
2564
2838
|
// are one symbol each and their natural widths differ by a few pixels.
|
|
2565
|
-
|
|
2839
|
+
// `flex:none`: beside a long label the chip used to SHRINK below its own
|
|
2840
|
+
// nowrap TeX, which then ran out of the painted box into the label — on a
|
|
2841
|
+
// HypePaper deck, "pixelcoord" 91px past the chip's right edge.
|
|
2842
|
+
`.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}`
|
|
2566
2843
|
].join("\n");
|
|
2567
2844
|
}
|
|
2568
2845
|
var equationWalk = (beat, ctx) => {
|
|
@@ -2574,37 +2851,53 @@ var equationWalk = (beat, ctx) => {
|
|
|
2574
2851
|
`equation-walk ${beat.id}: no equation "${p.equationId}" in source ${ctx.source.id}`
|
|
2575
2852
|
);
|
|
2576
2853
|
}
|
|
2577
|
-
const walk =
|
|
2854
|
+
const walk = prepareWalk(eq, p.terms, beat.id);
|
|
2578
2855
|
const terms = walk.used;
|
|
2579
2856
|
const legend = legendRows(sid, terms, theme);
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2857
|
+
const chips = terms.map((t2) => chipSetup(sid, t2));
|
|
2858
|
+
let eqHtml;
|
|
2859
|
+
let setup;
|
|
2860
|
+
let measure4;
|
|
2861
|
+
if (walk.plain) {
|
|
2862
|
+
eqHtml = `<div class="eq eq-plain" id="${sid}-eq" style="font-size:${PLAIN_FONT}px">${plainHtml(eq.tex, terms)}</div>`;
|
|
2863
|
+
setup = chips;
|
|
2864
|
+
measure4 = [...chipFit(sid, terms), ...swellFit(sid, terms)];
|
|
2865
|
+
} else {
|
|
2866
|
+
const wrapped = walk.tex;
|
|
2867
|
+
const split = statements(wrapped, isPortrait(ctx.format));
|
|
2868
|
+
const stacked = split.length > 1 && split.every((s) => texError(s, true) === null);
|
|
2869
|
+
const raw2 = statements(walk.raw, stacked);
|
|
2870
|
+
const shown = stacked ? split : [wrapped];
|
|
2871
|
+
const longest = raw2.reduce((a, b) => b.length > a.length ? b : a);
|
|
2872
|
+
const want = equationSize(longest);
|
|
2873
|
+
const size = Math.max(
|
|
2874
|
+
MIN_FONT,
|
|
2875
|
+
Math.min(want, Math.floor(contentW(ctx.format) / Math.max(...raw2.map(texUnits))))
|
|
2876
|
+
);
|
|
2877
|
+
const body = shown.length === 1 ? "" : shown.map((_, i) => `<div id="${sid}-eq${i}"></div>`).join("\n ");
|
|
2878
|
+
eqHtml = `<div class="eq${shown.length === 1 ? "" : " eqstack"}" id="${sid}-eq" style="font-size:${size}px">${body}</div>`;
|
|
2879
|
+
const targets = shown.map((_, i) => `${sid}-eq${shown.length === 1 ? "" : i}`);
|
|
2880
|
+
setup = [
|
|
2881
|
+
`var OPTS = ${OPTS};`,
|
|
2882
|
+
...shown.map(
|
|
2883
|
+
(part, i) => `katex.render('${js(part)}', document.getElementById("${targets[i]}"), OPTS);`
|
|
2884
|
+
),
|
|
2885
|
+
...chips
|
|
2886
|
+
];
|
|
2887
|
+
measure4 = [
|
|
2888
|
+
...chipFit(sid, terms),
|
|
2889
|
+
...mathFit(sid, targets, shown, walk.raw, want, MIN_FONT),
|
|
2890
|
+
// Last: it measures each term at the size the fit settled on.
|
|
2891
|
+
...swellFit(sid, terms)
|
|
2892
|
+
];
|
|
2893
|
+
}
|
|
2592
2894
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
2593
2895
|
<div class="eqslide">
|
|
2594
|
-
|
|
2896
|
+
${eqHtml}
|
|
2595
2897
|
<div class="legend">
|
|
2596
2898
|
${legend}
|
|
2597
2899
|
</div>
|
|
2598
2900
|
</div>`;
|
|
2599
|
-
const setup = [
|
|
2600
|
-
`var OPTS = ${OPTS};`,
|
|
2601
|
-
...shown.map(
|
|
2602
|
-
(part, i) => `katex.render('${js(part)}', document.getElementById("${sid}-eq${shown.length === 1 ? "" : i}"), OPTS);`
|
|
2603
|
-
),
|
|
2604
|
-
...terms.map(
|
|
2605
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2606
|
-
)
|
|
2607
|
-
];
|
|
2608
2901
|
const tl = [
|
|
2609
2902
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
2610
2903
|
tween(`#${sid}-eq`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8)
|
|
@@ -2616,6 +2909,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2616
2909
|
);
|
|
2617
2910
|
const holds = [];
|
|
2618
2911
|
const spot = spotlighter(sid, ".term");
|
|
2912
|
+
const swell = (t2) => raw(`dsSwell.${t2.tone}`);
|
|
2619
2913
|
terms.forEach((term, i) => {
|
|
2620
2914
|
const at = first + i * step;
|
|
2621
2915
|
const colour = theme.tones[term.tone];
|
|
@@ -2632,13 +2926,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2632
2926
|
tween(
|
|
2633
2927
|
`#${sid} .t-${term.tone}`,
|
|
2634
2928
|
{ color: theme.fg, scale: 1 },
|
|
2635
|
-
{ color: colour, scale:
|
|
2929
|
+
{ color: colour, scale: swell(term), duration: 0.5 },
|
|
2636
2930
|
at
|
|
2637
2931
|
)
|
|
2638
2932
|
);
|
|
2639
2933
|
const prev = terms[i - 1];
|
|
2640
2934
|
if (prev) {
|
|
2641
|
-
tl.push(
|
|
2935
|
+
tl.push(
|
|
2936
|
+
tween(`#${sid} .t-${prev.tone}`, { scale: swell(prev) }, { scale: 1, duration: 0.4 }, at)
|
|
2937
|
+
);
|
|
2642
2938
|
}
|
|
2643
2939
|
holds.push(at + 0.6);
|
|
2644
2940
|
});
|
|
@@ -2646,7 +2942,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2646
2942
|
if (last) {
|
|
2647
2943
|
const at = first + terms.length * step;
|
|
2648
2944
|
tl.push(
|
|
2649
|
-
tween(`#${sid} .t-${last.tone}`, { scale:
|
|
2945
|
+
tween(`#${sid} .t-${last.tone}`, { scale: swell(last) }, { scale: 1, duration: 0.4 }, at),
|
|
2650
2946
|
...spot.restore(at)
|
|
2651
2947
|
);
|
|
2652
2948
|
}
|
|
@@ -2654,6 +2950,8 @@ var equationWalk = (beat, ctx) => {
|
|
|
2654
2950
|
html,
|
|
2655
2951
|
tl,
|
|
2656
2952
|
setup,
|
|
2953
|
+
measure: measure4,
|
|
2954
|
+
...walk.notes.length ? { warnings: walk.notes } : {},
|
|
2657
2955
|
holds: holdsWithin(holds, beat.seconds),
|
|
2658
2956
|
css: [
|
|
2659
2957
|
chromeCss(theme),
|
|
@@ -2673,6 +2971,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2673
2971
|
// has its margin zeroed above, so without an explicit gap the two lines
|
|
2674
2972
|
// would touch and read as one equation broken mid-expression.
|
|
2675
2973
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
2974
|
+
// Set by `mathFit` when one line will not fit at the floor: the display is
|
|
2975
|
+
// re-rendered inline, where KaTeX breaks after a top-level relation or
|
|
2976
|
+
// operator, and the lines it breaks into are centred and spaced as lines.
|
|
2977
|
+
".eq-broken{line-height:1.5}",
|
|
2978
|
+
// An equation KaTeX refuses, shown as its source. Wraps anywhere, because
|
|
2979
|
+
// TeX source has few spaces and must not run off the slide either. The
|
|
2980
|
+
// deck's own face, not a monospace one: a family the bundle does not
|
|
2981
|
+
// declare falls back silently (invariant 9).
|
|
2982
|
+
".eq-plain{white-space:pre-wrap;overflow-wrap:anywhere}",
|
|
2676
2983
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
2677
2984
|
".term{display:inline-block}",
|
|
2678
2985
|
legendCss(theme),
|
|
@@ -2687,26 +2994,67 @@ var equationWalk = (beat, ctx) => {
|
|
|
2687
2994
|
|
|
2688
2995
|
// src/emit/archetypes/equation-morph.ts
|
|
2689
2996
|
var MORPH_SECONDS = 1.6;
|
|
2690
|
-
|
|
2691
|
-
const { sid, theme } = ctx;
|
|
2997
|
+
function prepareMorph(beat, source) {
|
|
2692
2998
|
const p = beat.params;
|
|
2693
2999
|
const find3 = (id2) => {
|
|
2694
|
-
const eq =
|
|
3000
|
+
const eq = source.equations.find((e) => e.id === id2);
|
|
2695
3001
|
if (!eq)
|
|
2696
|
-
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${
|
|
3002
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${source.id}`);
|
|
2697
3003
|
return eq;
|
|
2698
3004
|
};
|
|
2699
|
-
const
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
3005
|
+
const notes = [];
|
|
3006
|
+
const drawable = (eq) => {
|
|
3007
|
+
const fixed = repairTex(eq.tex, true);
|
|
3008
|
+
if (fixed.error !== null) {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
`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)}`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
notes.push(...fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`));
|
|
3014
|
+
return { id: eq.id, tex: fixed.tex };
|
|
3015
|
+
};
|
|
3016
|
+
const a = drawable(find3(p.fromId));
|
|
3017
|
+
const b = drawable(find3(p.toId));
|
|
3018
|
+
const repaired = p.terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
3019
|
+
const original = (t2) => p.terms[repaired.indexOf(t2)];
|
|
3020
|
+
let keyed = repaired.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
3021
|
+
if (keyed.length === 0) {
|
|
2703
3022
|
throw new Error(
|
|
2704
3023
|
`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)}`
|
|
2705
3024
|
);
|
|
2706
3025
|
}
|
|
3026
|
+
notes.push(
|
|
3027
|
+
...repaired.filter((t2) => !keyed.includes(t2)).map(
|
|
3028
|
+
(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`
|
|
3029
|
+
)
|
|
3030
|
+
);
|
|
2707
3031
|
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
2708
|
-
|
|
2709
|
-
|
|
3032
|
+
let wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
3033
|
+
let wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
3034
|
+
const paired = keyed.filter((t2) => wa.used.includes(t2) && wb.used.includes(t2));
|
|
3035
|
+
if (paired.length < keyed.length) {
|
|
3036
|
+
const unpaired = keyed.filter((t2) => !paired.includes(t2));
|
|
3037
|
+
notes.push(
|
|
3038
|
+
...unpaired.map(
|
|
3039
|
+
(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`
|
|
3040
|
+
)
|
|
3041
|
+
);
|
|
3042
|
+
if (paired.length === 0) {
|
|
3043
|
+
throw new Error(
|
|
3044
|
+
`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(", ")}`
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
keyed = paired;
|
|
3048
|
+
wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
3049
|
+
wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
3050
|
+
}
|
|
3051
|
+
const both = keyed.map(original);
|
|
3052
|
+
return { a, b, wa: wa.tex, wb: wb.tex, both, notes };
|
|
3053
|
+
}
|
|
3054
|
+
var equationMorph = (beat, ctx) => {
|
|
3055
|
+
const { sid, theme } = ctx;
|
|
3056
|
+
const p = beat.params;
|
|
3057
|
+
const { a, b, wa, wb, both, notes } = prepareMorph(beat, ctx.source);
|
|
2710
3058
|
const size = Math.max(
|
|
2711
3059
|
MIN_FONT,
|
|
2712
3060
|
Math.min(
|
|
@@ -2728,9 +3076,7 @@ var equationMorph = (beat, ctx) => {
|
|
|
2728
3076
|
`var OPTS = ${OPTS};`,
|
|
2729
3077
|
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
2730
3078
|
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
2731
|
-
...both.map(
|
|
2732
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2733
|
-
)
|
|
3079
|
+
...both.map((t2) => chipSetup(sid, t2))
|
|
2734
3080
|
];
|
|
2735
3081
|
const first = 1.8;
|
|
2736
3082
|
const at = Math.round(
|
|
@@ -2763,8 +3109,11 @@ var equationMorph = (beat, ctx) => {
|
|
|
2763
3109
|
setup,
|
|
2764
3110
|
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
2765
3111
|
// the ready gate, and the plugin tween above finds it on the host.
|
|
2766
|
-
|
|
3112
|
+
// The chips are fitted before the plan is built: growing one moves the
|
|
3113
|
+
// legend, and nothing the morph measures may move after it has measured.
|
|
3114
|
+
measure: [...chipFit(sid, both), `DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
2767
3115
|
plugins: ["dsMorph"],
|
|
3116
|
+
...notes.length ? { warnings: notes } : {},
|
|
2768
3117
|
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
2769
3118
|
css: [
|
|
2770
3119
|
chromeCss(theme),
|
|
@@ -3345,16 +3694,16 @@ var lineChart = (beat, ctx) => {
|
|
|
3345
3694
|
</svg>${readout}
|
|
3346
3695
|
</div>`;
|
|
3347
3696
|
const draw2 = 0.8;
|
|
3348
|
-
const
|
|
3349
|
-
const idealStep = Math.min(0.45, DRAW_SECONDS /
|
|
3697
|
+
const count2 = p.points.length;
|
|
3698
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count2);
|
|
3350
3699
|
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
3351
|
-
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length,
|
|
3700
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count2 - 1) : 0;
|
|
3352
3701
|
const tailAfter = (s) => Math.max(
|
|
3353
|
-
s *
|
|
3702
|
+
s * count2 + 0.4,
|
|
3354
3703
|
// the ring: its walk, then the 0.4s it takes to leave
|
|
3355
|
-
0.3 + s * (
|
|
3704
|
+
0.3 + s * (count2 - 1),
|
|
3356
3705
|
// the dots
|
|
3357
|
-
0.5 + s * (
|
|
3706
|
+
0.5 + s * (count2 - 1),
|
|
3358
3707
|
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
3359
3708
|
);
|
|
3360
3709
|
const room = beat.seconds - spine;
|
|
@@ -3417,8 +3766,8 @@ var lineChart = (beat, ctx) => {
|
|
|
3417
3766
|
)
|
|
3418
3767
|
);
|
|
3419
3768
|
}
|
|
3420
|
-
const walk = cmp ? sec(step *
|
|
3421
|
-
if (
|
|
3769
|
+
const walk = cmp ? sec(step * count2) : DRAW_SECONDS;
|
|
3770
|
+
if (count2 > 1) {
|
|
3422
3771
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
3423
3772
|
tl.push(
|
|
3424
3773
|
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
@@ -3433,7 +3782,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3433
3782
|
)
|
|
3434
3783
|
);
|
|
3435
3784
|
}
|
|
3436
|
-
const drawn = settled + (cmp ? tailAfter(step) : step *
|
|
3785
|
+
const drawn = settled + (cmp ? tailAfter(step) : step * count2 + 0.4);
|
|
3437
3786
|
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
3438
3787
|
if (p.readout) {
|
|
3439
3788
|
tl.push(
|
|
@@ -3449,7 +3798,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3449
3798
|
return {
|
|
3450
3799
|
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
3451
3800
|
warnings: [
|
|
3452
|
-
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${
|
|
3801
|
+
`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.`
|
|
3453
3802
|
]
|
|
3454
3803
|
};
|
|
3455
3804
|
}
|
|
@@ -3529,7 +3878,7 @@ var V_BOX_H = 268;
|
|
|
3529
3878
|
var V_LOOP_SIDE = 56;
|
|
3530
3879
|
var V_LOOP_PAD = 18;
|
|
3531
3880
|
function columnLayout(stageW, stageH, stages, face, loop) {
|
|
3532
|
-
const
|
|
3881
|
+
const count2 = stages.length;
|
|
3533
3882
|
const loopW = loop ? Math.max(240, Math.round(stageW * 0.32)) : 0;
|
|
3534
3883
|
const boxX = M;
|
|
3535
3884
|
const boxW = stageW - 2 * M - (loop ? V_LOOP_SIDE + V_LOOP_PAD + loopW : 0);
|
|
@@ -3546,11 +3895,11 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3546
3895
|
return Math.ceil(2 * PAD_Y2 + label + (note > 0 ? NOTE_TOP + note * NOTE * NOTE_LH : 0));
|
|
3547
3896
|
})
|
|
3548
3897
|
);
|
|
3549
|
-
const room = (stageH - 2 * M - (
|
|
3898
|
+
const room = (stageH - 2 * M - (count2 - 1) * V_GAP) / count2;
|
|
3550
3899
|
const boxH = Math.max(need, Math.min(room, V_BOX_H));
|
|
3551
3900
|
return {
|
|
3552
3901
|
size,
|
|
3553
|
-
boxes: Array.from({ length:
|
|
3902
|
+
boxes: Array.from({ length: count2 }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
|
|
3554
3903
|
boxH,
|
|
3555
3904
|
boxX,
|
|
3556
3905
|
boxW,
|
|
@@ -3562,7 +3911,7 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3562
3911
|
loopLines: loop ? wrap(loop.label, NOTE, loopW, 500, 0, face) : [],
|
|
3563
3912
|
loopX: boxX + boxW + V_LOOP_SIDE,
|
|
3564
3913
|
loopW,
|
|
3565
|
-
svgH: 2 * M +
|
|
3914
|
+
svgH: 2 * M + count2 * boxH + (count2 - 1) * V_GAP
|
|
3566
3915
|
};
|
|
3567
3916
|
}
|
|
3568
3917
|
function balance(label, k, face) {
|
|
@@ -4280,8 +4629,8 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4280
4629
|
if (inline2.fits) return inline2;
|
|
4281
4630
|
return inline2.wide && inline2.blockH < stacked.blockH ? inline2 : stacked;
|
|
4282
4631
|
}
|
|
4283
|
-
function labelWeight(i,
|
|
4284
|
-
return i ===
|
|
4632
|
+
function labelWeight(i, count2) {
|
|
4633
|
+
return i === count2 - 1 ? 700 : 600;
|
|
4285
4634
|
}
|
|
4286
4635
|
function floorFor(p, format) {
|
|
4287
4636
|
if (!p.tilt) return MIN_FONT;
|
|
@@ -4291,7 +4640,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4291
4640
|
const width = contentW(format);
|
|
4292
4641
|
const boxH = contentH(format);
|
|
4293
4642
|
const floor = floorFor(p, format);
|
|
4294
|
-
const
|
|
4643
|
+
const count2 = p.layers.length;
|
|
4295
4644
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4296
4645
|
const riseMax = RISE_MAX[k];
|
|
4297
4646
|
const syMax = SY_MAX[k];
|
|
@@ -4299,7 +4648,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4299
4648
|
const noteW = (l) => inline2 && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4300
4649
|
const want = Math.max(
|
|
4301
4650
|
...p.layers.map(
|
|
4302
|
-
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i,
|
|
4651
|
+
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count2), 0, false, face) + noteW(l)
|
|
4303
4652
|
)
|
|
4304
4653
|
);
|
|
4305
4654
|
const colCap = inline2 ? width * 0.56 : width * 0.5;
|
|
@@ -4308,7 +4657,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4308
4657
|
const labelX = width - colW;
|
|
4309
4658
|
const labelRoom = Math.min(
|
|
4310
4659
|
...p.layers.map(
|
|
4311
|
-
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i,
|
|
4660
|
+
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count2), 0, false, face))
|
|
4312
4661
|
)
|
|
4313
4662
|
);
|
|
4314
4663
|
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
@@ -4316,7 +4665,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4316
4665
|
const nw = noteW(l);
|
|
4317
4666
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
4318
4667
|
return {
|
|
4319
|
-
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i,
|
|
4668
|
+
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count2), 0, face),
|
|
4320
4669
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4321
4670
|
// short line" — and wrapping one would put its second line under the label.
|
|
4322
4671
|
note: l.note === void 0 ? [] : inline2 ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
@@ -4334,13 +4683,13 @@ function solve2(p, format, inline2, face) {
|
|
|
4334
4683
|
const noteH = noteHeight(p.note, noteWidth(format), 28, face);
|
|
4335
4684
|
const free = boxH - chromeH - 20 - noteH;
|
|
4336
4685
|
const avail = Math.max(300, free);
|
|
4337
|
-
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1,
|
|
4686
|
+
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count2 - 1);
|
|
4338
4687
|
const rise = clamp(room, RISE_MIN, riseMax);
|
|
4339
4688
|
const sy = clamp(rise * 0.62, 46, syMax);
|
|
4340
4689
|
const t2 = clamp(rise * 0.14, 10, tMax);
|
|
4341
4690
|
const span = width - NUM_W - GAP3 - colW;
|
|
4342
4691
|
const sx = clamp(span * 0.2, 110, 220);
|
|
4343
|
-
const height = 2 * pad + (
|
|
4692
|
+
const height = 2 * pad + (count2 - 1) * rise + sy + t2;
|
|
4344
4693
|
return {
|
|
4345
4694
|
// A rise shorter than a label block prints the note through the label below.
|
|
4346
4695
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
@@ -4365,7 +4714,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4365
4714
|
labelSize,
|
|
4366
4715
|
blockH,
|
|
4367
4716
|
pad,
|
|
4368
|
-
yBase: pad + (
|
|
4717
|
+
yBase: pad + (count2 - 1) * rise + sy,
|
|
4369
4718
|
lines
|
|
4370
4719
|
};
|
|
4371
4720
|
}
|
|
@@ -4382,11 +4731,11 @@ var stack = (beat, ctx) => {
|
|
|
4382
4731
|
const p = beat.params;
|
|
4383
4732
|
const face = faceOf(ctx.theme.fontStack);
|
|
4384
4733
|
const L = stackLayout(p, ctx.format, face);
|
|
4385
|
-
const
|
|
4386
|
-
const last =
|
|
4734
|
+
const count2 = p.layers.length;
|
|
4735
|
+
const last = count2 - 1;
|
|
4387
4736
|
if (!L.fits) {
|
|
4388
4737
|
throw new Error(
|
|
4389
|
-
`stack ${beat.id}: ${
|
|
4738
|
+
`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.`
|
|
4390
4739
|
);
|
|
4391
4740
|
}
|
|
4392
4741
|
const parts = {};
|
|
@@ -4414,7 +4763,7 @@ var stack = (beat, ctx) => {
|
|
|
4414
4763
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
4415
4764
|
{
|
|
4416
4765
|
size: L.labelSize,
|
|
4417
|
-
weight: labelWeight(i,
|
|
4766
|
+
weight: labelWeight(i, count2),
|
|
4418
4767
|
fill: top ? theme.tones.b : theme.fg,
|
|
4419
4768
|
maxWidth: block.labelMaxW,
|
|
4420
4769
|
lineHeight: 1.16,
|
|
@@ -4468,10 +4817,10 @@ var stack = (beat, ctx) => {
|
|
|
4468
4817
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4469
4818
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4470
4819
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe3)}</div>${noteHtml}`;
|
|
4471
|
-
const centre2 = (
|
|
4820
|
+
const centre2 = (count2 - 1) / 2;
|
|
4472
4821
|
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4473
4822
|
const first = 0.9;
|
|
4474
|
-
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) /
|
|
4823
|
+
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count2));
|
|
4475
4824
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
4476
4825
|
const holds = [];
|
|
4477
4826
|
const spot = spotlighter(sid);
|
|
@@ -4503,11 +4852,11 @@ var stack = (beat, ctx) => {
|
|
|
4503
4852
|
);
|
|
4504
4853
|
}
|
|
4505
4854
|
if (p.note) {
|
|
4506
|
-
const at = first +
|
|
4855
|
+
const at = first + count2 * step;
|
|
4507
4856
|
tl.push(tween(`#${sid}-note`, { opacity: 0 }, { opacity: 1, duration: 0.6 }, at));
|
|
4508
4857
|
holds.push(at + 0.7);
|
|
4509
4858
|
}
|
|
4510
|
-
if (
|
|
4859
|
+
if (count2 > 1) tl.push(...spot.restore(first + count2 * step));
|
|
4511
4860
|
return {
|
|
4512
4861
|
html,
|
|
4513
4862
|
parts,
|
|
@@ -5809,8 +6158,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5809
6158
|
const text2 = beat.narration?.trim();
|
|
5810
6159
|
if (!text2) continue;
|
|
5811
6160
|
const segments2 = [];
|
|
5812
|
-
const
|
|
5813
|
-
const plan = planSegments(text2, Math.min(
|
|
6161
|
+
const count2 = stopsFor(beat, source, format, `s${i + 1}`, theme);
|
|
6162
|
+
const plan = planSegments(text2, Math.min(count2, paced.speakingStops));
|
|
5814
6163
|
for (const [stop, line2] of plan.entries()) {
|
|
5815
6164
|
if (!line2) continue;
|
|
5816
6165
|
const speech = await synthesize(line2, {
|
|
@@ -5832,7 +6181,7 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5832
6181
|
}
|
|
5833
6182
|
if (segments2.length > 0) {
|
|
5834
6183
|
beats[beat.id] = segments2;
|
|
5835
|
-
staged[beat.id] =
|
|
6184
|
+
staged[beat.id] = count2;
|
|
5836
6185
|
}
|
|
5837
6186
|
}
|
|
5838
6187
|
return {
|
|
@@ -6702,11 +7051,11 @@ function stageScene(scene, speed) {
|
|
|
6702
7051
|
function speechPlan(open, holds, segments2) {
|
|
6703
7052
|
const starts = [];
|
|
6704
7053
|
let at = open;
|
|
6705
|
-
for (const [i,
|
|
6706
|
-
const hold = holds[Math.min(
|
|
7054
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7055
|
+
const hold = holds[Math.min(segment2.stop, holds.length - 1)] ?? 0;
|
|
6707
7056
|
if (i > 0) at = Math.max(at, hold);
|
|
6708
7057
|
starts.push(at);
|
|
6709
|
-
at +=
|
|
7058
|
+
at += segment2.seconds;
|
|
6710
7059
|
}
|
|
6711
7060
|
return { starts, end: at };
|
|
6712
7061
|
}
|
|
@@ -6931,10 +7280,10 @@ function pack(words2, width) {
|
|
|
6931
7280
|
function splitCue(cue, max = CUE_MAX_CHARS) {
|
|
6932
7281
|
if (cue.text.length <= max) return [cue];
|
|
6933
7282
|
const words2 = cue.text.split(/\s+/).filter(Boolean);
|
|
6934
|
-
const
|
|
7283
|
+
const count2 = pack(words2, max).length;
|
|
6935
7284
|
let width = max;
|
|
6936
|
-
for (let w = Math.ceil(cue.text.length /
|
|
6937
|
-
if (pack(words2, w).length <=
|
|
7285
|
+
for (let w = Math.ceil(cue.text.length / count2); w < max; w++) {
|
|
7286
|
+
if (pack(words2, w).length <= count2) {
|
|
6938
7287
|
width = w;
|
|
6939
7288
|
break;
|
|
6940
7289
|
}
|
|
@@ -7026,17 +7375,17 @@ function place(scenes, spoken) {
|
|
|
7026
7375
|
);
|
|
7027
7376
|
}
|
|
7028
7377
|
const { starts } = speechPlan(scene.open, scene.holds, segments2);
|
|
7029
|
-
for (const [i,
|
|
7030
|
-
const index = Math.min(
|
|
7378
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7379
|
+
const index = Math.min(segment2.stop, scene.holds.length - 1);
|
|
7031
7380
|
out.push({
|
|
7032
|
-
id: `${scene.id}.${
|
|
7381
|
+
id: `${scene.id}.${segment2.stop}`,
|
|
7033
7382
|
scene: scene.id,
|
|
7034
|
-
stop:
|
|
7035
|
-
audio:
|
|
7383
|
+
stop: segment2.stop,
|
|
7384
|
+
audio: segment2.audio,
|
|
7036
7385
|
hold: round5(scene.start + scene.holds[index]),
|
|
7037
7386
|
start: round5(scene.start + starts[i]),
|
|
7038
|
-
duration:
|
|
7039
|
-
cues:
|
|
7387
|
+
duration: segment2.seconds,
|
|
7388
|
+
cues: segment2.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
|
|
7040
7389
|
});
|
|
7041
7390
|
}
|
|
7042
7391
|
}
|
|
@@ -7113,6 +7462,15 @@ function planTiming(input) {
|
|
|
7113
7462
|
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
7114
7463
|
// disagree with the box the archetypes actually drew into.
|
|
7115
7464
|
captionReserve: format.captionReserve ?? 0,
|
|
7465
|
+
// An unbudgeted format's limit is Infinity, which JSON writes as `null`.
|
|
7466
|
+
// Left out instead: recorded as a format with no limit, never with a limit
|
|
7467
|
+
// of null or zero.
|
|
7468
|
+
format: {
|
|
7469
|
+
id: format.id,
|
|
7470
|
+
minWeight: format.minWeight,
|
|
7471
|
+
...Number.isFinite(format.maxSeconds) && { maxSeconds: format.maxSeconds },
|
|
7472
|
+
...format.warnSeconds !== void 0 && { warnSeconds: format.warnSeconds }
|
|
7473
|
+
},
|
|
7116
7474
|
scenes,
|
|
7117
7475
|
segments: segments2
|
|
7118
7476
|
};
|
|
@@ -7139,15 +7497,15 @@ function framePlan(timing, fps) {
|
|
|
7139
7497
|
);
|
|
7140
7498
|
}
|
|
7141
7499
|
pieces.push({ from: first, motion: last - first, freeze: 0 });
|
|
7142
|
-
for (const
|
|
7143
|
-
const at = f(
|
|
7500
|
+
for (const segment2 of segments2) {
|
|
7501
|
+
const at = f(segment2.start);
|
|
7144
7502
|
audio.push({
|
|
7145
|
-
id:
|
|
7146
|
-
audio:
|
|
7503
|
+
id: segment2.id,
|
|
7504
|
+
audio: segment2.audio,
|
|
7147
7505
|
startFrame: at,
|
|
7148
7506
|
delayMs: Math.round(at / fps * 1e3)
|
|
7149
7507
|
});
|
|
7150
|
-
for (const cue of
|
|
7508
|
+
for (const cue of segment2.cues) {
|
|
7151
7509
|
const start = at / fps + cue.start;
|
|
7152
7510
|
const end = at / fps + cue.end;
|
|
7153
7511
|
if (end > start) cues.push({ start, end, text: cue.text });
|
|
@@ -7712,6 +8070,22 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
7712
8070
|
async function chromePath(need = "open the deck with") {
|
|
7713
8071
|
return (await resolveChrome(need)).path;
|
|
7714
8072
|
}
|
|
8073
|
+
function sandboxMissing(err) {
|
|
8074
|
+
return /No usable sandbox/.test(err instanceof Error ? err.message : String(err));
|
|
8075
|
+
}
|
|
8076
|
+
var SANDBOX_FIX = "`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`, or give Chrome an AppArmor profile";
|
|
8077
|
+
async function launchOwnPage(launch, args, what) {
|
|
8078
|
+
try {
|
|
8079
|
+
return await launch(args);
|
|
8080
|
+
} catch (err) {
|
|
8081
|
+
if (!sandboxMissing(err)) throw err;
|
|
8082
|
+
process.stderr.write(
|
|
8083
|
+
`chrome: this host gives Chrome no sandbox, so ${what} runs without one, as hyperframes' render always does. To keep the sandbox: ${SANDBOX_FIX}.
|
|
8084
|
+
`
|
|
8085
|
+
);
|
|
8086
|
+
return launch([...args, "--no-sandbox"]);
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
7715
8089
|
|
|
7716
8090
|
// src/source/assets.ts
|
|
7717
8091
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -8683,6 +9057,14 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
8683
9057
|
// happens to be that run.
|
|
8684
9058
|
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
8685
9059
|
]
|
|
9060
|
+
// No `--no-sandbox` fallback here, unlike `launchOwnPage`: this is a page
|
|
9061
|
+
// from the web, and a renderer bug it triggers is exactly what the sandbox
|
|
9062
|
+
// contains. Refused, with the fix, rather than Chrome's raw FATAL.
|
|
9063
|
+
}).catch((err) => {
|
|
9064
|
+
if (!sandboxMissing(err)) throw err;
|
|
9065
|
+
throw new Error(
|
|
9066
|
+
`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.`
|
|
9067
|
+
);
|
|
8686
9068
|
});
|
|
8687
9069
|
try {
|
|
8688
9070
|
const page = await browser.newPage();
|
|
@@ -10039,8 +10421,13 @@ function renderSource(source) {
|
|
|
10039
10421
|
}
|
|
10040
10422
|
}
|
|
10041
10423
|
out.push("", "== EQUATIONS ==");
|
|
10042
|
-
for (const e of source.equations)
|
|
10043
|
-
|
|
10424
|
+
for (const e of source.equations) {
|
|
10425
|
+
const fixed = repairTex(e.tex, true);
|
|
10426
|
+
const note = fixed.error === null ? "" : " (KaTeX cannot draw this one: a walk shows it as plain source, a morph cannot use it)";
|
|
10427
|
+
out.push(
|
|
10428
|
+
`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${fixed.error === null ? fixed.tex : e.tex}${note}`
|
|
10429
|
+
);
|
|
10430
|
+
}
|
|
10044
10431
|
if (!source.equations.length)
|
|
10045
10432
|
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
10046
10433
|
out.push("", "== TABLES ==");
|
|
@@ -10572,10 +10959,10 @@ function drawSvg(req) {
|
|
|
10572
10959
|
const seed = createHash7("sha256").update([req.prompt, req.style, req.aspect].join("|")).digest();
|
|
10573
10960
|
const rand = mulberry32(seed.readUInt32BE(0));
|
|
10574
10961
|
const accent = ACCENTS[Math.floor(rand() * ACCENTS.length)];
|
|
10575
|
-
const
|
|
10962
|
+
const count2 = 6 + Math.floor(rand() * 5);
|
|
10576
10963
|
const unit = Math.min(W, H);
|
|
10577
10964
|
const parts = [`<rect width="${W}" height="${H}" fill="${PAPER}"/>`];
|
|
10578
|
-
for (let i = 0; i <
|
|
10965
|
+
for (let i = 0; i < count2; i++) {
|
|
10579
10966
|
const kind = Math.floor(rand() * 3);
|
|
10580
10967
|
const cx = Math.round(W * (0.15 + 0.7 * rand()));
|
|
10581
10968
|
const cy = Math.round(H * (0.15 + 0.7 * rand()));
|
|
@@ -10866,7 +11253,7 @@ ${bands}
|
|
|
10866
11253
|
</div>
|
|
10867
11254
|
`;
|
|
10868
11255
|
}
|
|
10869
|
-
function measure3(
|
|
11256
|
+
function measure3(count2) {
|
|
10870
11257
|
const doc = document;
|
|
10871
11258
|
const probe3 = doc.getElementById("c0");
|
|
10872
11259
|
if (probe3) {
|
|
@@ -10879,7 +11266,7 @@ function measure3(count) {
|
|
|
10879
11266
|
probe3.classList.remove("on");
|
|
10880
11267
|
}
|
|
10881
11268
|
const rects = [];
|
|
10882
|
-
for (let i = 0; i <
|
|
11269
|
+
for (let i = 0; i < count2; i++) {
|
|
10883
11270
|
const el = doc.getElementById(`c${i}`);
|
|
10884
11271
|
if (!el) continue;
|
|
10885
11272
|
el.classList.add("on");
|
|
@@ -10907,11 +11294,12 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
10907
11294
|
await mkdir7(work2, { recursive: true });
|
|
10908
11295
|
await writeFile9(page, captionPage(cues, style, href));
|
|
10909
11296
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
10910
|
-
const
|
|
10911
|
-
|
|
10912
|
-
headless: true,
|
|
10913
|
-
|
|
10914
|
-
|
|
11297
|
+
const executablePath = await chromePath(CAPTION_NEED);
|
|
11298
|
+
const browser = await launchOwnPage(
|
|
11299
|
+
(args) => puppeteer.launch({ executablePath, headless: true, args }),
|
|
11300
|
+
["--force-device-scale-factor=1", "--hide-scrollbars"],
|
|
11301
|
+
"the caption band"
|
|
11302
|
+
);
|
|
10915
11303
|
try {
|
|
10916
11304
|
const tab = await browser.newPage();
|
|
10917
11305
|
await tab.setViewport({ width: style.width, height: style.height, deviceScaleFactor: 1 });
|
|
@@ -11336,10 +11724,10 @@ function safeEntryPath(name) {
|
|
|
11336
11724
|
const segments2 = name.replace(/\\/g, "/").split("/");
|
|
11337
11725
|
if (/^[a-zA-Z]:/.test(name) || name.startsWith("/") || name.startsWith("\\")) return null;
|
|
11338
11726
|
const kept = [];
|
|
11339
|
-
for (const
|
|
11340
|
-
if (
|
|
11341
|
-
if (
|
|
11342
|
-
kept.push(
|
|
11727
|
+
for (const segment2 of segments2) {
|
|
11728
|
+
if (segment2 === "" || segment2 === ".") continue;
|
|
11729
|
+
if (segment2 === "..") return null;
|
|
11730
|
+
kept.push(segment2);
|
|
11343
11731
|
}
|
|
11344
11732
|
if (kept.length === 0) return null;
|
|
11345
11733
|
const path2 = kept.join("/");
|