@jokerized/decksmith 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/dist/cli.js +599 -179
- package/dist/index.js +593 -176
- package/dist/mcp.js +534 -166
- package/dist/types/emit/archetypes/equation-morph.d.ts +40 -0
- package/dist/types/emit/archetypes/equation-walk.d.ts +60 -1
- package/dist/types/emit/tex.d.ts +39 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/plan/prompt.d.ts +0 -12
- package/dist/types/render/capture.d.ts +24 -0
- package/dist/types/render/timing.d.ts +16 -0
- package/dist/types/verify/budget.d.ts +8 -1
- package/dist/types/verify/fidelity.d.ts +19 -0
- package/dist/types/verify/index.d.ts +17 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -864,35 +864,35 @@ function elbow(sid, from, to, o) {
|
|
|
864
864
|
}
|
|
865
865
|
return path(d, { ...strokeAttrs(o), "marker-end": `url(#${headId(sid, o.stroke)})` });
|
|
866
866
|
}
|
|
867
|
-
function tracks(width,
|
|
868
|
-
const w =
|
|
869
|
-
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 }));
|
|
870
870
|
}
|
|
871
871
|
function fitBoxes(req) {
|
|
872
|
-
const
|
|
872
|
+
const count2 = req.labels.length;
|
|
873
873
|
const minGap = req.minGap ?? 24;
|
|
874
874
|
const padEm = req.padEm ?? 0.8;
|
|
875
875
|
const weight = req.weight ?? 600;
|
|
876
876
|
const x0 = req.x0 ?? 0;
|
|
877
877
|
const unit = Math.max(1, ...req.labels.map((l) => textWidth(l, 1, weight) + 2 * padEm));
|
|
878
|
-
const sizeAt = (gap) =>
|
|
878
|
+
const sizeAt = (gap) => count2 < 1 ? req.size : (req.width - gap * (count2 - 1)) / count2 / unit;
|
|
879
879
|
if (sizeAt(req.gap) >= req.size) {
|
|
880
|
-
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) };
|
|
881
881
|
}
|
|
882
|
-
if (
|
|
883
|
-
const gap = (req.width -
|
|
884
|
-
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) };
|
|
885
885
|
}
|
|
886
886
|
const size2 = sizeAt(minGap);
|
|
887
887
|
if (size2 >= MIN_FONT) {
|
|
888
|
-
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) };
|
|
889
889
|
}
|
|
890
890
|
return {
|
|
891
891
|
ok: false,
|
|
892
892
|
size: MIN_FONT,
|
|
893
893
|
gap: minGap,
|
|
894
|
-
boxes: tracks(req.width,
|
|
895
|
-
needed:
|
|
894
|
+
boxes: tracks(req.width, count2, minGap, x0),
|
|
895
|
+
needed: count2 * MIN_FONT * unit + minGap * (count2 - 1)
|
|
896
896
|
};
|
|
897
897
|
}
|
|
898
898
|
|
|
@@ -1225,8 +1225,8 @@ var MID_GAP = 56;
|
|
|
1225
1225
|
var BAND_GAP = 34;
|
|
1226
1226
|
var COL_GAP = 28;
|
|
1227
1227
|
var MIN_COL = 250;
|
|
1228
|
-
function lineBudget(
|
|
1229
|
-
const perSide = Math.max(1, Math.ceil(
|
|
1228
|
+
function lineBudget(count2, stageH) {
|
|
1229
|
+
const perSide = Math.max(1, Math.ceil(count2 / 2));
|
|
1230
1230
|
const room = (stageH - (perSide - 1) * LAB_GAP) / perSide;
|
|
1231
1231
|
const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
|
|
1232
1232
|
return Math.min(LINE_CEIL, Math.max(1, lines));
|
|
@@ -1637,7 +1637,7 @@ var HEAD_GAP = 14;
|
|
|
1637
1637
|
var barCompare = (beat, ctx) => {
|
|
1638
1638
|
const { sid, theme } = ctx;
|
|
1639
1639
|
const p = beat.params;
|
|
1640
|
-
const
|
|
1640
|
+
const count2 = p.bars.length;
|
|
1641
1641
|
const W = contentW(ctx.format);
|
|
1642
1642
|
const face = faceOf(ctx.theme.fontStack);
|
|
1643
1643
|
const tall = isPortrait(ctx.format);
|
|
@@ -1651,7 +1651,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1651
1651
|
0,
|
|
1652
1652
|
face
|
|
1653
1653
|
);
|
|
1654
|
-
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (
|
|
1654
|
+
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count2 + GAP_RATIO * (count2 - 1)));
|
|
1655
1655
|
let pitch = bar * (1 + GAP_RATIO);
|
|
1656
1656
|
const gutterMax = tall ? 0 : share(GUTTER_MAX, W);
|
|
1657
1657
|
const gutterInner = tall ? W : gutterMax - GUTTER_PAD;
|
|
@@ -1673,7 +1673,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1673
1673
|
if (tall) {
|
|
1674
1674
|
bar = Math.min(
|
|
1675
1675
|
BAR_MAX,
|
|
1676
|
-
(avail - unitBand - FOOT -
|
|
1676
|
+
(avail - unitBand - FOOT - count2 * head) / (count2 + GAP_RATIO * (count2 - 1))
|
|
1677
1677
|
);
|
|
1678
1678
|
pitch = head + bar * (1 + GAP_RATIO);
|
|
1679
1679
|
} else {
|
|
@@ -1684,7 +1684,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1684
1684
|
}
|
|
1685
1685
|
}
|
|
1686
1686
|
const valueSize = Math.max(MIN_FONT, Math.min(VALUE_MAX, bar * 0.9));
|
|
1687
|
-
const barsH = (
|
|
1687
|
+
const barsH = (count2 - 1) * pitch + head + bar;
|
|
1688
1688
|
const foot = Math.max(
|
|
1689
1689
|
0,
|
|
1690
1690
|
Math.ceil(
|
|
@@ -1697,7 +1697,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1697
1697
|
const H = barsH + unitBand + foot;
|
|
1698
1698
|
if (H > avail) {
|
|
1699
1699
|
throw new Error(
|
|
1700
|
-
`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.`
|
|
1701
1701
|
);
|
|
1702
1702
|
}
|
|
1703
1703
|
const gutter = tall ? 0 : Math.min(
|
|
@@ -1850,7 +1850,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1850
1850
|
</div>${note2}`;
|
|
1851
1851
|
const railsAt = 0.6;
|
|
1852
1852
|
const barsAt = 0.95;
|
|
1853
|
-
const step = Math.min(0.4, 2.4 /
|
|
1853
|
+
const step = Math.min(0.4, 2.4 / count2);
|
|
1854
1854
|
const grow = 0.85;
|
|
1855
1855
|
const tl = [
|
|
1856
1856
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
@@ -1900,11 +1900,11 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1900
1900
|
);
|
|
1901
1901
|
}
|
|
1902
1902
|
}
|
|
1903
|
-
const settled = barsAt + (
|
|
1903
|
+
const settled = barsAt + (count2 - 1) * step + grow + 0.05;
|
|
1904
1904
|
const holds = [settled + 0.2];
|
|
1905
1905
|
const focal = rows.reduce((best, r) => Math.abs(r.value) > Math.abs(best.value) ? r : best);
|
|
1906
1906
|
const spot = spotlighter(sid, ".bc-bar");
|
|
1907
|
-
if (
|
|
1907
|
+
if (count2 > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
|
|
1908
1908
|
const tailAt = settled + 0.3;
|
|
1909
1909
|
if (p.unit) {
|
|
1910
1910
|
tl.push(tween(`#${id(sid, "unit")}`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, tailAt));
|
|
@@ -2397,6 +2397,90 @@ var dataTable = (beat, ctx) => {
|
|
|
2397
2397
|
};
|
|
2398
2398
|
};
|
|
2399
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
|
+
|
|
2400
2484
|
// src/emit/archetypes/equation-walk.ts
|
|
2401
2485
|
var TRUST = 'function (c) { return c.command === "\\\\htmlClass"; }';
|
|
2402
2486
|
var OPTS = `{ displayMode: true, trust: ${TRUST}, strict: false, output: "html" }`;
|
|
@@ -2411,6 +2495,9 @@ var SYNONYMS = [
|
|
|
2411
2495
|
function normalise(tex) {
|
|
2412
2496
|
let out = "";
|
|
2413
2497
|
const map = [];
|
|
2498
|
+
const ends = [];
|
|
2499
|
+
const lead = [];
|
|
2500
|
+
let hint = null;
|
|
2414
2501
|
for (let i = 0; i < tex.length; ) {
|
|
2415
2502
|
if (/\s/.test(tex[i])) {
|
|
2416
2503
|
i++;
|
|
@@ -2422,19 +2509,19 @@ function normalise(tex) {
|
|
|
2422
2509
|
const m = new RegExp(`^(?:${re.source})`).exec(rest);
|
|
2423
2510
|
return m ? { len: m[0].length, to } : null;
|
|
2424
2511
|
}).find(Boolean);
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
i
|
|
2431
|
-
|
|
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;
|
|
2432
2521
|
}
|
|
2433
|
-
|
|
2434
|
-
map.push(i);
|
|
2435
|
-
i++;
|
|
2522
|
+
i += len;
|
|
2436
2523
|
}
|
|
2437
|
-
return { text: out, map };
|
|
2524
|
+
return { text: out, map, ends, lead };
|
|
2438
2525
|
}
|
|
2439
2526
|
function locate(tex, term) {
|
|
2440
2527
|
const hay = normalise(tex);
|
|
@@ -2442,58 +2529,131 @@ function locate(tex, term) {
|
|
|
2442
2529
|
if (needle === "") return null;
|
|
2443
2530
|
const at = hay.text.indexOf(needle);
|
|
2444
2531
|
if (at < 0) return null;
|
|
2445
|
-
const start = hay.
|
|
2446
|
-
const
|
|
2447
|
-
const lastOrig = hay.map[lastNorm];
|
|
2448
|
-
let end = lastOrig + 1;
|
|
2449
|
-
while (end < tex.length && (hay.map[lastNorm + 1] ?? tex.length) > end && /\s/.test(tex[end])) {
|
|
2450
|
-
end++;
|
|
2451
|
-
}
|
|
2452
|
-
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];
|
|
2453
2534
|
return { start, end: Math.min(end, tex.length) };
|
|
2454
2535
|
}
|
|
2455
2536
|
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
2537
|
+
const checked = texError(tex, true) === null;
|
|
2456
2538
|
let parts = [{ text: tex, raw: true }];
|
|
2457
2539
|
const used = [];
|
|
2458
2540
|
const missing = [];
|
|
2541
|
+
const broken = [];
|
|
2542
|
+
const joined = (ps) => ps.map((p) => p.text).join("");
|
|
2459
2543
|
for (const term of terms) {
|
|
2460
2544
|
let placed = false;
|
|
2545
|
+
let found = false;
|
|
2461
2546
|
for (let i = 0; i < parts.length && !placed; i++) {
|
|
2462
2547
|
const part = parts[i];
|
|
2463
2548
|
if (!part.raw) continue;
|
|
2464
2549
|
const at = locate(part.text, term.tex);
|
|
2465
2550
|
if (!at) continue;
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
raw:
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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);
|
|
2478
2571
|
}
|
|
2479
|
-
if (!placed) missing.push(term);
|
|
2480
2572
|
}
|
|
2481
2573
|
if (used.length === 0) {
|
|
2482
2574
|
throw new Error(
|
|
2483
|
-
`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)}`
|
|
2484
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
|
+
};
|
|
2485
2607
|
}
|
|
2486
|
-
|
|
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)}';`;
|
|
2487
2647
|
}
|
|
2488
2648
|
function equationSize(tex) {
|
|
2489
2649
|
return tex.length > 120 ? 68 : tex.length > 90 ? 80 : tex.length > 55 ? 92 : 108;
|
|
2490
2650
|
}
|
|
2491
|
-
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;
|
|
2492
2652
|
var GLYPH_EM = 0.9;
|
|
2493
2653
|
function texUnits(tex) {
|
|
2494
2654
|
let ems = 0;
|
|
2495
2655
|
for (const m of tex.matchAll(/\\qquad|\\quad/g)) ems += m[0] === "\\qquad" ? 2 : 1;
|
|
2496
|
-
const glyphs = tex.replace(/\\q?quad/g, "").replace(
|
|
2656
|
+
const glyphs = tex.replace(/\\q?quad/g, "").replace(NO_INK2, "").replace(/[{}\s_^]/g, "");
|
|
2497
2657
|
return ems + glyphs.length * GLYPH_EM;
|
|
2498
2658
|
}
|
|
2499
2659
|
function statements(tex, stacked) {
|
|
@@ -2501,6 +2661,100 @@ function statements(tex, stacked) {
|
|
|
2501
2661
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
2502
2662
|
return parts.length > 0 ? parts : [tex];
|
|
2503
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
|
+
}
|
|
2504
2758
|
function legendRows(sid, terms, theme) {
|
|
2505
2759
|
return terms.map(
|
|
2506
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>`
|
|
@@ -2516,7 +2770,10 @@ function legendCss(theme) {
|
|
|
2516
2770
|
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
2517
2771
|
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
2518
2772
|
// are one symbol each and their natural widths differ by a few pixels.
|
|
2519
|
-
|
|
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}`
|
|
2520
2777
|
].join("\n");
|
|
2521
2778
|
}
|
|
2522
2779
|
var equationWalk = (beat, ctx) => {
|
|
@@ -2528,37 +2785,53 @@ var equationWalk = (beat, ctx) => {
|
|
|
2528
2785
|
`equation-walk ${beat.id}: no equation "${p.equationId}" in source ${ctx.source.id}`
|
|
2529
2786
|
);
|
|
2530
2787
|
}
|
|
2531
|
-
const walk =
|
|
2788
|
+
const walk = prepareWalk(eq, p.terms, beat.id);
|
|
2532
2789
|
const terms = walk.used;
|
|
2533
2790
|
const legend = legendRows(sid, terms, theme);
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
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
|
+
}
|
|
2546
2828
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
2547
2829
|
<div class="eqslide">
|
|
2548
|
-
|
|
2830
|
+
${eqHtml}
|
|
2549
2831
|
<div class="legend">
|
|
2550
2832
|
${legend}
|
|
2551
2833
|
</div>
|
|
2552
2834
|
</div>`;
|
|
2553
|
-
const setup = [
|
|
2554
|
-
`var OPTS = ${OPTS};`,
|
|
2555
|
-
...shown.map(
|
|
2556
|
-
(part, i) => `katex.render('${js(part)}', document.getElementById("${sid}-eq${shown.length === 1 ? "" : i}"), OPTS);`
|
|
2557
|
-
),
|
|
2558
|
-
...terms.map(
|
|
2559
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2560
|
-
)
|
|
2561
|
-
];
|
|
2562
2835
|
const tl = [
|
|
2563
2836
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
2564
2837
|
tween(`#${sid}-eq`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8)
|
|
@@ -2570,6 +2843,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2570
2843
|
);
|
|
2571
2844
|
const holds = [];
|
|
2572
2845
|
const spot = spotlighter(sid, ".term");
|
|
2846
|
+
const swell = (t2) => raw(`dsSwell.${t2.tone}`);
|
|
2573
2847
|
terms.forEach((term, i) => {
|
|
2574
2848
|
const at = first + i * step;
|
|
2575
2849
|
const colour = theme.tones[term.tone];
|
|
@@ -2586,13 +2860,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2586
2860
|
tween(
|
|
2587
2861
|
`#${sid} .t-${term.tone}`,
|
|
2588
2862
|
{ color: theme.fg, scale: 1 },
|
|
2589
|
-
{ color: colour, scale:
|
|
2863
|
+
{ color: colour, scale: swell(term), duration: 0.5 },
|
|
2590
2864
|
at
|
|
2591
2865
|
)
|
|
2592
2866
|
);
|
|
2593
2867
|
const prev = terms[i - 1];
|
|
2594
2868
|
if (prev) {
|
|
2595
|
-
tl.push(
|
|
2869
|
+
tl.push(
|
|
2870
|
+
tween(`#${sid} .t-${prev.tone}`, { scale: swell(prev) }, { scale: 1, duration: 0.4 }, at)
|
|
2871
|
+
);
|
|
2596
2872
|
}
|
|
2597
2873
|
holds.push(at + 0.6);
|
|
2598
2874
|
});
|
|
@@ -2600,7 +2876,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2600
2876
|
if (last) {
|
|
2601
2877
|
const at = first + terms.length * step;
|
|
2602
2878
|
tl.push(
|
|
2603
|
-
tween(`#${sid} .t-${last.tone}`, { scale:
|
|
2879
|
+
tween(`#${sid} .t-${last.tone}`, { scale: swell(last) }, { scale: 1, duration: 0.4 }, at),
|
|
2604
2880
|
...spot.restore(at)
|
|
2605
2881
|
);
|
|
2606
2882
|
}
|
|
@@ -2608,6 +2884,8 @@ var equationWalk = (beat, ctx) => {
|
|
|
2608
2884
|
html,
|
|
2609
2885
|
tl,
|
|
2610
2886
|
setup,
|
|
2887
|
+
measure: measure4,
|
|
2888
|
+
...walk.notes.length ? { warnings: walk.notes } : {},
|
|
2611
2889
|
holds: holdsWithin(holds, beat.seconds),
|
|
2612
2890
|
css: [
|
|
2613
2891
|
chromeCss(theme),
|
|
@@ -2627,6 +2905,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2627
2905
|
// has its margin zeroed above, so without an explicit gap the two lines
|
|
2628
2906
|
// would touch and read as one equation broken mid-expression.
|
|
2629
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}",
|
|
2630
2917
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
2631
2918
|
".term{display:inline-block}",
|
|
2632
2919
|
legendCss(theme),
|
|
@@ -2641,26 +2928,67 @@ var equationWalk = (beat, ctx) => {
|
|
|
2641
2928
|
|
|
2642
2929
|
// src/emit/archetypes/equation-morph.ts
|
|
2643
2930
|
var MORPH_SECONDS = 1.6;
|
|
2644
|
-
|
|
2645
|
-
const { sid, theme } = ctx;
|
|
2931
|
+
function prepareMorph(beat, source) {
|
|
2646
2932
|
const p = beat.params;
|
|
2647
2933
|
const find3 = (id2) => {
|
|
2648
|
-
const eq =
|
|
2934
|
+
const eq = source.equations.find((e) => e.id === id2);
|
|
2649
2935
|
if (!eq)
|
|
2650
|
-
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}`);
|
|
2651
2937
|
return eq;
|
|
2652
2938
|
};
|
|
2653
|
-
const
|
|
2654
|
-
const
|
|
2655
|
-
|
|
2656
|
-
|
|
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) {
|
|
2657
2956
|
throw new Error(
|
|
2658
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)}`
|
|
2659
2958
|
);
|
|
2660
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
|
+
);
|
|
2661
2965
|
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
2662
|
-
|
|
2663
|
-
|
|
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);
|
|
2664
2992
|
const size2 = Math.max(
|
|
2665
2993
|
MIN_FONT,
|
|
2666
2994
|
Math.min(
|
|
@@ -2682,9 +3010,7 @@ var equationMorph = (beat, ctx) => {
|
|
|
2682
3010
|
`var OPTS = ${OPTS};`,
|
|
2683
3011
|
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
2684
3012
|
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
2685
|
-
...both.map(
|
|
2686
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2687
|
-
)
|
|
3013
|
+
...both.map((t2) => chipSetup(sid, t2))
|
|
2688
3014
|
];
|
|
2689
3015
|
const first = 1.8;
|
|
2690
3016
|
const at = Math.round(
|
|
@@ -2717,8 +3043,11 @@ var equationMorph = (beat, ctx) => {
|
|
|
2717
3043
|
setup,
|
|
2718
3044
|
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
2719
3045
|
// the ready gate, and the plugin tween above finds it on the host.
|
|
2720
|
-
|
|
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"));`],
|
|
2721
3049
|
plugins: ["dsMorph"],
|
|
3050
|
+
...notes.length ? { warnings: notes } : {},
|
|
2722
3051
|
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
2723
3052
|
css: [
|
|
2724
3053
|
chromeCss(theme),
|
|
@@ -3299,16 +3628,16 @@ var lineChart = (beat, ctx) => {
|
|
|
3299
3628
|
</svg>${readout}
|
|
3300
3629
|
</div>`;
|
|
3301
3630
|
const draw2 = 0.8;
|
|
3302
|
-
const
|
|
3303
|
-
const idealStep = Math.min(0.45, DRAW_SECONDS /
|
|
3631
|
+
const count2 = p.points.length;
|
|
3632
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count2);
|
|
3304
3633
|
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
3305
|
-
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length,
|
|
3634
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count2 - 1) : 0;
|
|
3306
3635
|
const tailAfter = (s) => Math.max(
|
|
3307
|
-
s *
|
|
3636
|
+
s * count2 + 0.4,
|
|
3308
3637
|
// the ring: its walk, then the 0.4s it takes to leave
|
|
3309
|
-
0.3 + s * (
|
|
3638
|
+
0.3 + s * (count2 - 1),
|
|
3310
3639
|
// the dots
|
|
3311
|
-
0.5 + s * (
|
|
3640
|
+
0.5 + s * (count2 - 1),
|
|
3312
3641
|
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
3313
3642
|
);
|
|
3314
3643
|
const room = beat.seconds - spine;
|
|
@@ -3371,8 +3700,8 @@ var lineChart = (beat, ctx) => {
|
|
|
3371
3700
|
)
|
|
3372
3701
|
);
|
|
3373
3702
|
}
|
|
3374
|
-
const walk = cmp ? sec(step *
|
|
3375
|
-
if (
|
|
3703
|
+
const walk = cmp ? sec(step * count2) : DRAW_SECONDS;
|
|
3704
|
+
if (count2 > 1) {
|
|
3376
3705
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
3377
3706
|
tl.push(
|
|
3378
3707
|
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
@@ -3387,7 +3716,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3387
3716
|
)
|
|
3388
3717
|
);
|
|
3389
3718
|
}
|
|
3390
|
-
const drawn = settled + (cmp ? tailAfter(step) : step *
|
|
3719
|
+
const drawn = settled + (cmp ? tailAfter(step) : step * count2 + 0.4);
|
|
3391
3720
|
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
3392
3721
|
if (p.readout) {
|
|
3393
3722
|
tl.push(
|
|
@@ -3403,7 +3732,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3403
3732
|
return {
|
|
3404
3733
|
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
3405
3734
|
warnings: [
|
|
3406
|
-
`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.`
|
|
3407
3736
|
]
|
|
3408
3737
|
};
|
|
3409
3738
|
}
|
|
@@ -3483,7 +3812,7 @@ var V_BOX_H = 268;
|
|
|
3483
3812
|
var V_LOOP_SIDE = 56;
|
|
3484
3813
|
var V_LOOP_PAD = 18;
|
|
3485
3814
|
function columnLayout(stageW, stageH, stages, face, loop) {
|
|
3486
|
-
const
|
|
3815
|
+
const count2 = stages.length;
|
|
3487
3816
|
const loopW = loop ? Math.max(240, Math.round(stageW * 0.32)) : 0;
|
|
3488
3817
|
const boxX = M;
|
|
3489
3818
|
const boxW = stageW - 2 * M - (loop ? V_LOOP_SIDE + V_LOOP_PAD + loopW : 0);
|
|
@@ -3500,11 +3829,11 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3500
3829
|
return Math.ceil(2 * PAD_Y2 + label + (note2 > 0 ? NOTE_TOP + note2 * NOTE * NOTE_LH : 0));
|
|
3501
3830
|
})
|
|
3502
3831
|
);
|
|
3503
|
-
const room = (stageH - 2 * M - (
|
|
3832
|
+
const room = (stageH - 2 * M - (count2 - 1) * V_GAP) / count2;
|
|
3504
3833
|
const boxH = Math.max(need, Math.min(room, V_BOX_H));
|
|
3505
3834
|
return {
|
|
3506
3835
|
size: size2,
|
|
3507
|
-
boxes: Array.from({ length:
|
|
3836
|
+
boxes: Array.from({ length: count2 }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
|
|
3508
3837
|
boxH,
|
|
3509
3838
|
boxX,
|
|
3510
3839
|
boxW,
|
|
@@ -3516,7 +3845,7 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3516
3845
|
loopLines: loop ? wrap(loop.label, NOTE, loopW, 500, 0, face) : [],
|
|
3517
3846
|
loopX: boxX + boxW + V_LOOP_SIDE,
|
|
3518
3847
|
loopW,
|
|
3519
|
-
svgH: 2 * M +
|
|
3848
|
+
svgH: 2 * M + count2 * boxH + (count2 - 1) * V_GAP
|
|
3520
3849
|
};
|
|
3521
3850
|
}
|
|
3522
3851
|
function balance(label, k, face) {
|
|
@@ -4234,8 +4563,8 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4234
4563
|
if (inline2.fits) return inline2;
|
|
4235
4564
|
return inline2.wide && inline2.blockH < stacked.blockH ? inline2 : stacked;
|
|
4236
4565
|
}
|
|
4237
|
-
function labelWeight(i,
|
|
4238
|
-
return i ===
|
|
4566
|
+
function labelWeight(i, count2) {
|
|
4567
|
+
return i === count2 - 1 ? 700 : 600;
|
|
4239
4568
|
}
|
|
4240
4569
|
function floorFor(p, format) {
|
|
4241
4570
|
if (!p.tilt) return MIN_FONT;
|
|
@@ -4245,7 +4574,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4245
4574
|
const width = contentW(format);
|
|
4246
4575
|
const boxH = contentH(format);
|
|
4247
4576
|
const floor = floorFor(p, format);
|
|
4248
|
-
const
|
|
4577
|
+
const count2 = p.layers.length;
|
|
4249
4578
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4250
4579
|
const riseMax = RISE_MAX[k];
|
|
4251
4580
|
const syMax = SY_MAX[k];
|
|
@@ -4253,7 +4582,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4253
4582
|
const noteW = (l) => inline2 && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4254
4583
|
const want = Math.max(
|
|
4255
4584
|
...p.layers.map(
|
|
4256
|
-
(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)
|
|
4257
4586
|
)
|
|
4258
4587
|
);
|
|
4259
4588
|
const colCap = inline2 ? width * 0.56 : width * 0.5;
|
|
@@ -4262,7 +4591,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4262
4591
|
const labelX = width - colW;
|
|
4263
4592
|
const labelRoom = Math.min(
|
|
4264
4593
|
...p.layers.map(
|
|
4265
|
-
(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))
|
|
4266
4595
|
)
|
|
4267
4596
|
);
|
|
4268
4597
|
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
@@ -4270,7 +4599,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4270
4599
|
const nw = noteW(l);
|
|
4271
4600
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
4272
4601
|
return {
|
|
4273
|
-
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i,
|
|
4602
|
+
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count2), 0, face),
|
|
4274
4603
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4275
4604
|
// short line" — and wrapping one would put its second line under the label.
|
|
4276
4605
|
note: l.note === void 0 ? [] : inline2 ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
@@ -4288,13 +4617,13 @@ function solve2(p, format, inline2, face) {
|
|
|
4288
4617
|
const noteH = noteHeight(p.note, noteWidth(format), 28, face);
|
|
4289
4618
|
const free = boxH - chromeH - 20 - noteH;
|
|
4290
4619
|
const avail = Math.max(300, free);
|
|
4291
|
-
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1,
|
|
4620
|
+
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count2 - 1);
|
|
4292
4621
|
const rise = clamp(room, RISE_MIN, riseMax);
|
|
4293
4622
|
const sy = clamp(rise * 0.62, 46, syMax);
|
|
4294
4623
|
const t2 = clamp(rise * 0.14, 10, tMax);
|
|
4295
4624
|
const span = width - NUM_W - GAP3 - colW;
|
|
4296
4625
|
const sx = clamp(span * 0.2, 110, 220);
|
|
4297
|
-
const height = 2 * pad + (
|
|
4626
|
+
const height = 2 * pad + (count2 - 1) * rise + sy + t2;
|
|
4298
4627
|
return {
|
|
4299
4628
|
// A rise shorter than a label block prints the note through the label below.
|
|
4300
4629
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
@@ -4319,7 +4648,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4319
4648
|
labelSize,
|
|
4320
4649
|
blockH,
|
|
4321
4650
|
pad,
|
|
4322
|
-
yBase: pad + (
|
|
4651
|
+
yBase: pad + (count2 - 1) * rise + sy,
|
|
4323
4652
|
lines
|
|
4324
4653
|
};
|
|
4325
4654
|
}
|
|
@@ -4336,11 +4665,11 @@ var stack = (beat, ctx) => {
|
|
|
4336
4665
|
const p = beat.params;
|
|
4337
4666
|
const face = faceOf(ctx.theme.fontStack);
|
|
4338
4667
|
const L = stackLayout(p, ctx.format, face);
|
|
4339
|
-
const
|
|
4340
|
-
const last =
|
|
4668
|
+
const count2 = p.layers.length;
|
|
4669
|
+
const last = count2 - 1;
|
|
4341
4670
|
if (!L.fits) {
|
|
4342
4671
|
throw new Error(
|
|
4343
|
-
`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.`
|
|
4344
4673
|
);
|
|
4345
4674
|
}
|
|
4346
4675
|
const parts = {};
|
|
@@ -4368,7 +4697,7 @@ var stack = (beat, ctx) => {
|
|
|
4368
4697
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
4369
4698
|
{
|
|
4370
4699
|
size: L.labelSize,
|
|
4371
|
-
weight: labelWeight(i,
|
|
4700
|
+
weight: labelWeight(i, count2),
|
|
4372
4701
|
fill: top ? theme.tones.b : theme.fg,
|
|
4373
4702
|
maxWidth: block.labelMaxW,
|
|
4374
4703
|
lineHeight: 1.16,
|
|
@@ -4422,10 +4751,10 @@ var stack = (beat, ctx) => {
|
|
|
4422
4751
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4423
4752
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4424
4753
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
|
|
4425
|
-
const centre2 = (
|
|
4754
|
+
const centre2 = (count2 - 1) / 2;
|
|
4426
4755
|
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4427
4756
|
const first = 0.9;
|
|
4428
|
-
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));
|
|
4429
4758
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
4430
4759
|
const holds = [];
|
|
4431
4760
|
const spot = spotlighter(sid);
|
|
@@ -4457,11 +4786,11 @@ var stack = (beat, ctx) => {
|
|
|
4457
4786
|
);
|
|
4458
4787
|
}
|
|
4459
4788
|
if (p.note) {
|
|
4460
|
-
const at = first +
|
|
4789
|
+
const at = first + count2 * step;
|
|
4461
4790
|
tl.push(tween(`#${sid}-note`, { opacity: 0 }, { opacity: 1, duration: 0.6 }, at));
|
|
4462
4791
|
holds.push(at + 0.7);
|
|
4463
4792
|
}
|
|
4464
|
-
if (
|
|
4793
|
+
if (count2 > 1) tl.push(...spot.restore(first + count2 * step));
|
|
4465
4794
|
return {
|
|
4466
4795
|
html,
|
|
4467
4796
|
parts,
|
|
@@ -5802,8 +6131,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5802
6131
|
const text2 = beat.narration?.trim();
|
|
5803
6132
|
if (!text2) continue;
|
|
5804
6133
|
const segments2 = [];
|
|
5805
|
-
const
|
|
5806
|
-
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));
|
|
5807
6136
|
for (const [stop, line2] of plan.entries()) {
|
|
5808
6137
|
if (!line2) continue;
|
|
5809
6138
|
const speech = await synthesize(line2, {
|
|
@@ -5825,7 +6154,7 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5825
6154
|
}
|
|
5826
6155
|
if (segments2.length > 0) {
|
|
5827
6156
|
beats[beat.id] = segments2;
|
|
5828
|
-
staged[beat.id] =
|
|
6157
|
+
staged[beat.id] = count2;
|
|
5829
6158
|
}
|
|
5830
6159
|
}
|
|
5831
6160
|
return {
|
|
@@ -6715,11 +7044,11 @@ function stageScene(scene, speed) {
|
|
|
6715
7044
|
function speechPlan(open, holds, segments2) {
|
|
6716
7045
|
const starts = [];
|
|
6717
7046
|
let at = open;
|
|
6718
|
-
for (const [i,
|
|
6719
|
-
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;
|
|
6720
7049
|
if (i > 0) at = Math.max(at, hold);
|
|
6721
7050
|
starts.push(at);
|
|
6722
|
-
at +=
|
|
7051
|
+
at += segment2.seconds;
|
|
6723
7052
|
}
|
|
6724
7053
|
return { starts, end: at };
|
|
6725
7054
|
}
|
|
@@ -6947,10 +7276,10 @@ function pack(words2, width) {
|
|
|
6947
7276
|
function splitCue(cue, max = CUE_MAX_CHARS) {
|
|
6948
7277
|
if (cue.text.length <= max) return [cue];
|
|
6949
7278
|
const words2 = cue.text.split(/\s+/).filter(Boolean);
|
|
6950
|
-
const
|
|
7279
|
+
const count2 = pack(words2, max).length;
|
|
6951
7280
|
let width = max;
|
|
6952
|
-
for (let w = Math.ceil(cue.text.length /
|
|
6953
|
-
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) {
|
|
6954
7283
|
width = w;
|
|
6955
7284
|
break;
|
|
6956
7285
|
}
|
|
@@ -7042,17 +7371,17 @@ function place(scenes, spoken) {
|
|
|
7042
7371
|
);
|
|
7043
7372
|
}
|
|
7044
7373
|
const { starts } = speechPlan(scene.open, scene.holds, segments2);
|
|
7045
|
-
for (const [i,
|
|
7046
|
-
const index = Math.min(
|
|
7374
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7375
|
+
const index = Math.min(segment2.stop, scene.holds.length - 1);
|
|
7047
7376
|
out.push({
|
|
7048
|
-
id: `${scene.id}.${
|
|
7377
|
+
id: `${scene.id}.${segment2.stop}`,
|
|
7049
7378
|
scene: scene.id,
|
|
7050
|
-
stop:
|
|
7051
|
-
audio:
|
|
7379
|
+
stop: segment2.stop,
|
|
7380
|
+
audio: segment2.audio,
|
|
7052
7381
|
hold: round5(scene.start + scene.holds[index]),
|
|
7053
7382
|
start: round5(scene.start + starts[i]),
|
|
7054
|
-
duration:
|
|
7055
|
-
cues:
|
|
7383
|
+
duration: segment2.seconds,
|
|
7384
|
+
cues: segment2.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
|
|
7056
7385
|
});
|
|
7057
7386
|
}
|
|
7058
7387
|
}
|
|
@@ -7129,6 +7458,15 @@ function planTiming(input) {
|
|
|
7129
7458
|
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
7130
7459
|
// disagree with the box the archetypes actually drew into.
|
|
7131
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
|
+
},
|
|
7132
7470
|
scenes,
|
|
7133
7471
|
segments: segments2
|
|
7134
7472
|
};
|
|
@@ -7155,15 +7493,15 @@ function framePlan(timing, fps) {
|
|
|
7155
7493
|
);
|
|
7156
7494
|
}
|
|
7157
7495
|
pieces.push({ from: first, motion: last - first, freeze: 0 });
|
|
7158
|
-
for (const
|
|
7159
|
-
const at = f(
|
|
7496
|
+
for (const segment2 of segments2) {
|
|
7497
|
+
const at = f(segment2.start);
|
|
7160
7498
|
audio.push({
|
|
7161
|
-
id:
|
|
7162
|
-
audio:
|
|
7499
|
+
id: segment2.id,
|
|
7500
|
+
audio: segment2.audio,
|
|
7163
7501
|
startFrame: at,
|
|
7164
7502
|
delayMs: Math.round(at / fps * 1e3)
|
|
7165
7503
|
});
|
|
7166
|
-
for (const cue of
|
|
7504
|
+
for (const cue of segment2.cues) {
|
|
7167
7505
|
const start = at / fps + cue.start;
|
|
7168
7506
|
const end = at / fps + cue.end;
|
|
7169
7507
|
if (end > start) cues.push({ start, end, text: cue.text });
|
|
@@ -7737,6 +8075,22 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
7737
8075
|
async function chromePath(need = "open the deck with") {
|
|
7738
8076
|
return (await resolveChrome(need)).path;
|
|
7739
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
|
+
}
|
|
7740
8094
|
function runtimePath() {
|
|
7741
8095
|
return createRequire3(import.meta.url).resolve("hyperframes/dist/hyperframe.runtime.iife.js");
|
|
7742
8096
|
}
|
|
@@ -7754,10 +8108,9 @@ async function openDeck(dir, opts = {}) {
|
|
|
7754
8108
|
const timeout = opts.timeoutMs ?? 6e4;
|
|
7755
8109
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
7756
8110
|
const chrome2 = await resolveChrome();
|
|
7757
|
-
const browser = await
|
|
7758
|
-
executablePath: chrome2.path,
|
|
7759
|
-
|
|
7760
|
-
args: [
|
|
8111
|
+
const browser = await launchOwnPage(
|
|
8112
|
+
(args) => puppeteer.launch({ executablePath: chrome2.path, headless: true, args }),
|
|
8113
|
+
[
|
|
7761
8114
|
// A retina host would otherwise hand back a 2x frame, whose clip is not the
|
|
7762
8115
|
// renderer's.
|
|
7763
8116
|
"--force-device-scale-factor=1",
|
|
@@ -7770,8 +8123,9 @@ async function openDeck(dir, opts = {}) {
|
|
|
7770
8123
|
"--use-gl=angle",
|
|
7771
8124
|
"--use-angle=swiftshader",
|
|
7772
8125
|
"--enable-unsafe-swiftshader"
|
|
7773
|
-
]
|
|
7774
|
-
|
|
8126
|
+
],
|
|
8127
|
+
"the gates' page"
|
|
8128
|
+
);
|
|
7775
8129
|
try {
|
|
7776
8130
|
const page = await browser.newPage();
|
|
7777
8131
|
await page.evaluateOnNewDocument("self.__name = self.__name || ((fn) => fn);");
|
|
@@ -8792,6 +9146,14 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
8792
9146
|
// happens to be that run.
|
|
8793
9147
|
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
8794
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
|
+
);
|
|
8795
9157
|
});
|
|
8796
9158
|
try {
|
|
8797
9159
|
const page = await browser.newPage();
|
|
@@ -10250,8 +10612,13 @@ function renderSource(source) {
|
|
|
10250
10612
|
}
|
|
10251
10613
|
}
|
|
10252
10614
|
out.push("", "== EQUATIONS ==");
|
|
10253
|
-
for (const e of source.equations)
|
|
10254
|
-
|
|
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
|
+
}
|
|
10255
10622
|
if (!source.equations.length)
|
|
10256
10623
|
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
10257
10624
|
out.push("", "== TABLES ==");
|
|
@@ -10825,10 +11192,10 @@ function drawSvg(req) {
|
|
|
10825
11192
|
const seed = createHash7("sha256").update([req.prompt, req.style, req.aspect].join("|")).digest();
|
|
10826
11193
|
const rand = mulberry32(seed.readUInt32BE(0));
|
|
10827
11194
|
const accent = ACCENTS[Math.floor(rand() * ACCENTS.length)];
|
|
10828
|
-
const
|
|
11195
|
+
const count2 = 6 + Math.floor(rand() * 5);
|
|
10829
11196
|
const unit = Math.min(W, H);
|
|
10830
11197
|
const parts = [`<rect width="${W}" height="${H}" fill="${PAPER}"/>`];
|
|
10831
|
-
for (let i = 0; i <
|
|
11198
|
+
for (let i = 0; i < count2; i++) {
|
|
10832
11199
|
const kind = Math.floor(rand() * 3);
|
|
10833
11200
|
const cx = Math.round(W * (0.15 + 0.7 * rand()));
|
|
10834
11201
|
const cy = Math.round(H * (0.15 + 0.7 * rand()));
|
|
@@ -11051,10 +11418,10 @@ function readCanvas(html) {
|
|
|
11051
11418
|
function profilesFor(width, height, formats = FORMATS) {
|
|
11052
11419
|
return Object.values(formats).filter((f) => f.width === width && f.height === height);
|
|
11053
11420
|
}
|
|
11054
|
-
function scanBudget(html, storyboard, kept, formats = FORMATS) {
|
|
11421
|
+
function scanBudget(html, storyboard, kept, formats = FORMATS, recorded) {
|
|
11055
11422
|
const canvas = readCanvas(html);
|
|
11056
11423
|
if (!canvas) return [];
|
|
11057
|
-
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);
|
|
11058
11425
|
if (profiles.length === 0) {
|
|
11059
11426
|
return [
|
|
11060
11427
|
{
|
|
@@ -11782,6 +12149,20 @@ function captionBottom(sid, selector, fallbackPx) {
|
|
|
11782
12149
|
}
|
|
11783
12150
|
return bottom > 0 ? bottom : fallbackPx;
|
|
11784
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
|
+
}
|
|
11785
12166
|
async function fidelity(dir, opts = {}) {
|
|
11786
12167
|
const started = Date.now();
|
|
11787
12168
|
const floor = opts.floor ?? INK_FLOOR;
|
|
@@ -11805,6 +12186,10 @@ async function fidelity(dir, opts = {}) {
|
|
|
11805
12186
|
try {
|
|
11806
12187
|
deck = await openDeck(dir, { timeoutMs: opts.timeoutMs });
|
|
11807
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);
|
|
11808
12193
|
const measured = [];
|
|
11809
12194
|
const collided = [];
|
|
11810
12195
|
const apparent = [];
|
|
@@ -11848,7 +12233,8 @@ async function fidelity(dir, opts = {}) {
|
|
|
11848
12233
|
...gradeFidelity(measured, floor),
|
|
11849
12234
|
...gradeReserve(measured),
|
|
11850
12235
|
...gradeOverprint(collided),
|
|
11851
|
-
...gradeApparent(apparent)
|
|
12236
|
+
...gradeApparent(apparent),
|
|
12237
|
+
...gradeUnfit(unfit)
|
|
11852
12238
|
],
|
|
11853
12239
|
elapsedMs: Date.now() - started
|
|
11854
12240
|
};
|
|
@@ -12195,9 +12581,13 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
12195
12581
|
await readFile14(join13(dir, DECK_PAGE), "utf8").catch(() => ""),
|
|
12196
12582
|
await listFiles(dir)
|
|
12197
12583
|
);
|
|
12198
|
-
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
|
+
);
|
|
12199
12589
|
const type = html.flatMap(([file, text2]) => scanTypeFloor(text2, file));
|
|
12200
|
-
const lead = kept
|
|
12590
|
+
const lead = kept && timing ? scanNarrationLead(kept, timing) : [];
|
|
12201
12591
|
const ours = [...determinism, ...narration, ...budget2, ...type, ...lead];
|
|
12202
12592
|
const stops = await declaredStops(dir);
|
|
12203
12593
|
const [verdict, frames2] = await Promise.all([
|
|
@@ -12386,6 +12776,31 @@ function scanUnusedFigures(storyboard, source) {
|
|
|
12386
12776
|
}
|
|
12387
12777
|
];
|
|
12388
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
|
+
}
|
|
12389
12804
|
function figuresShown2(params) {
|
|
12390
12805
|
const sides = ["left", "right"].map(
|
|
12391
12806
|
(side) => params[side]?.figureId
|
|
@@ -12467,14 +12882,14 @@ function scanNarrationLead(beats, timing) {
|
|
|
12467
12882
|
return findings;
|
|
12468
12883
|
}
|
|
12469
12884
|
function firstMention(segments2, label) {
|
|
12470
|
-
for (const
|
|
12471
|
-
for (const cue of
|
|
12885
|
+
for (const segment2 of segments2) {
|
|
12886
|
+
for (const cue of segment2.cues) {
|
|
12472
12887
|
const words2 = cue.text.split(/\s+/);
|
|
12473
12888
|
let at = 0;
|
|
12474
12889
|
for (const word of words2) {
|
|
12475
12890
|
if (mentions(word, label)) {
|
|
12476
12891
|
const share2 = cue.text.length > 0 ? at / cue.text.length : 0;
|
|
12477
|
-
return
|
|
12892
|
+
return segment2.start + cue.start + share2 * (cue.end - cue.start);
|
|
12478
12893
|
}
|
|
12479
12894
|
at += word.length + 1;
|
|
12480
12895
|
}
|
|
@@ -12609,7 +13024,7 @@ ${bands}
|
|
|
12609
13024
|
</div>
|
|
12610
13025
|
`;
|
|
12611
13026
|
}
|
|
12612
|
-
function measure3(
|
|
13027
|
+
function measure3(count2) {
|
|
12613
13028
|
const doc = document;
|
|
12614
13029
|
const probe2 = doc.getElementById("c0");
|
|
12615
13030
|
if (probe2) {
|
|
@@ -12622,7 +13037,7 @@ function measure3(count) {
|
|
|
12622
13037
|
probe2.classList.remove("on");
|
|
12623
13038
|
}
|
|
12624
13039
|
const rects = [];
|
|
12625
|
-
for (let i = 0; i <
|
|
13040
|
+
for (let i = 0; i < count2; i++) {
|
|
12626
13041
|
const el = doc.getElementById(`c${i}`);
|
|
12627
13042
|
if (!el) continue;
|
|
12628
13043
|
el.classList.add("on");
|
|
@@ -12650,11 +13065,12 @@ async function renderCaptions(cues, style, deck, work) {
|
|
|
12650
13065
|
await mkdir8(work, { recursive: true });
|
|
12651
13066
|
await writeFile10(page, captionPage(cues, style, href));
|
|
12652
13067
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
12653
|
-
const
|
|
12654
|
-
|
|
12655
|
-
headless: true,
|
|
12656
|
-
|
|
12657
|
-
|
|
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
|
+
);
|
|
12658
13074
|
try {
|
|
12659
13075
|
const tab = await browser.newPage();
|
|
12660
13076
|
await tab.setViewport({ width: style.width, height: style.height, deviceScaleFactor: 1 });
|
|
@@ -13314,6 +13730,7 @@ export {
|
|
|
13314
13730
|
sampleTimes,
|
|
13315
13731
|
scanBeatCount,
|
|
13316
13732
|
scanHeadlines,
|
|
13733
|
+
scanMath,
|
|
13317
13734
|
scanNarrationDrift,
|
|
13318
13735
|
scanNarrationLead,
|
|
13319
13736
|
scanPaperArc,
|