@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/index.js
CHANGED
|
@@ -152,6 +152,11 @@ async function copyAudio(from, narration, out, step) {
|
|
|
152
152
|
return names.map((n3) => join2(dir, n3));
|
|
153
153
|
}
|
|
154
154
|
async function refreshFont(storyboard, source, out, step) {
|
|
155
|
+
if (familyFor(storyboard.lang) === null) {
|
|
156
|
+
const css = await vendorInter(join2(out, "assets", "fonts"));
|
|
157
|
+
step("build: vendored Inter beside the deck");
|
|
158
|
+
return css;
|
|
159
|
+
}
|
|
155
160
|
try {
|
|
156
161
|
const bundle = await bundleFont(
|
|
157
162
|
storyboard.lang,
|
|
@@ -167,6 +172,21 @@ async function refreshFont(storyboard, source, out, step) {
|
|
|
167
172
|
return void 0;
|
|
168
173
|
}
|
|
169
174
|
}
|
|
175
|
+
async function vendorInter(dir) {
|
|
176
|
+
const pkg = dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter"));
|
|
177
|
+
const css = await readFile2(join2(pkg, "index.css"), "utf8");
|
|
178
|
+
await mkdir2(dir, { recursive: true });
|
|
179
|
+
for (const [, name] of css.matchAll(/url\(\.\/files\/([^)]+)\)/g)) {
|
|
180
|
+
await cp(join2(pkg, "files", name ?? ""), join2(dir, name ?? ""));
|
|
181
|
+
}
|
|
182
|
+
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')");
|
|
183
|
+
if (/Inter Variable|\.\/files\/|swap/.test(ours) || !ours.includes("font-family: 'Inter';")) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`@fontsource-variable/inter's index.css changed shape; update vendorInter (${pkg})`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return ours;
|
|
189
|
+
}
|
|
170
190
|
|
|
171
191
|
// src/emit/kit.ts
|
|
172
192
|
var PAD_X = 110;
|
|
@@ -844,35 +864,35 @@ function elbow(sid, from, to, o) {
|
|
|
844
864
|
}
|
|
845
865
|
return path(d, { ...strokeAttrs(o), "marker-end": `url(#${headId(sid, o.stroke)})` });
|
|
846
866
|
}
|
|
847
|
-
function tracks(width,
|
|
848
|
-
const w =
|
|
849
|
-
return Array.from({ length:
|
|
867
|
+
function tracks(width, count2, gap, x0 = 0) {
|
|
868
|
+
const w = count2 > 1 ? (width - gap * (count2 - 1)) / count2 : width;
|
|
869
|
+
return Array.from({ length: count2 }, (_, i) => ({ x: x0 + i * (w + gap), w }));
|
|
850
870
|
}
|
|
851
871
|
function fitBoxes(req) {
|
|
852
|
-
const
|
|
872
|
+
const count2 = req.labels.length;
|
|
853
873
|
const minGap = req.minGap ?? 24;
|
|
854
874
|
const padEm = req.padEm ?? 0.8;
|
|
855
875
|
const weight = req.weight ?? 600;
|
|
856
876
|
const x0 = req.x0 ?? 0;
|
|
857
877
|
const unit = Math.max(1, ...req.labels.map((l) => textWidth(l, 1, weight) + 2 * padEm));
|
|
858
|
-
const sizeAt = (gap) =>
|
|
878
|
+
const sizeAt = (gap) => count2 < 1 ? req.size : (req.width - gap * (count2 - 1)) / count2 / unit;
|
|
859
879
|
if (sizeAt(req.gap) >= req.size) {
|
|
860
|
-
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width,
|
|
880
|
+
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width, count2, req.gap, x0) };
|
|
861
881
|
}
|
|
862
|
-
if (
|
|
863
|
-
const gap = (req.width -
|
|
864
|
-
return { ok: true, size: req.size, gap, boxes: tracks(req.width,
|
|
882
|
+
if (count2 > 1 && sizeAt(minGap) >= req.size) {
|
|
883
|
+
const gap = (req.width - count2 * req.size * unit) / (count2 - 1);
|
|
884
|
+
return { ok: true, size: req.size, gap, boxes: tracks(req.width, count2, gap, x0) };
|
|
865
885
|
}
|
|
866
886
|
const size2 = sizeAt(minGap);
|
|
867
887
|
if (size2 >= MIN_FONT) {
|
|
868
|
-
return { ok: true, size: size2, gap: minGap, boxes: tracks(req.width,
|
|
888
|
+
return { ok: true, size: size2, gap: minGap, boxes: tracks(req.width, count2, minGap, x0) };
|
|
869
889
|
}
|
|
870
890
|
return {
|
|
871
891
|
ok: false,
|
|
872
892
|
size: MIN_FONT,
|
|
873
893
|
gap: minGap,
|
|
874
|
-
boxes: tracks(req.width,
|
|
875
|
-
needed:
|
|
894
|
+
boxes: tracks(req.width, count2, minGap, x0),
|
|
895
|
+
needed: count2 * MIN_FONT * unit + minGap * (count2 - 1)
|
|
876
896
|
};
|
|
877
897
|
}
|
|
878
898
|
|
|
@@ -1205,8 +1225,8 @@ var MID_GAP = 56;
|
|
|
1205
1225
|
var BAND_GAP = 34;
|
|
1206
1226
|
var COL_GAP = 28;
|
|
1207
1227
|
var MIN_COL = 250;
|
|
1208
|
-
function lineBudget(
|
|
1209
|
-
const perSide = Math.max(1, Math.ceil(
|
|
1228
|
+
function lineBudget(count2, stageH) {
|
|
1229
|
+
const perSide = Math.max(1, Math.ceil(count2 / 2));
|
|
1210
1230
|
const room = (stageH - (perSide - 1) * LAB_GAP) / perSide;
|
|
1211
1231
|
const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
|
|
1212
1232
|
return Math.min(LINE_CEIL, Math.max(1, lines));
|
|
@@ -1617,7 +1637,7 @@ var HEAD_GAP = 14;
|
|
|
1617
1637
|
var barCompare = (beat, ctx) => {
|
|
1618
1638
|
const { sid, theme } = ctx;
|
|
1619
1639
|
const p = beat.params;
|
|
1620
|
-
const
|
|
1640
|
+
const count2 = p.bars.length;
|
|
1621
1641
|
const W = contentW(ctx.format);
|
|
1622
1642
|
const face = faceOf(ctx.theme.fontStack);
|
|
1623
1643
|
const tall = isPortrait(ctx.format);
|
|
@@ -1631,7 +1651,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1631
1651
|
0,
|
|
1632
1652
|
face
|
|
1633
1653
|
);
|
|
1634
|
-
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (
|
|
1654
|
+
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count2 + GAP_RATIO * (count2 - 1)));
|
|
1635
1655
|
let pitch = bar * (1 + GAP_RATIO);
|
|
1636
1656
|
const gutterMax = tall ? 0 : share(GUTTER_MAX, W);
|
|
1637
1657
|
const gutterInner = tall ? W : gutterMax - GUTTER_PAD;
|
|
@@ -1653,7 +1673,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1653
1673
|
if (tall) {
|
|
1654
1674
|
bar = Math.min(
|
|
1655
1675
|
BAR_MAX,
|
|
1656
|
-
(avail - unitBand - FOOT -
|
|
1676
|
+
(avail - unitBand - FOOT - count2 * head) / (count2 + GAP_RATIO * (count2 - 1))
|
|
1657
1677
|
);
|
|
1658
1678
|
pitch = head + bar * (1 + GAP_RATIO);
|
|
1659
1679
|
} else {
|
|
@@ -1664,7 +1684,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1664
1684
|
}
|
|
1665
1685
|
}
|
|
1666
1686
|
const valueSize = Math.max(MIN_FONT, Math.min(VALUE_MAX, bar * 0.9));
|
|
1667
|
-
const barsH = (
|
|
1687
|
+
const barsH = (count2 - 1) * pitch + head + bar;
|
|
1668
1688
|
const foot = Math.max(
|
|
1669
1689
|
0,
|
|
1670
1690
|
Math.ceil(
|
|
@@ -1677,7 +1697,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1677
1697
|
const H = barsH + unitBand + foot;
|
|
1678
1698
|
if (H > avail) {
|
|
1679
1699
|
throw new Error(
|
|
1680
|
-
`bar-compare ${beat.id}: ${
|
|
1700
|
+
`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.`
|
|
1681
1701
|
);
|
|
1682
1702
|
}
|
|
1683
1703
|
const gutter = tall ? 0 : Math.min(
|
|
@@ -1830,7 +1850,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1830
1850
|
</div>${note2}`;
|
|
1831
1851
|
const railsAt = 0.6;
|
|
1832
1852
|
const barsAt = 0.95;
|
|
1833
|
-
const step = Math.min(0.4, 2.4 /
|
|
1853
|
+
const step = Math.min(0.4, 2.4 / count2);
|
|
1834
1854
|
const grow = 0.85;
|
|
1835
1855
|
const tl = [
|
|
1836
1856
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
@@ -1880,11 +1900,11 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1880
1900
|
);
|
|
1881
1901
|
}
|
|
1882
1902
|
}
|
|
1883
|
-
const settled = barsAt + (
|
|
1903
|
+
const settled = barsAt + (count2 - 1) * step + grow + 0.05;
|
|
1884
1904
|
const holds = [settled + 0.2];
|
|
1885
1905
|
const focal = rows.reduce((best, r) => Math.abs(r.value) > Math.abs(best.value) ? r : best);
|
|
1886
1906
|
const spot = spotlighter(sid, ".bc-bar");
|
|
1887
|
-
if (
|
|
1907
|
+
if (count2 > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
|
|
1888
1908
|
const tailAt = settled + 0.3;
|
|
1889
1909
|
if (p.unit) {
|
|
1890
1910
|
tl.push(tween(`#${id(sid, "unit")}`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, tailAt));
|
|
@@ -2377,6 +2397,90 @@ var dataTable = (beat, ctx) => {
|
|
|
2377
2397
|
};
|
|
2378
2398
|
};
|
|
2379
2399
|
|
|
2400
|
+
// src/emit/tex.ts
|
|
2401
|
+
import katex from "katex";
|
|
2402
|
+
var UNFIT_ATTR = "data-ds-unfit";
|
|
2403
|
+
var trust = (c) => c.command === "\\htmlClass";
|
|
2404
|
+
function texError(tex, display) {
|
|
2405
|
+
try {
|
|
2406
|
+
katex.renderToString(tex, {
|
|
2407
|
+
displayMode: display,
|
|
2408
|
+
throwOnError: true,
|
|
2409
|
+
strict: false,
|
|
2410
|
+
trust,
|
|
2411
|
+
output: "html"
|
|
2412
|
+
});
|
|
2413
|
+
return null;
|
|
2414
|
+
} catch (err) {
|
|
2415
|
+
return err instanceof Error ? err.message : String(err);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
var LEFT = /\\left(?![a-zA-Z])/g;
|
|
2419
|
+
var RIGHT = /\\right(?![a-zA-Z])/g;
|
|
2420
|
+
var NO_INK = /\\label\{[^{}]*\}|\\nonumber(?![a-zA-Z])|\\notag(?![a-zA-Z])/g;
|
|
2421
|
+
function braceBalance(tex) {
|
|
2422
|
+
let depth = 0;
|
|
2423
|
+
for (let i = 0; i < tex.length; i++) {
|
|
2424
|
+
const c = tex[i];
|
|
2425
|
+
if (c === "\\") {
|
|
2426
|
+
i++;
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
if (c === "{") depth++;
|
|
2430
|
+
else if (c === "}") depth--;
|
|
2431
|
+
}
|
|
2432
|
+
return depth;
|
|
2433
|
+
}
|
|
2434
|
+
var count = (tex, re) => (tex.match(re) ?? []).length;
|
|
2435
|
+
function repairTex(input, display) {
|
|
2436
|
+
const repairs = [];
|
|
2437
|
+
let tex = input;
|
|
2438
|
+
let error = texError(tex, display);
|
|
2439
|
+
if (error === null) return { tex, repairs, error };
|
|
2440
|
+
const gone = tex.match(NO_INK) ?? [];
|
|
2441
|
+
if (gone.length) {
|
|
2442
|
+
tex = tex.replace(NO_INK, "").trim();
|
|
2443
|
+
repairs.push(`removed ${gone.join(", ")}, which draw nothing`);
|
|
2444
|
+
error = texError(tex, display);
|
|
2445
|
+
}
|
|
2446
|
+
const lefts = count(tex, LEFT);
|
|
2447
|
+
const rights = count(tex, RIGHT);
|
|
2448
|
+
if (error !== null && lefts !== rights) {
|
|
2449
|
+
const n3 = Math.abs(lefts - rights);
|
|
2450
|
+
tex = lefts > rights ? `${tex} ${"\\right.".repeat(n3)}` : `${"\\left.".repeat(n3)} ${tex}`;
|
|
2451
|
+
repairs.push(
|
|
2452
|
+
`${lefts} \\left against ${rights} \\right: added ${n3} invisible ${lefts > rights ? "\\right." : "\\left."} to balance them`
|
|
2453
|
+
);
|
|
2454
|
+
error = texError(tex, display);
|
|
2455
|
+
}
|
|
2456
|
+
const depth = braceBalance(tex);
|
|
2457
|
+
if (error !== null && depth !== 0) {
|
|
2458
|
+
const n3 = Math.abs(depth);
|
|
2459
|
+
tex = depth > 0 ? `${tex}${"}".repeat(n3)}` : `${"{".repeat(n3)}${tex}`;
|
|
2460
|
+
repairs.push(
|
|
2461
|
+
`${n3} unclosed brace group(s): ${depth > 0 ? "closed at the end" : "opened at the start"}`
|
|
2462
|
+
);
|
|
2463
|
+
error = texError(tex, display);
|
|
2464
|
+
}
|
|
2465
|
+
const undefinedNames = [];
|
|
2466
|
+
for (let round7 = 0; error !== null && round7 < 12; round7++) {
|
|
2467
|
+
const m = /Undefined control sequence: \\([a-zA-Z]+)/.exec(error);
|
|
2468
|
+
if (!m) break;
|
|
2469
|
+
const name = m[1];
|
|
2470
|
+
const next = tex.replace(new RegExp(`\\\\${name}(?![a-zA-Z])`, "g"), `\\operatorname{${name}}`);
|
|
2471
|
+
if (next === tex) break;
|
|
2472
|
+
tex = next;
|
|
2473
|
+
undefinedNames.push(`\\${name}`);
|
|
2474
|
+
error = texError(tex, display);
|
|
2475
|
+
}
|
|
2476
|
+
if (undefinedNames.length) {
|
|
2477
|
+
repairs.push(
|
|
2478
|
+
`${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`
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
return { tex, repairs, error };
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2380
2484
|
// src/emit/archetypes/equation-walk.ts
|
|
2381
2485
|
var TRUST = 'function (c) { return c.command === "\\\\htmlClass"; }';
|
|
2382
2486
|
var OPTS = `{ displayMode: true, trust: ${TRUST}, strict: false, output: "html" }`;
|
|
@@ -2391,6 +2495,9 @@ var SYNONYMS = [
|
|
|
2391
2495
|
function normalise(tex) {
|
|
2392
2496
|
let out = "";
|
|
2393
2497
|
const map = [];
|
|
2498
|
+
const ends = [];
|
|
2499
|
+
const lead = [];
|
|
2500
|
+
let hint = null;
|
|
2394
2501
|
for (let i = 0; i < tex.length; ) {
|
|
2395
2502
|
if (/\s/.test(tex[i])) {
|
|
2396
2503
|
i++;
|
|
@@ -2402,19 +2509,19 @@ function normalise(tex) {
|
|
|
2402
2509
|
const m = new RegExp(`^(?:${re.source})`).exec(rest);
|
|
2403
2510
|
return m ? { len: m[0].length, to } : null;
|
|
2404
2511
|
}).find(Boolean);
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
i
|
|
2411
|
-
|
|
2512
|
+
const len = hit ? hit.len : 1;
|
|
2513
|
+
const chars = hit ? hit.to : tex[i];
|
|
2514
|
+
if (chars === "") hint ??= i;
|
|
2515
|
+
for (const ch of chars) {
|
|
2516
|
+
out += ch;
|
|
2517
|
+
map.push(i);
|
|
2518
|
+
ends.push(i + len);
|
|
2519
|
+
lead.push(hint ?? i);
|
|
2520
|
+
hint = null;
|
|
2412
2521
|
}
|
|
2413
|
-
|
|
2414
|
-
map.push(i);
|
|
2415
|
-
i++;
|
|
2522
|
+
i += len;
|
|
2416
2523
|
}
|
|
2417
|
-
return { text: out, map };
|
|
2524
|
+
return { text: out, map, ends, lead };
|
|
2418
2525
|
}
|
|
2419
2526
|
function locate(tex, term) {
|
|
2420
2527
|
const hay = normalise(tex);
|
|
@@ -2422,58 +2529,131 @@ function locate(tex, term) {
|
|
|
2422
2529
|
if (needle === "") return null;
|
|
2423
2530
|
const at = hay.text.indexOf(needle);
|
|
2424
2531
|
if (at < 0) return null;
|
|
2425
|
-
const start = hay.
|
|
2426
|
-
const
|
|
2427
|
-
const lastOrig = hay.map[lastNorm];
|
|
2428
|
-
let end = lastOrig + 1;
|
|
2429
|
-
while (end < tex.length && (hay.map[lastNorm + 1] ?? tex.length) > end && /\s/.test(tex[end])) {
|
|
2430
|
-
end++;
|
|
2431
|
-
}
|
|
2432
|
-
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
2532
|
+
const start = hay.lead[at];
|
|
2533
|
+
const end = hay.ends[at + needle.length - 1];
|
|
2433
2534
|
return { start, end: Math.min(end, tex.length) };
|
|
2434
2535
|
}
|
|
2435
2536
|
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
2537
|
+
const checked = texError(tex, true) === null;
|
|
2436
2538
|
let parts = [{ text: tex, raw: true }];
|
|
2437
2539
|
const used = [];
|
|
2438
2540
|
const missing = [];
|
|
2541
|
+
const broken = [];
|
|
2542
|
+
const joined = (ps) => ps.map((p) => p.text).join("");
|
|
2439
2543
|
for (const term of terms) {
|
|
2440
2544
|
let placed = false;
|
|
2545
|
+
let found = false;
|
|
2441
2546
|
for (let i = 0; i < parts.length && !placed; i++) {
|
|
2442
2547
|
const part = parts[i];
|
|
2443
2548
|
if (!part.raw) continue;
|
|
2444
2549
|
const at = locate(part.text, term.tex);
|
|
2445
2550
|
if (!at) continue;
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
raw:
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2551
|
+
found = true;
|
|
2552
|
+
const body = `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`;
|
|
2553
|
+
for (const text2 of [body, `{${body}}`]) {
|
|
2554
|
+
const next = parts.toSpliced(
|
|
2555
|
+
i,
|
|
2556
|
+
1,
|
|
2557
|
+
{ text: part.text.slice(0, at.start), raw: true },
|
|
2558
|
+
{ text: text2, raw: false },
|
|
2559
|
+
{ text: part.text.slice(at.end), raw: true }
|
|
2560
|
+
);
|
|
2561
|
+
if (checked && texError(joined(next), true) !== null) continue;
|
|
2562
|
+
parts = next;
|
|
2563
|
+
used.push(term);
|
|
2564
|
+
placed = true;
|
|
2565
|
+
break;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (!placed) {
|
|
2569
|
+
missing.push(term);
|
|
2570
|
+
if (found) broken.push(term);
|
|
2458
2571
|
}
|
|
2459
|
-
if (!placed) missing.push(term);
|
|
2460
2572
|
}
|
|
2461
2573
|
if (used.length === 0) {
|
|
2462
2574
|
throw new Error(
|
|
2463
|
-
`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)}`
|
|
2575
|
+
`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)}`
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
return { tex: joined(parts), used, missing, broken };
|
|
2579
|
+
}
|
|
2580
|
+
function droppedTerms(eqId, missing, broken) {
|
|
2581
|
+
return missing.map(
|
|
2582
|
+
(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`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
function prepareWalk(eq, terms, beatId) {
|
|
2586
|
+
const fixed = repairTex(eq.tex, true);
|
|
2587
|
+
const notes = fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`);
|
|
2588
|
+
const repairedTerms = terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
2589
|
+
if (fixed.error !== null) {
|
|
2590
|
+
notes.push(
|
|
2591
|
+
`${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)}`
|
|
2464
2592
|
);
|
|
2593
|
+
const parts = segment(eq.tex, [...terms]);
|
|
2594
|
+
if (parts.used.length === 0) {
|
|
2595
|
+
throw new Error(
|
|
2596
|
+
`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)}`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
notes.push(...droppedTerms(eq.id, parts.missing, []));
|
|
2600
|
+
return {
|
|
2601
|
+
raw: eq.tex,
|
|
2602
|
+
plain: { error: fixed.error },
|
|
2603
|
+
used: parts.used,
|
|
2604
|
+
missing: parts.missing,
|
|
2605
|
+
notes
|
|
2606
|
+
};
|
|
2465
2607
|
}
|
|
2466
|
-
|
|
2608
|
+
const walk = wrapTerms(fixed.tex, repairedTerms, beatId);
|
|
2609
|
+
const back = (ts) => ts.map((t2) => terms[repairedTerms.indexOf(t2)]);
|
|
2610
|
+
const used = back(walk.used);
|
|
2611
|
+
const missing = back(walk.missing);
|
|
2612
|
+
notes.push(...droppedTerms(eq.id, missing, back(walk.broken)));
|
|
2613
|
+
return { tex: walk.tex, raw: fixed.tex, used, missing, notes };
|
|
2614
|
+
}
|
|
2615
|
+
function segment(tex, terms) {
|
|
2616
|
+
let parts = [{ text: tex }];
|
|
2617
|
+
const used = [];
|
|
2618
|
+
const missing = [];
|
|
2619
|
+
for (const term of terms) {
|
|
2620
|
+
const i = parts.findIndex((p) => !p.term && locate(p.text, term.tex));
|
|
2621
|
+
const part = parts[i];
|
|
2622
|
+
const at = part && locate(part.text, term.tex);
|
|
2623
|
+
if (!part || !at) {
|
|
2624
|
+
missing.push(term);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
parts = parts.toSpliced(
|
|
2628
|
+
i,
|
|
2629
|
+
1,
|
|
2630
|
+
{ text: part.text.slice(0, at.start) },
|
|
2631
|
+
{ text: part.text.slice(at.start, at.end), term },
|
|
2632
|
+
{ text: part.text.slice(at.end) }
|
|
2633
|
+
);
|
|
2634
|
+
used.push(term);
|
|
2635
|
+
}
|
|
2636
|
+
return { parts, used, missing };
|
|
2637
|
+
}
|
|
2638
|
+
function plainHtml(tex, terms) {
|
|
2639
|
+
return segment(tex, terms).parts.map(
|
|
2640
|
+
(p) => p.term ? `<span class="term t-${p.term.tone}">${esc(p.text)}</span>` : esc(p.text)
|
|
2641
|
+
).join("");
|
|
2642
|
+
}
|
|
2643
|
+
function chipSetup(sid, term) {
|
|
2644
|
+
const fixed = repairTex(term.tex, false);
|
|
2645
|
+
const el = `document.getElementById("${sid}-chip-${term.tone}")`;
|
|
2646
|
+
return fixed.error === null ? `katex.render('${js(fixed.tex)}', ${el}, ${INLINE_OPTS});` : `${el}.textContent = '${js(term.tex)}';`;
|
|
2467
2647
|
}
|
|
2468
2648
|
function equationSize(tex) {
|
|
2469
2649
|
return tex.length > 120 ? 68 : tex.length > 90 ? 80 : tex.length > 55 ? 92 : 108;
|
|
2470
2650
|
}
|
|
2471
|
-
var
|
|
2651
|
+
var NO_INK2 = /\\(math[a-z]+|text[a-z]*|bm|boldsymbol|displaystyle|textstyle|scriptstyle|left|right|operatorname|limits|nolimits|htmlClass|hspace|phantom|[,;:!])/g;
|
|
2472
2652
|
var GLYPH_EM = 0.9;
|
|
2473
2653
|
function texUnits(tex) {
|
|
2474
2654
|
let ems = 0;
|
|
2475
2655
|
for (const m of tex.matchAll(/\\qquad|\\quad/g)) ems += m[0] === "\\qquad" ? 2 : 1;
|
|
2476
|
-
const glyphs = tex.replace(/\\q?quad/g, "").replace(
|
|
2656
|
+
const glyphs = tex.replace(/\\q?quad/g, "").replace(NO_INK2, "").replace(/[{}\s_^]/g, "");
|
|
2477
2657
|
return ems + glyphs.length * GLYPH_EM;
|
|
2478
2658
|
}
|
|
2479
2659
|
function statements(tex, stacked) {
|
|
@@ -2481,6 +2661,100 @@ function statements(tex, stacked) {
|
|
|
2481
2661
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
2482
2662
|
return parts.length > 0 ? parts : [tex];
|
|
2483
2663
|
}
|
|
2664
|
+
var PLAIN_FONT = 48;
|
|
2665
|
+
function mathFit(sid, targets, shown, raw2, want, floor) {
|
|
2666
|
+
return [
|
|
2667
|
+
`var dsEqFit = (function () {
|
|
2668
|
+
var box = document.getElementById("${sid}-eq");
|
|
2669
|
+
var slide = box && box.parentNode;
|
|
2670
|
+
var bw = box ? box.getBoundingClientRect().width : 0;
|
|
2671
|
+
if (!(bw > 0) || !slide) return "unmeasured";
|
|
2672
|
+
var targets = [${targets.map((id2) => `"${id2}"`).join(", ")}].map(function (id) { return document.getElementById(id); });
|
|
2673
|
+
var TEX = [${shown.map((s) => `'${js(s)}'`).join(", ")}];
|
|
2674
|
+
var spill0 = slide.scrollHeight - slide.clientHeight;
|
|
2675
|
+
function extent(each) {
|
|
2676
|
+
var w = 0;
|
|
2677
|
+
targets.forEach(function (t) {
|
|
2678
|
+
var lo = Infinity, hi = -Infinity;
|
|
2679
|
+
Array.prototype.forEach.call(t.querySelectorAll(".katex-html > .base"), function (b) {
|
|
2680
|
+
var r = b.getBoundingClientRect();
|
|
2681
|
+
if (each) w = Math.max(w, r.width);
|
|
2682
|
+
else { lo = Math.min(lo, r.left); hi = Math.max(hi, r.right); }
|
|
2683
|
+
});
|
|
2684
|
+
if (!each && hi > lo) w = Math.max(w, hi - lo);
|
|
2685
|
+
});
|
|
2686
|
+
return w;
|
|
2687
|
+
}
|
|
2688
|
+
function fits(each) {
|
|
2689
|
+
return extent(each) <= bw + 0.5 && slide.scrollHeight - slide.clientHeight <= Math.max(1, spill0 + 1);
|
|
2690
|
+
}
|
|
2691
|
+
function settle(each, size) {
|
|
2692
|
+
size = Math.max(${floor}, Math.min(${want}, Math.floor(size)));
|
|
2693
|
+
box.style.fontSize = size + "px";
|
|
2694
|
+
while (!fits(each) && size > ${floor}) {
|
|
2695
|
+
size = Math.max(${floor}, size - 2);
|
|
2696
|
+
box.style.fontSize = size + "px";
|
|
2697
|
+
}
|
|
2698
|
+
return fits(each);
|
|
2699
|
+
}
|
|
2700
|
+
if (fits(false)) return "fits";
|
|
2701
|
+
var size = parseFloat(box.style.fontSize);
|
|
2702
|
+
if (settle(false, (size * bw) / extent(false))) return "scaled";
|
|
2703
|
+
targets.forEach(function (t, i) { katex.render("\\\\displaystyle " + TEX[i], t, ${INLINE_OPTS}); });
|
|
2704
|
+
box.className += " eq-broken";
|
|
2705
|
+
box.style.fontSize = "${want}px";
|
|
2706
|
+
var wb = extent(true);
|
|
2707
|
+
if (settle(true, wb > bw ? (${want} * bw) / wb : ${want})) return "broken";
|
|
2708
|
+
box.setAttribute("${UNFIT_ATTR}", '${js(raw2)}');
|
|
2709
|
+
return "unfit";
|
|
2710
|
+
})()`
|
|
2711
|
+
];
|
|
2712
|
+
}
|
|
2713
|
+
function chipFit(sid, terms) {
|
|
2714
|
+
if (terms.length === 0) return [];
|
|
2715
|
+
return [
|
|
2716
|
+
`[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2717
|
+
var chip = document.getElementById("${sid}-chip-" + tone);
|
|
2718
|
+
var r = chip && chip.getBoundingClientRect();
|
|
2719
|
+
if (!r || !(r.height > 0)) return;
|
|
2720
|
+
var k = chip.offsetHeight / r.height;
|
|
2721
|
+
var top = r.top, bottom = r.bottom;
|
|
2722
|
+
Array.prototype.forEach.call(chip.querySelectorAll("span"), function (el) {
|
|
2723
|
+
for (var n = el.firstChild; n; n = n.nextSibling) {
|
|
2724
|
+
if (n.nodeType === 3 && n.nodeValue.trim()) {
|
|
2725
|
+
var q = el.getBoundingClientRect();
|
|
2726
|
+
top = Math.min(top, q.top);
|
|
2727
|
+
bottom = Math.max(bottom, q.bottom);
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
var cs = getComputedStyle(chip);
|
|
2733
|
+
if (top < r.top) chip.style.paddingTop = parseFloat(cs.paddingTop) + Math.ceil((r.top - top) * k) + "px";
|
|
2734
|
+
if (bottom > r.bottom) chip.style.paddingBottom = parseFloat(cs.paddingBottom) + Math.ceil((bottom - r.bottom) * k) + "px";
|
|
2735
|
+
})`
|
|
2736
|
+
];
|
|
2737
|
+
}
|
|
2738
|
+
var SWELL = 1.16;
|
|
2739
|
+
var SWELL_EM = 0.06;
|
|
2740
|
+
function swellFit(sid, terms) {
|
|
2741
|
+
return [
|
|
2742
|
+
`var dsSwell = (function () {
|
|
2743
|
+
var out = {};
|
|
2744
|
+
var box = document.getElementById("${sid}-eq");
|
|
2745
|
+
var k = box && box.offsetWidth ? box.getBoundingClientRect().width / box.offsetWidth : 1;
|
|
2746
|
+
var em = box ? parseFloat(getComputedStyle(box).fontSize) : 0;
|
|
2747
|
+
[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2748
|
+
var w = 0;
|
|
2749
|
+
Array.prototype.forEach.call(document.querySelectorAll("#${sid} .t-" + tone), function (el) {
|
|
2750
|
+
w = Math.max(w, el.getBoundingClientRect().width / k);
|
|
2751
|
+
});
|
|
2752
|
+
out[tone] = w > 0 && em > 0 ? Math.min(${SWELL}, Math.round((1 + (2 * ${SWELL_EM} * em) / w) * 1000) / 1000) : ${SWELL};
|
|
2753
|
+
});
|
|
2754
|
+
return out;
|
|
2755
|
+
})()`
|
|
2756
|
+
];
|
|
2757
|
+
}
|
|
2484
2758
|
function legendRows(sid, terms, theme) {
|
|
2485
2759
|
return terms.map(
|
|
2486
2760
|
(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>`
|
|
@@ -2496,7 +2770,10 @@ function legendCss(theme) {
|
|
|
2496
2770
|
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
2497
2771
|
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
2498
2772
|
// are one symbol each and their natural widths differ by a few pixels.
|
|
2499
|
-
|
|
2773
|
+
// `flex:none`: beside a long label the chip used to SHRINK below its own
|
|
2774
|
+
// nowrap TeX, which then ran out of the painted box into the label — on a
|
|
2775
|
+
// HypePaper deck, "pixelcoord" 91px past the chip's right edge.
|
|
2776
|
+
`.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}`
|
|
2500
2777
|
].join("\n");
|
|
2501
2778
|
}
|
|
2502
2779
|
var equationWalk = (beat, ctx) => {
|
|
@@ -2508,37 +2785,53 @@ var equationWalk = (beat, ctx) => {
|
|
|
2508
2785
|
`equation-walk ${beat.id}: no equation "${p.equationId}" in source ${ctx.source.id}`
|
|
2509
2786
|
);
|
|
2510
2787
|
}
|
|
2511
|
-
const walk =
|
|
2788
|
+
const walk = prepareWalk(eq, p.terms, beat.id);
|
|
2512
2789
|
const terms = walk.used;
|
|
2513
2790
|
const legend = legendRows(sid, terms, theme);
|
|
2514
|
-
const
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2791
|
+
const chips = terms.map((t2) => chipSetup(sid, t2));
|
|
2792
|
+
let eqHtml;
|
|
2793
|
+
let setup;
|
|
2794
|
+
let measure4;
|
|
2795
|
+
if (walk.plain) {
|
|
2796
|
+
eqHtml = `<div class="eq eq-plain" id="${sid}-eq" style="font-size:${PLAIN_FONT}px">${plainHtml(eq.tex, terms)}</div>`;
|
|
2797
|
+
setup = chips;
|
|
2798
|
+
measure4 = [...chipFit(sid, terms), ...swellFit(sid, terms)];
|
|
2799
|
+
} else {
|
|
2800
|
+
const wrapped = walk.tex;
|
|
2801
|
+
const split = statements(wrapped, isPortrait(ctx.format));
|
|
2802
|
+
const stacked = split.length > 1 && split.every((s) => texError(s, true) === null);
|
|
2803
|
+
const raw2 = statements(walk.raw, stacked);
|
|
2804
|
+
const shown = stacked ? split : [wrapped];
|
|
2805
|
+
const longest = raw2.reduce((a, b) => b.length > a.length ? b : a);
|
|
2806
|
+
const want = equationSize(longest);
|
|
2807
|
+
const size2 = Math.max(
|
|
2808
|
+
MIN_FONT,
|
|
2809
|
+
Math.min(want, Math.floor(contentW(ctx.format) / Math.max(...raw2.map(texUnits))))
|
|
2810
|
+
);
|
|
2811
|
+
const body = shown.length === 1 ? "" : shown.map((_, i) => `<div id="${sid}-eq${i}"></div>`).join("\n ");
|
|
2812
|
+
eqHtml = `<div class="eq${shown.length === 1 ? "" : " eqstack"}" id="${sid}-eq" style="font-size:${size2}px">${body}</div>`;
|
|
2813
|
+
const targets = shown.map((_, i) => `${sid}-eq${shown.length === 1 ? "" : i}`);
|
|
2814
|
+
setup = [
|
|
2815
|
+
`var OPTS = ${OPTS};`,
|
|
2816
|
+
...shown.map(
|
|
2817
|
+
(part, i) => `katex.render('${js(part)}', document.getElementById("${targets[i]}"), OPTS);`
|
|
2818
|
+
),
|
|
2819
|
+
...chips
|
|
2820
|
+
];
|
|
2821
|
+
measure4 = [
|
|
2822
|
+
...chipFit(sid, terms),
|
|
2823
|
+
...mathFit(sid, targets, shown, walk.raw, want, MIN_FONT),
|
|
2824
|
+
// Last: it measures each term at the size the fit settled on.
|
|
2825
|
+
...swellFit(sid, terms)
|
|
2826
|
+
];
|
|
2827
|
+
}
|
|
2526
2828
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
2527
2829
|
<div class="eqslide">
|
|
2528
|
-
|
|
2830
|
+
${eqHtml}
|
|
2529
2831
|
<div class="legend">
|
|
2530
2832
|
${legend}
|
|
2531
2833
|
</div>
|
|
2532
2834
|
</div>`;
|
|
2533
|
-
const setup = [
|
|
2534
|
-
`var OPTS = ${OPTS};`,
|
|
2535
|
-
...shown.map(
|
|
2536
|
-
(part, i) => `katex.render('${js(part)}', document.getElementById("${sid}-eq${shown.length === 1 ? "" : i}"), OPTS);`
|
|
2537
|
-
),
|
|
2538
|
-
...terms.map(
|
|
2539
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2540
|
-
)
|
|
2541
|
-
];
|
|
2542
2835
|
const tl = [
|
|
2543
2836
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
2544
2837
|
tween(`#${sid}-eq`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8)
|
|
@@ -2550,6 +2843,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2550
2843
|
);
|
|
2551
2844
|
const holds = [];
|
|
2552
2845
|
const spot = spotlighter(sid, ".term");
|
|
2846
|
+
const swell = (t2) => raw(`dsSwell.${t2.tone}`);
|
|
2553
2847
|
terms.forEach((term, i) => {
|
|
2554
2848
|
const at = first + i * step;
|
|
2555
2849
|
const colour = theme.tones[term.tone];
|
|
@@ -2566,13 +2860,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2566
2860
|
tween(
|
|
2567
2861
|
`#${sid} .t-${term.tone}`,
|
|
2568
2862
|
{ color: theme.fg, scale: 1 },
|
|
2569
|
-
{ color: colour, scale:
|
|
2863
|
+
{ color: colour, scale: swell(term), duration: 0.5 },
|
|
2570
2864
|
at
|
|
2571
2865
|
)
|
|
2572
2866
|
);
|
|
2573
2867
|
const prev = terms[i - 1];
|
|
2574
2868
|
if (prev) {
|
|
2575
|
-
tl.push(
|
|
2869
|
+
tl.push(
|
|
2870
|
+
tween(`#${sid} .t-${prev.tone}`, { scale: swell(prev) }, { scale: 1, duration: 0.4 }, at)
|
|
2871
|
+
);
|
|
2576
2872
|
}
|
|
2577
2873
|
holds.push(at + 0.6);
|
|
2578
2874
|
});
|
|
@@ -2580,7 +2876,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2580
2876
|
if (last) {
|
|
2581
2877
|
const at = first + terms.length * step;
|
|
2582
2878
|
tl.push(
|
|
2583
|
-
tween(`#${sid} .t-${last.tone}`, { scale:
|
|
2879
|
+
tween(`#${sid} .t-${last.tone}`, { scale: swell(last) }, { scale: 1, duration: 0.4 }, at),
|
|
2584
2880
|
...spot.restore(at)
|
|
2585
2881
|
);
|
|
2586
2882
|
}
|
|
@@ -2588,6 +2884,8 @@ var equationWalk = (beat, ctx) => {
|
|
|
2588
2884
|
html,
|
|
2589
2885
|
tl,
|
|
2590
2886
|
setup,
|
|
2887
|
+
measure: measure4,
|
|
2888
|
+
...walk.notes.length ? { warnings: walk.notes } : {},
|
|
2591
2889
|
holds: holdsWithin(holds, beat.seconds),
|
|
2592
2890
|
css: [
|
|
2593
2891
|
chromeCss(theme),
|
|
@@ -2607,6 +2905,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2607
2905
|
// has its margin zeroed above, so without an explicit gap the two lines
|
|
2608
2906
|
// would touch and read as one equation broken mid-expression.
|
|
2609
2907
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
2908
|
+
// Set by `mathFit` when one line will not fit at the floor: the display is
|
|
2909
|
+
// re-rendered inline, where KaTeX breaks after a top-level relation or
|
|
2910
|
+
// operator, and the lines it breaks into are centred and spaced as lines.
|
|
2911
|
+
".eq-broken{line-height:1.5}",
|
|
2912
|
+
// An equation KaTeX refuses, shown as its source. Wraps anywhere, because
|
|
2913
|
+
// TeX source has few spaces and must not run off the slide either. The
|
|
2914
|
+
// deck's own face, not a monospace one: a family the bundle does not
|
|
2915
|
+
// declare falls back silently (invariant 9).
|
|
2916
|
+
".eq-plain{white-space:pre-wrap;overflow-wrap:anywhere}",
|
|
2610
2917
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
2611
2918
|
".term{display:inline-block}",
|
|
2612
2919
|
legendCss(theme),
|
|
@@ -2621,26 +2928,67 @@ var equationWalk = (beat, ctx) => {
|
|
|
2621
2928
|
|
|
2622
2929
|
// src/emit/archetypes/equation-morph.ts
|
|
2623
2930
|
var MORPH_SECONDS = 1.6;
|
|
2624
|
-
|
|
2625
|
-
const { sid, theme } = ctx;
|
|
2931
|
+
function prepareMorph(beat, source) {
|
|
2626
2932
|
const p = beat.params;
|
|
2627
2933
|
const find3 = (id2) => {
|
|
2628
|
-
const eq =
|
|
2934
|
+
const eq = source.equations.find((e) => e.id === id2);
|
|
2629
2935
|
if (!eq)
|
|
2630
|
-
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${
|
|
2936
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${source.id}`);
|
|
2631
2937
|
return eq;
|
|
2632
2938
|
};
|
|
2633
|
-
const
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2939
|
+
const notes = [];
|
|
2940
|
+
const drawable = (eq) => {
|
|
2941
|
+
const fixed = repairTex(eq.tex, true);
|
|
2942
|
+
if (fixed.error !== null) {
|
|
2943
|
+
throw new Error(
|
|
2944
|
+
`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)}`
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
notes.push(...fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`));
|
|
2948
|
+
return { id: eq.id, tex: fixed.tex };
|
|
2949
|
+
};
|
|
2950
|
+
const a = drawable(find3(p.fromId));
|
|
2951
|
+
const b = drawable(find3(p.toId));
|
|
2952
|
+
const repaired = p.terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
2953
|
+
const original = (t2) => p.terms[repaired.indexOf(t2)];
|
|
2954
|
+
let keyed = repaired.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
2955
|
+
if (keyed.length === 0) {
|
|
2637
2956
|
throw new Error(
|
|
2638
2957
|
`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)}`
|
|
2639
2958
|
);
|
|
2640
2959
|
}
|
|
2960
|
+
notes.push(
|
|
2961
|
+
...repaired.filter((t2) => !keyed.includes(t2)).map(
|
|
2962
|
+
(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`
|
|
2963
|
+
)
|
|
2964
|
+
);
|
|
2641
2965
|
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
2642
|
-
|
|
2643
|
-
|
|
2966
|
+
let wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
2967
|
+
let wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
2968
|
+
const paired = keyed.filter((t2) => wa.used.includes(t2) && wb.used.includes(t2));
|
|
2969
|
+
if (paired.length < keyed.length) {
|
|
2970
|
+
const unpaired = keyed.filter((t2) => !paired.includes(t2));
|
|
2971
|
+
notes.push(
|
|
2972
|
+
...unpaired.map(
|
|
2973
|
+
(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`
|
|
2974
|
+
)
|
|
2975
|
+
);
|
|
2976
|
+
if (paired.length === 0) {
|
|
2977
|
+
throw new Error(
|
|
2978
|
+
`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(", ")}`
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
keyed = paired;
|
|
2982
|
+
wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
2983
|
+
wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
2984
|
+
}
|
|
2985
|
+
const both = keyed.map(original);
|
|
2986
|
+
return { a, b, wa: wa.tex, wb: wb.tex, both, notes };
|
|
2987
|
+
}
|
|
2988
|
+
var equationMorph = (beat, ctx) => {
|
|
2989
|
+
const { sid, theme } = ctx;
|
|
2990
|
+
const p = beat.params;
|
|
2991
|
+
const { a, b, wa, wb, both, notes } = prepareMorph(beat, ctx.source);
|
|
2644
2992
|
const size2 = Math.max(
|
|
2645
2993
|
MIN_FONT,
|
|
2646
2994
|
Math.min(
|
|
@@ -2662,9 +3010,7 @@ var equationMorph = (beat, ctx) => {
|
|
|
2662
3010
|
`var OPTS = ${OPTS};`,
|
|
2663
3011
|
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
2664
3012
|
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
2665
|
-
...both.map(
|
|
2666
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2667
|
-
)
|
|
3013
|
+
...both.map((t2) => chipSetup(sid, t2))
|
|
2668
3014
|
];
|
|
2669
3015
|
const first = 1.8;
|
|
2670
3016
|
const at = Math.round(
|
|
@@ -2697,8 +3043,11 @@ var equationMorph = (beat, ctx) => {
|
|
|
2697
3043
|
setup,
|
|
2698
3044
|
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
2699
3045
|
// the ready gate, and the plugin tween above finds it on the host.
|
|
2700
|
-
|
|
3046
|
+
// The chips are fitted before the plan is built: growing one moves the
|
|
3047
|
+
// legend, and nothing the morph measures may move after it has measured.
|
|
3048
|
+
measure: [...chipFit(sid, both), `DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
2701
3049
|
plugins: ["dsMorph"],
|
|
3050
|
+
...notes.length ? { warnings: notes } : {},
|
|
2702
3051
|
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
2703
3052
|
css: [
|
|
2704
3053
|
chromeCss(theme),
|
|
@@ -3279,16 +3628,16 @@ var lineChart = (beat, ctx) => {
|
|
|
3279
3628
|
</svg>${readout}
|
|
3280
3629
|
</div>`;
|
|
3281
3630
|
const draw2 = 0.8;
|
|
3282
|
-
const
|
|
3283
|
-
const idealStep = Math.min(0.45, DRAW_SECONDS /
|
|
3631
|
+
const count2 = p.points.length;
|
|
3632
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count2);
|
|
3284
3633
|
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
3285
|
-
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length,
|
|
3634
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count2 - 1) : 0;
|
|
3286
3635
|
const tailAfter = (s) => Math.max(
|
|
3287
|
-
s *
|
|
3636
|
+
s * count2 + 0.4,
|
|
3288
3637
|
// the ring: its walk, then the 0.4s it takes to leave
|
|
3289
|
-
0.3 + s * (
|
|
3638
|
+
0.3 + s * (count2 - 1),
|
|
3290
3639
|
// the dots
|
|
3291
|
-
0.5 + s * (
|
|
3640
|
+
0.5 + s * (count2 - 1),
|
|
3292
3641
|
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
3293
3642
|
);
|
|
3294
3643
|
const room = beat.seconds - spine;
|
|
@@ -3351,8 +3700,8 @@ var lineChart = (beat, ctx) => {
|
|
|
3351
3700
|
)
|
|
3352
3701
|
);
|
|
3353
3702
|
}
|
|
3354
|
-
const walk = cmp ? sec(step *
|
|
3355
|
-
if (
|
|
3703
|
+
const walk = cmp ? sec(step * count2) : DRAW_SECONDS;
|
|
3704
|
+
if (count2 > 1) {
|
|
3356
3705
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
3357
3706
|
tl.push(
|
|
3358
3707
|
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
@@ -3367,7 +3716,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3367
3716
|
)
|
|
3368
3717
|
);
|
|
3369
3718
|
}
|
|
3370
|
-
const drawn = settled + (cmp ? tailAfter(step) : step *
|
|
3719
|
+
const drawn = settled + (cmp ? tailAfter(step) : step * count2 + 0.4);
|
|
3371
3720
|
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
3372
3721
|
if (p.readout) {
|
|
3373
3722
|
tl.push(
|
|
@@ -3383,7 +3732,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3383
3732
|
return {
|
|
3384
3733
|
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
3385
3734
|
warnings: [
|
|
3386
|
-
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${
|
|
3735
|
+
`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.`
|
|
3387
3736
|
]
|
|
3388
3737
|
};
|
|
3389
3738
|
}
|
|
@@ -3463,7 +3812,7 @@ var V_BOX_H = 268;
|
|
|
3463
3812
|
var V_LOOP_SIDE = 56;
|
|
3464
3813
|
var V_LOOP_PAD = 18;
|
|
3465
3814
|
function columnLayout(stageW, stageH, stages, face, loop) {
|
|
3466
|
-
const
|
|
3815
|
+
const count2 = stages.length;
|
|
3467
3816
|
const loopW = loop ? Math.max(240, Math.round(stageW * 0.32)) : 0;
|
|
3468
3817
|
const boxX = M;
|
|
3469
3818
|
const boxW = stageW - 2 * M - (loop ? V_LOOP_SIDE + V_LOOP_PAD + loopW : 0);
|
|
@@ -3480,11 +3829,11 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3480
3829
|
return Math.ceil(2 * PAD_Y2 + label + (note2 > 0 ? NOTE_TOP + note2 * NOTE * NOTE_LH : 0));
|
|
3481
3830
|
})
|
|
3482
3831
|
);
|
|
3483
|
-
const room = (stageH - 2 * M - (
|
|
3832
|
+
const room = (stageH - 2 * M - (count2 - 1) * V_GAP) / count2;
|
|
3484
3833
|
const boxH = Math.max(need, Math.min(room, V_BOX_H));
|
|
3485
3834
|
return {
|
|
3486
3835
|
size: size2,
|
|
3487
|
-
boxes: Array.from({ length:
|
|
3836
|
+
boxes: Array.from({ length: count2 }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
|
|
3488
3837
|
boxH,
|
|
3489
3838
|
boxX,
|
|
3490
3839
|
boxW,
|
|
@@ -3496,7 +3845,7 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3496
3845
|
loopLines: loop ? wrap(loop.label, NOTE, loopW, 500, 0, face) : [],
|
|
3497
3846
|
loopX: boxX + boxW + V_LOOP_SIDE,
|
|
3498
3847
|
loopW,
|
|
3499
|
-
svgH: 2 * M +
|
|
3848
|
+
svgH: 2 * M + count2 * boxH + (count2 - 1) * V_GAP
|
|
3500
3849
|
};
|
|
3501
3850
|
}
|
|
3502
3851
|
function balance(label, k, face) {
|
|
@@ -4214,8 +4563,8 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4214
4563
|
if (inline2.fits) return inline2;
|
|
4215
4564
|
return inline2.wide && inline2.blockH < stacked.blockH ? inline2 : stacked;
|
|
4216
4565
|
}
|
|
4217
|
-
function labelWeight(i,
|
|
4218
|
-
return i ===
|
|
4566
|
+
function labelWeight(i, count2) {
|
|
4567
|
+
return i === count2 - 1 ? 700 : 600;
|
|
4219
4568
|
}
|
|
4220
4569
|
function floorFor(p, format) {
|
|
4221
4570
|
if (!p.tilt) return MIN_FONT;
|
|
@@ -4225,7 +4574,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4225
4574
|
const width = contentW(format);
|
|
4226
4575
|
const boxH = contentH(format);
|
|
4227
4576
|
const floor = floorFor(p, format);
|
|
4228
|
-
const
|
|
4577
|
+
const count2 = p.layers.length;
|
|
4229
4578
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4230
4579
|
const riseMax = RISE_MAX[k];
|
|
4231
4580
|
const syMax = SY_MAX[k];
|
|
@@ -4233,7 +4582,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4233
4582
|
const noteW = (l) => inline2 && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4234
4583
|
const want = Math.max(
|
|
4235
4584
|
...p.layers.map(
|
|
4236
|
-
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i,
|
|
4585
|
+
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count2), 0, false, face) + noteW(l)
|
|
4237
4586
|
)
|
|
4238
4587
|
);
|
|
4239
4588
|
const colCap = inline2 ? width * 0.56 : width * 0.5;
|
|
@@ -4242,7 +4591,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4242
4591
|
const labelX = width - colW;
|
|
4243
4592
|
const labelRoom = Math.min(
|
|
4244
4593
|
...p.layers.map(
|
|
4245
|
-
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i,
|
|
4594
|
+
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count2), 0, false, face))
|
|
4246
4595
|
)
|
|
4247
4596
|
);
|
|
4248
4597
|
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
@@ -4250,7 +4599,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4250
4599
|
const nw = noteW(l);
|
|
4251
4600
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
4252
4601
|
return {
|
|
4253
|
-
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i,
|
|
4602
|
+
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count2), 0, face),
|
|
4254
4603
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4255
4604
|
// short line" — and wrapping one would put its second line under the label.
|
|
4256
4605
|
note: l.note === void 0 ? [] : inline2 ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
@@ -4268,13 +4617,13 @@ function solve2(p, format, inline2, face) {
|
|
|
4268
4617
|
const noteH = noteHeight(p.note, noteWidth(format), 28, face);
|
|
4269
4618
|
const free = boxH - chromeH - 20 - noteH;
|
|
4270
4619
|
const avail = Math.max(300, free);
|
|
4271
|
-
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1,
|
|
4620
|
+
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count2 - 1);
|
|
4272
4621
|
const rise = clamp(room, RISE_MIN, riseMax);
|
|
4273
4622
|
const sy = clamp(rise * 0.62, 46, syMax);
|
|
4274
4623
|
const t2 = clamp(rise * 0.14, 10, tMax);
|
|
4275
4624
|
const span = width - NUM_W - GAP3 - colW;
|
|
4276
4625
|
const sx = clamp(span * 0.2, 110, 220);
|
|
4277
|
-
const height = 2 * pad + (
|
|
4626
|
+
const height = 2 * pad + (count2 - 1) * rise + sy + t2;
|
|
4278
4627
|
return {
|
|
4279
4628
|
// A rise shorter than a label block prints the note through the label below.
|
|
4280
4629
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
@@ -4299,7 +4648,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4299
4648
|
labelSize,
|
|
4300
4649
|
blockH,
|
|
4301
4650
|
pad,
|
|
4302
|
-
yBase: pad + (
|
|
4651
|
+
yBase: pad + (count2 - 1) * rise + sy,
|
|
4303
4652
|
lines
|
|
4304
4653
|
};
|
|
4305
4654
|
}
|
|
@@ -4316,11 +4665,11 @@ var stack = (beat, ctx) => {
|
|
|
4316
4665
|
const p = beat.params;
|
|
4317
4666
|
const face = faceOf(ctx.theme.fontStack);
|
|
4318
4667
|
const L = stackLayout(p, ctx.format, face);
|
|
4319
|
-
const
|
|
4320
|
-
const last =
|
|
4668
|
+
const count2 = p.layers.length;
|
|
4669
|
+
const last = count2 - 1;
|
|
4321
4670
|
if (!L.fits) {
|
|
4322
4671
|
throw new Error(
|
|
4323
|
-
`stack ${beat.id}: ${
|
|
4672
|
+
`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.`
|
|
4324
4673
|
);
|
|
4325
4674
|
}
|
|
4326
4675
|
const parts = {};
|
|
@@ -4348,7 +4697,7 @@ var stack = (beat, ctx) => {
|
|
|
4348
4697
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
4349
4698
|
{
|
|
4350
4699
|
size: L.labelSize,
|
|
4351
|
-
weight: labelWeight(i,
|
|
4700
|
+
weight: labelWeight(i, count2),
|
|
4352
4701
|
fill: top ? theme.tones.b : theme.fg,
|
|
4353
4702
|
maxWidth: block.labelMaxW,
|
|
4354
4703
|
lineHeight: 1.16,
|
|
@@ -4402,10 +4751,10 @@ var stack = (beat, ctx) => {
|
|
|
4402
4751
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4403
4752
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4404
4753
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
|
|
4405
|
-
const centre2 = (
|
|
4754
|
+
const centre2 = (count2 - 1) / 2;
|
|
4406
4755
|
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4407
4756
|
const first = 0.9;
|
|
4408
|
-
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) /
|
|
4757
|
+
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count2));
|
|
4409
4758
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
4410
4759
|
const holds = [];
|
|
4411
4760
|
const spot = spotlighter(sid);
|
|
@@ -4437,11 +4786,11 @@ var stack = (beat, ctx) => {
|
|
|
4437
4786
|
);
|
|
4438
4787
|
}
|
|
4439
4788
|
if (p.note) {
|
|
4440
|
-
const at = first +
|
|
4789
|
+
const at = first + count2 * step;
|
|
4441
4790
|
tl.push(tween(`#${sid}-note`, { opacity: 0 }, { opacity: 1, duration: 0.6 }, at));
|
|
4442
4791
|
holds.push(at + 0.7);
|
|
4443
4792
|
}
|
|
4444
|
-
if (
|
|
4793
|
+
if (count2 > 1) tl.push(...spot.restore(first + count2 * step));
|
|
4445
4794
|
return {
|
|
4446
4795
|
html,
|
|
4447
4796
|
parts,
|
|
@@ -5782,8 +6131,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5782
6131
|
const text2 = beat.narration?.trim();
|
|
5783
6132
|
if (!text2) continue;
|
|
5784
6133
|
const segments2 = [];
|
|
5785
|
-
const
|
|
5786
|
-
const plan = planSegments(text2, Math.min(
|
|
6134
|
+
const count2 = stopsFor(beat, source, format, `s${i + 1}`, theme);
|
|
6135
|
+
const plan = planSegments(text2, Math.min(count2, paced.speakingStops));
|
|
5787
6136
|
for (const [stop, line2] of plan.entries()) {
|
|
5788
6137
|
if (!line2) continue;
|
|
5789
6138
|
const speech = await synthesize(line2, {
|
|
@@ -5805,7 +6154,7 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5805
6154
|
}
|
|
5806
6155
|
if (segments2.length > 0) {
|
|
5807
6156
|
beats[beat.id] = segments2;
|
|
5808
|
-
staged[beat.id] =
|
|
6157
|
+
staged[beat.id] = count2;
|
|
5809
6158
|
}
|
|
5810
6159
|
}
|
|
5811
6160
|
return {
|
|
@@ -6695,11 +7044,11 @@ function stageScene(scene, speed) {
|
|
|
6695
7044
|
function speechPlan(open, holds, segments2) {
|
|
6696
7045
|
const starts = [];
|
|
6697
7046
|
let at = open;
|
|
6698
|
-
for (const [i,
|
|
6699
|
-
const hold = holds[Math.min(
|
|
7047
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7048
|
+
const hold = holds[Math.min(segment2.stop, holds.length - 1)] ?? 0;
|
|
6700
7049
|
if (i > 0) at = Math.max(at, hold);
|
|
6701
7050
|
starts.push(at);
|
|
6702
|
-
at +=
|
|
7051
|
+
at += segment2.seconds;
|
|
6703
7052
|
}
|
|
6704
7053
|
return { starts, end: at };
|
|
6705
7054
|
}
|
|
@@ -6927,10 +7276,10 @@ function pack(words2, width) {
|
|
|
6927
7276
|
function splitCue(cue, max = CUE_MAX_CHARS) {
|
|
6928
7277
|
if (cue.text.length <= max) return [cue];
|
|
6929
7278
|
const words2 = cue.text.split(/\s+/).filter(Boolean);
|
|
6930
|
-
const
|
|
7279
|
+
const count2 = pack(words2, max).length;
|
|
6931
7280
|
let width = max;
|
|
6932
|
-
for (let w = Math.ceil(cue.text.length /
|
|
6933
|
-
if (pack(words2, w).length <=
|
|
7281
|
+
for (let w = Math.ceil(cue.text.length / count2); w < max; w++) {
|
|
7282
|
+
if (pack(words2, w).length <= count2) {
|
|
6934
7283
|
width = w;
|
|
6935
7284
|
break;
|
|
6936
7285
|
}
|
|
@@ -7022,17 +7371,17 @@ function place(scenes, spoken) {
|
|
|
7022
7371
|
);
|
|
7023
7372
|
}
|
|
7024
7373
|
const { starts } = speechPlan(scene.open, scene.holds, segments2);
|
|
7025
|
-
for (const [i,
|
|
7026
|
-
const index = Math.min(
|
|
7374
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7375
|
+
const index = Math.min(segment2.stop, scene.holds.length - 1);
|
|
7027
7376
|
out.push({
|
|
7028
|
-
id: `${scene.id}.${
|
|
7377
|
+
id: `${scene.id}.${segment2.stop}`,
|
|
7029
7378
|
scene: scene.id,
|
|
7030
|
-
stop:
|
|
7031
|
-
audio:
|
|
7379
|
+
stop: segment2.stop,
|
|
7380
|
+
audio: segment2.audio,
|
|
7032
7381
|
hold: round5(scene.start + scene.holds[index]),
|
|
7033
7382
|
start: round5(scene.start + starts[i]),
|
|
7034
|
-
duration:
|
|
7035
|
-
cues:
|
|
7383
|
+
duration: segment2.seconds,
|
|
7384
|
+
cues: segment2.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
|
|
7036
7385
|
});
|
|
7037
7386
|
}
|
|
7038
7387
|
}
|
|
@@ -7109,6 +7458,15 @@ function planTiming(input) {
|
|
|
7109
7458
|
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
7110
7459
|
// disagree with the box the archetypes actually drew into.
|
|
7111
7460
|
captionReserve: format.captionReserve ?? 0,
|
|
7461
|
+
// An unbudgeted format's limit is Infinity, which JSON writes as `null`.
|
|
7462
|
+
// Left out instead: recorded as a format with no limit, never with a limit
|
|
7463
|
+
// of null or zero.
|
|
7464
|
+
format: {
|
|
7465
|
+
id: format.id,
|
|
7466
|
+
minWeight: format.minWeight,
|
|
7467
|
+
...Number.isFinite(format.maxSeconds) && { maxSeconds: format.maxSeconds },
|
|
7468
|
+
...format.warnSeconds !== void 0 && { warnSeconds: format.warnSeconds }
|
|
7469
|
+
},
|
|
7112
7470
|
scenes,
|
|
7113
7471
|
segments: segments2
|
|
7114
7472
|
};
|
|
@@ -7135,15 +7493,15 @@ function framePlan(timing, fps) {
|
|
|
7135
7493
|
);
|
|
7136
7494
|
}
|
|
7137
7495
|
pieces.push({ from: first, motion: last - first, freeze: 0 });
|
|
7138
|
-
for (const
|
|
7139
|
-
const at = f(
|
|
7496
|
+
for (const segment2 of segments2) {
|
|
7497
|
+
const at = f(segment2.start);
|
|
7140
7498
|
audio.push({
|
|
7141
|
-
id:
|
|
7142
|
-
audio:
|
|
7499
|
+
id: segment2.id,
|
|
7500
|
+
audio: segment2.audio,
|
|
7143
7501
|
startFrame: at,
|
|
7144
7502
|
delayMs: Math.round(at / fps * 1e3)
|
|
7145
7503
|
});
|
|
7146
|
-
for (const cue of
|
|
7504
|
+
for (const cue of segment2.cues) {
|
|
7147
7505
|
const start = at / fps + cue.start;
|
|
7148
7506
|
const end = at / fps + cue.end;
|
|
7149
7507
|
if (end > start) cues.push({ start, end, text: cue.text });
|
|
@@ -7717,6 +8075,22 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
7717
8075
|
async function chromePath(need = "open the deck with") {
|
|
7718
8076
|
return (await resolveChrome(need)).path;
|
|
7719
8077
|
}
|
|
8078
|
+
function sandboxMissing(err) {
|
|
8079
|
+
return /No usable sandbox/.test(err instanceof Error ? err.message : String(err));
|
|
8080
|
+
}
|
|
8081
|
+
var SANDBOX_FIX = "`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`, or give Chrome an AppArmor profile";
|
|
8082
|
+
async function launchOwnPage(launch, args, what) {
|
|
8083
|
+
try {
|
|
8084
|
+
return await launch(args);
|
|
8085
|
+
} catch (err) {
|
|
8086
|
+
if (!sandboxMissing(err)) throw err;
|
|
8087
|
+
process.stderr.write(
|
|
8088
|
+
`chrome: this host gives Chrome no sandbox, so ${what} runs without one, as hyperframes' render always does. To keep the sandbox: ${SANDBOX_FIX}.
|
|
8089
|
+
`
|
|
8090
|
+
);
|
|
8091
|
+
return launch([...args, "--no-sandbox"]);
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
7720
8094
|
function runtimePath() {
|
|
7721
8095
|
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
7722
8096
|
}
|
|
@@ -7734,10 +8108,9 @@ async function openDeck(dir, opts = {}) {
|
|
|
7734
8108
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
7735
8109
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
7736
8110
|
const chrome2 = await resolveChrome();
|
|
7737
|
-
const browser = await
|
|
7738
|
-
executablePath: chrome2.path,
|
|
7739
|
-
|
|
7740
|
-
args: [
|
|
8111
|
+
const browser = await launchOwnPage(
|
|
8112
|
+
(args) => puppeteer.launch({ executablePath: chrome2.path, headless: true, args }),
|
|
8113
|
+
[
|
|
7741
8114
|
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
7742
8115
|
// renderer's.
|
|
7743
8116
|
"--force-device-scale-factor=1",
|
|
@@ -7750,8 +8123,9 @@ async function openDeck(dir, opts = {}) {
|
|
|
7750
8123
|
"--use-gl=angle",
|
|
7751
8124
|
"--use-angle=swiftshader",
|
|
7752
8125
|
"--enable-unsafe-swiftshader"
|
|
7753
|
-
]
|
|
7754
|
-
|
|
8126
|
+
],
|
|
8127
|
+
"the gates' page"
|
|
8128
|
+
);
|
|
7755
8129
|
try {
|
|
7756
8130
|
const page = await browser.newPage();
|
|
7757
8131
|
await page.evaluateOnNewDocument("self.__name = self.__name || ((fn) => fn);");
|
|
@@ -8772,6 +9146,14 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
8772
9146
|
// happens to be that run.
|
|
8773
9147
|
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
8774
9148
|
]
|
|
9149
|
+
// No `--no-sandbox` fallback here, unlike `launchOwnPage`: this is a page
|
|
9150
|
+
// from the web, and a renderer bug it triggers is exactly what the sandbox
|
|
9151
|
+
// contains. Refused, with the fix, rather than Chrome's raw FATAL.
|
|
9152
|
+
}).catch((err) => {
|
|
9153
|
+
if (!sandboxMissing(err)) throw err;
|
|
9154
|
+
throw new Error(
|
|
9155
|
+
`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.`
|
|
9156
|
+
);
|
|
8775
9157
|
});
|
|
8776
9158
|
try {
|
|
8777
9159
|
const page = await browser.newPage();
|
|
@@ -10230,8 +10612,13 @@ function renderSource(source) {
|
|
|
10230
10612
|
}
|
|
10231
10613
|
}
|
|
10232
10614
|
out.push("", "== EQUATIONS ==");
|
|
10233
|
-
for (const e of source.equations)
|
|
10234
|
-
|
|
10615
|
+
for (const e of source.equations) {
|
|
10616
|
+
const fixed = repairTex(e.tex, true);
|
|
10617
|
+
const note2 = fixed.error === null ? "" : " (KaTeX cannot draw this one: a walk shows it as plain source, a morph cannot use it)";
|
|
10618
|
+
out.push(
|
|
10619
|
+
`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${fixed.error === null ? fixed.tex : e.tex}${note2}`
|
|
10620
|
+
);
|
|
10621
|
+
}
|
|
10235
10622
|
if (!source.equations.length)
|
|
10236
10623
|
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
10237
10624
|
out.push("", "== TABLES ==");
|
|
@@ -10805,10 +11192,10 @@ function drawSvg(req) {
|
|
|
10805
11192
|
const seed = createHash7("sha256").update([req.prompt, req.style, req.aspect].join("|")).digest();
|
|
10806
11193
|
const rand = mulberry32(seed.readUInt32BE(0));
|
|
10807
11194
|
const accent = ACCENTS[Math.floor(rand() * ACCENTS.length)];
|
|
10808
|
-
const
|
|
11195
|
+
const count2 = 6 + Math.floor(rand() * 5);
|
|
10809
11196
|
const unit = Math.min(W, H);
|
|
10810
11197
|
const parts = [`<rect width="${W}" height="${H}" fill="${PAPER}"/>`];
|
|
10811
|
-
for (let i = 0; i <
|
|
11198
|
+
for (let i = 0; i < count2; i++) {
|
|
10812
11199
|
const kind = Math.floor(rand() * 3);
|
|
10813
11200
|
const cx = Math.round(W * (0.15 + 0.7 * rand()));
|
|
10814
11201
|
const cy = Math.round(H * (0.15 + 0.7 * rand()));
|
|
@@ -11031,10 +11418,10 @@ function readCanvas(html) {
|
|
|
11031
11418
|
function profilesFor(width, height, formats = FORMATS) {
|
|
11032
11419
|
return Object.values(formats).filter((f) => f.width === width && f.height === height);
|
|
11033
11420
|
}
|
|
11034
|
-
function scanBudget(html, storyboard, kept, formats = FORMATS) {
|
|
11421
|
+
function scanBudget(html, storyboard, kept, formats = FORMATS, recorded) {
|
|
11035
11422
|
const canvas = readCanvas(html);
|
|
11036
11423
|
if (!canvas) return [];
|
|
11037
|
-
const profiles = profilesFor(canvas.width, canvas.height, formats);
|
|
11424
|
+
const profiles = recorded && recorded.width === canvas.width && recorded.height === canvas.height ? [recorded] : profilesFor(canvas.width, canvas.height, formats);
|
|
11038
11425
|
if (profiles.length === 0) {
|
|
11039
11426
|
return [
|
|
11040
11427
|
{
|
|
@@ -11762,6 +12149,20 @@ function captionBottom(sid, selector, fallbackPx) {
|
|
|
11762
12149
|
}
|
|
11763
12150
|
return bottom > 0 ? bottom : fallbackPx;
|
|
11764
12151
|
}
|
|
12152
|
+
function collectUnfit(attr) {
|
|
12153
|
+
return Array.from(document.querySelectorAll(`[${attr}]`)).map((el) => ({
|
|
12154
|
+
sid: el.closest("[data-composition-id]")?.getAttribute("data-composition-id") ?? "?",
|
|
12155
|
+
tex: el.getAttribute(attr) ?? ""
|
|
12156
|
+
}));
|
|
12157
|
+
}
|
|
12158
|
+
function gradeUnfit(rows) {
|
|
12159
|
+
return rows.map((r) => ({
|
|
12160
|
+
severity: "error",
|
|
12161
|
+
gate: "fidelity",
|
|
12162
|
+
rule: "math_unfit",
|
|
12163
|
+
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.`
|
|
12164
|
+
}));
|
|
12165
|
+
}
|
|
11765
12166
|
async function fidelity(dir, opts = {}) {
|
|
11766
12167
|
const started = Date.now();
|
|
11767
12168
|
const floor = opts.floor ?? INK_FLOOR;
|
|
@@ -11785,6 +12186,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
11785
12186
|
try {
|
|
11786
12187
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
11787
12188
|
const { page, height } = deck;
|
|
12189
|
+
await page.waitForFunction("window.__hfTimelinesBuilding === false", {
|
|
12190
|
+
timeout: opts.timeoutMs ?? 6e4
|
|
12191
|
+
});
|
|
12192
|
+
const unfit = await page.evaluate(collectUnfit, UNFIT_ATTR);
|
|
11788
12193
|
const measured = [];
|
|
11789
12194
|
const collided = [];
|
|
11790
12195
|
const apparent = [];
|
|
@@ -11828,7 +12233,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
11828
12233
|
...gradeFidelity(measured, floor),
|
|
11829
12234
|
...gradeReserve(measured),
|
|
11830
12235
|
...gradeOverprint(collided),
|
|
11831
|
-
...gradeApparent(apparent)
|
|
12236
|
+
...gradeApparent(apparent),
|
|
12237
|
+
...gradeUnfit(unfit)
|
|
11832
12238
|
],
|
|
11833
12239
|
elapsedMs: Date.now() - started
|
|
11834
12240
|
};
|
|
@@ -12175,9 +12581,13 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12175
12581
|
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12176
12582
|
await listFiles(dir)
|
|
12177
12583
|
);
|
|
12178
|
-
const
|
|
12584
|
+
const timing = await readTiming(dir);
|
|
12585
|
+
const recorded = typeof timing?.format?.id === "string" ? { ...timing.format, width: timing.width, height: timing.height } : void 0;
|
|
12586
|
+
const budget2 = html.flatMap(
|
|
12587
|
+
([, text2]) => scanBudget(text2, storyboard, kept, void 0, recorded)
|
|
12588
|
+
);
|
|
12179
12589
|
const type = html.flatMap(([file, text2]) => scanTypeFloor(text2, file));
|
|
12180
|
-
const lead = kept
|
|
12590
|
+
const lead = kept && timing ? scanNarrationLead(kept, timing) : [];
|
|
12181
12591
|
const ours = [...determinism, ...narration, ...budget2, ...type, ...lead];
|
|
12182
12592
|
const stops = await declaredStops(dir);
|
|
12183
12593
|
const [verdict, frames2] = await Promise.all([
|
|
@@ -12366,6 +12776,31 @@ function scanUnusedFigures(storyboard, source) {
|
|
|
12366
12776
|
}
|
|
12367
12777
|
];
|
|
12368
12778
|
}
|
|
12779
|
+
function scanMath(storyboard, source) {
|
|
12780
|
+
const out = [];
|
|
12781
|
+
const say = (rule, message2) => {
|
|
12782
|
+
out.push({ severity: "warning", gate: "storyboard", rule, message: message2 });
|
|
12783
|
+
};
|
|
12784
|
+
for (const beat of storyboard.beats) {
|
|
12785
|
+
try {
|
|
12786
|
+
let notes = [];
|
|
12787
|
+
if (beat.archetype === "equation-walk") {
|
|
12788
|
+
const params = beat.params;
|
|
12789
|
+
const eq = source.equations.find((e) => e.id === params.equationId);
|
|
12790
|
+
if (eq) notes = prepareWalk(eq, params.terms, beat.id).notes;
|
|
12791
|
+
} else if (beat.archetype === "equation-morph") {
|
|
12792
|
+
notes = prepareMorph(beat, source).notes;
|
|
12793
|
+
}
|
|
12794
|
+
for (const n3 of notes) say("tex_repaired", `${beat.id}: ${n3}`);
|
|
12795
|
+
} catch (err) {
|
|
12796
|
+
say(
|
|
12797
|
+
"tex_unrenderable",
|
|
12798
|
+
`${beat.id}: ${err instanceof Error ? err.message : String(err)} \u2014 build will leave this beat out`
|
|
12799
|
+
);
|
|
12800
|
+
}
|
|
12801
|
+
}
|
|
12802
|
+
return out;
|
|
12803
|
+
}
|
|
12369
12804
|
function figuresShown2(params) {
|
|
12370
12805
|
const sides = ["left", "right"].map(
|
|
12371
12806
|
(side) => params[side]?.figureId
|
|
@@ -12447,14 +12882,14 @@ function scanNarrationLead(beats, timing) {
|
|
|
12447
12882
|
return findings;
|
|
12448
12883
|
}
|
|
12449
12884
|
function firstMention(segments2, label) {
|
|
12450
|
-
for (const
|
|
12451
|
-
for (const cue of
|
|
12885
|
+
for (const segment2 of segments2) {
|
|
12886
|
+
for (const cue of segment2.cues) {
|
|
12452
12887
|
const words2 = cue.text.split(/\s+/);
|
|
12453
12888
|
let at = 0;
|
|
12454
12889
|
for (const word of words2) {
|
|
12455
12890
|
if (mentions(word, label)) {
|
|
12456
12891
|
const share2 = cue.text.length > 0 ? at / cue.text.length : 0;
|
|
12457
|
-
return
|
|
12892
|
+
return segment2.start + cue.start + share2 * (cue.end - cue.start);
|
|
12458
12893
|
}
|
|
12459
12894
|
at += word.length + 1;
|
|
12460
12895
|
}
|
|
@@ -12589,7 +13024,7 @@ ${bands}
|
|
|
12589
13024
|
</div>
|
|
12590
13025
|
`;
|
|
12591
13026
|
}
|
|
12592
|
-
function measure3(
|
|
13027
|
+
function measure3(count2) {
|
|
12593
13028
|
const doc = document;
|
|
12594
13029
|
const probe2 = doc.getElementById("c0");
|
|
12595
13030
|
if (probe2) {
|
|
@@ -12602,7 +13037,7 @@ function measure3(count) {
|
|
|
12602
13037
|
probe2.classList.remove("on");
|
|
12603
13038
|
}
|
|
12604
13039
|
const rects = [];
|
|
12605
|
-
for (let i = 0; i <
|
|
13040
|
+
for (let i = 0; i < count2; i++) {
|
|
12606
13041
|
const el = doc.getElementById(`c${i}`);
|
|
12607
13042
|
if (!el) continue;
|
|
12608
13043
|
el.classList.add("on");
|
|
@@ -12630,11 +13065,12 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
12630
13065
|
await mkdir8(work, { recursive: true });
|
|
12631
13066
|
await writeFile10(page, captionPage(cues, style, href));
|
|
12632
13067
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
12633
|
-
const
|
|
12634
|
-
|
|
12635
|
-
headless: true,
|
|
12636
|
-
|
|
12637
|
-
|
|
13068
|
+
const executablePath = await chromePath(CAPTION_NEED);
|
|
13069
|
+
const browser = await launchOwnPage(
|
|
13070
|
+
(args) => puppeteer.launch({ executablePath, headless: true, args }),
|
|
13071
|
+
["--force-device-scale-factor=1", "--hide-scrollbars"],
|
|
13072
|
+
"the caption band"
|
|
13073
|
+
);
|
|
12638
13074
|
try {
|
|
12639
13075
|
const tab = await browser.newPage();
|
|
12640
13076
|
await tab.setViewport({ width: style.width, height: style.height, deviceScaleFactor: 1 });
|
|
@@ -13294,6 +13730,7 @@ export {
|
|
|
13294
13730
|
sampleTimes,
|
|
13295
13731
|
scanBeatCount,
|
|
13296
13732
|
scanHeadlines,
|
|
13733
|
+
scanMath,
|
|
13297
13734
|
scanNarrationDrift,
|
|
13298
13735
|
scanNarrationLead,
|
|
13299
13736
|
scanPaperArc,
|