@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/mcp.js
CHANGED
|
@@ -930,35 +930,35 @@ function elbow(sid, from, to, o) {
|
|
|
930
930
|
}
|
|
931
931
|
return path(d, { ...strokeAttrs(o), "marker-end": `url(#${headId(sid, o.stroke)})` });
|
|
932
932
|
}
|
|
933
|
-
function tracks(width,
|
|
934
|
-
const w =
|
|
935
|
-
return Array.from({ length:
|
|
933
|
+
function tracks(width, count2, gap, x0 = 0) {
|
|
934
|
+
const w = count2 > 1 ? (width - gap * (count2 - 1)) / count2 : width;
|
|
935
|
+
return Array.from({ length: count2 }, (_, i) => ({ x: x0 + i * (w + gap), w }));
|
|
936
936
|
}
|
|
937
937
|
function fitBoxes(req) {
|
|
938
|
-
const
|
|
938
|
+
const count2 = req.labels.length;
|
|
939
939
|
const minGap = req.minGap ?? 24;
|
|
940
940
|
const padEm = req.padEm ?? 0.8;
|
|
941
941
|
const weight = req.weight ?? 600;
|
|
942
942
|
const x0 = req.x0 ?? 0;
|
|
943
943
|
const unit = Math.max(1, ...req.labels.map((l) => textWidth(l, 1, weight) + 2 * padEm));
|
|
944
|
-
const sizeAt = (gap) =>
|
|
944
|
+
const sizeAt = (gap) => count2 < 1 ? req.size : (req.width - gap * (count2 - 1)) / count2 / unit;
|
|
945
945
|
if (sizeAt(req.gap) >= req.size) {
|
|
946
|
-
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width,
|
|
946
|
+
return { ok: true, size: req.size, gap: req.gap, boxes: tracks(req.width, count2, req.gap, x0) };
|
|
947
947
|
}
|
|
948
|
-
if (
|
|
949
|
-
const gap = (req.width -
|
|
950
|
-
return { ok: true, size: req.size, gap, boxes: tracks(req.width,
|
|
948
|
+
if (count2 > 1 && sizeAt(minGap) >= req.size) {
|
|
949
|
+
const gap = (req.width - count2 * req.size * unit) / (count2 - 1);
|
|
950
|
+
return { ok: true, size: req.size, gap, boxes: tracks(req.width, count2, gap, x0) };
|
|
951
951
|
}
|
|
952
952
|
const size = sizeAt(minGap);
|
|
953
953
|
if (size >= MIN_FONT) {
|
|
954
|
-
return { ok: true, size, gap: minGap, boxes: tracks(req.width,
|
|
954
|
+
return { ok: true, size, gap: minGap, boxes: tracks(req.width, count2, minGap, x0) };
|
|
955
955
|
}
|
|
956
956
|
return {
|
|
957
957
|
ok: false,
|
|
958
958
|
size: MIN_FONT,
|
|
959
959
|
gap: minGap,
|
|
960
|
-
boxes: tracks(req.width,
|
|
961
|
-
needed:
|
|
960
|
+
boxes: tracks(req.width, count2, minGap, x0),
|
|
961
|
+
needed: count2 * MIN_FONT * unit + minGap * (count2 - 1)
|
|
962
962
|
};
|
|
963
963
|
}
|
|
964
964
|
|
|
@@ -1291,8 +1291,8 @@ var MID_GAP = 56;
|
|
|
1291
1291
|
var BAND_GAP = 34;
|
|
1292
1292
|
var COL_GAP = 28;
|
|
1293
1293
|
var MIN_COL = 250;
|
|
1294
|
-
function lineBudget(
|
|
1295
|
-
const perSide = Math.max(1, Math.ceil(
|
|
1294
|
+
function lineBudget(count2, stageH) {
|
|
1295
|
+
const perSide = Math.max(1, Math.ceil(count2 / 2));
|
|
1296
1296
|
const room = (stageH - (perSide - 1) * LAB_GAP) / perSide;
|
|
1297
1297
|
const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
|
|
1298
1298
|
return Math.min(LINE_CEIL, Math.max(1, lines));
|
|
@@ -1703,7 +1703,7 @@ var HEAD_GAP = 14;
|
|
|
1703
1703
|
var barCompare = (beat, ctx) => {
|
|
1704
1704
|
const { sid, theme } = ctx;
|
|
1705
1705
|
const p = beat.params;
|
|
1706
|
-
const
|
|
1706
|
+
const count2 = p.bars.length;
|
|
1707
1707
|
const W = contentW(ctx.format);
|
|
1708
1708
|
const face = faceOf(ctx.theme.fontStack);
|
|
1709
1709
|
const tall = isPortrait(ctx.format);
|
|
@@ -1717,7 +1717,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1717
1717
|
0,
|
|
1718
1718
|
face
|
|
1719
1719
|
);
|
|
1720
|
-
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (
|
|
1720
|
+
let bar = Math.min(BAR_MAX, (avail - unitBand - FOOT) / (count2 + GAP_RATIO * (count2 - 1)));
|
|
1721
1721
|
let pitch = bar * (1 + GAP_RATIO);
|
|
1722
1722
|
const gutterMax = tall ? 0 : share(GUTTER_MAX, W);
|
|
1723
1723
|
const gutterInner = tall ? W : gutterMax - GUTTER_PAD;
|
|
@@ -1739,7 +1739,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1739
1739
|
if (tall) {
|
|
1740
1740
|
bar = Math.min(
|
|
1741
1741
|
BAR_MAX,
|
|
1742
|
-
(avail - unitBand - FOOT -
|
|
1742
|
+
(avail - unitBand - FOOT - count2 * head) / (count2 + GAP_RATIO * (count2 - 1))
|
|
1743
1743
|
);
|
|
1744
1744
|
pitch = head + bar * (1 + GAP_RATIO);
|
|
1745
1745
|
} else {
|
|
@@ -1750,7 +1750,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1750
1750
|
}
|
|
1751
1751
|
}
|
|
1752
1752
|
const valueSize = Math.max(MIN_FONT, Math.min(VALUE_MAX, bar * 0.9));
|
|
1753
|
-
const barsH = (
|
|
1753
|
+
const barsH = (count2 - 1) * pitch + head + bar;
|
|
1754
1754
|
const foot = Math.max(
|
|
1755
1755
|
0,
|
|
1756
1756
|
Math.ceil(
|
|
@@ -1763,7 +1763,7 @@ var barCompare = (beat, ctx) => {
|
|
|
1763
1763
|
const H = barsH + unitBand + foot;
|
|
1764
1764
|
if (H > avail) {
|
|
1765
1765
|
throw new Error(
|
|
1766
|
-
`bar-compare ${beat.id}: ${
|
|
1766
|
+
`bar-compare ${beat.id}: ${count2} bars with labels this long need ${Math.ceil(H)}px of the ${Math.floor(avail)}px this slide has. Shorten the labels or split the beat.`
|
|
1767
1767
|
);
|
|
1768
1768
|
}
|
|
1769
1769
|
const gutter = tall ? 0 : Math.min(
|
|
@@ -1916,7 +1916,7 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1916
1916
|
</div>${note}`;
|
|
1917
1917
|
const railsAt = 0.6;
|
|
1918
1918
|
const barsAt = 0.95;
|
|
1919
|
-
const step = Math.min(0.4, 2.4 /
|
|
1919
|
+
const step = Math.min(0.4, 2.4 / count2);
|
|
1920
1920
|
const grow = 0.85;
|
|
1921
1921
|
const tl = [
|
|
1922
1922
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
@@ -1966,11 +1966,11 @@ ${svg(id(sid, "chart"), W, H, body)}
|
|
|
1966
1966
|
);
|
|
1967
1967
|
}
|
|
1968
1968
|
}
|
|
1969
|
-
const settled = barsAt + (
|
|
1969
|
+
const settled = barsAt + (count2 - 1) * step + grow + 0.05;
|
|
1970
1970
|
const holds = [settled + 0.2];
|
|
1971
1971
|
const focal = rows.reduce((best, r) => Math.abs(r.value) > Math.abs(best.value) ? r : best);
|
|
1972
1972
|
const spot = spotlighter(sid, ".bc-bar");
|
|
1973
|
-
if (
|
|
1973
|
+
if (count2 > 1) tl.push(...spot.lit(`#${id(sid, "bar", focal.i)}`, settled));
|
|
1974
1974
|
const tailAt = settled + 0.3;
|
|
1975
1975
|
if (p.unit) {
|
|
1976
1976
|
tl.push(tween(`#${id(sid, "unit")}`, { opacity: 0 }, { opacity: 1, duration: 0.5 }, tailAt));
|
|
@@ -2463,6 +2463,90 @@ var dataTable = (beat, ctx) => {
|
|
|
2463
2463
|
};
|
|
2464
2464
|
};
|
|
2465
2465
|
|
|
2466
|
+
// src/emit/tex.ts
|
|
2467
|
+
import katex from "katex";
|
|
2468
|
+
var UNFIT_ATTR = "data-ds-unfit";
|
|
2469
|
+
var trust = (c) => c.command === "\\htmlClass";
|
|
2470
|
+
function texError(tex, display) {
|
|
2471
|
+
try {
|
|
2472
|
+
katex.renderToString(tex, {
|
|
2473
|
+
displayMode: display,
|
|
2474
|
+
throwOnError: true,
|
|
2475
|
+
strict: false,
|
|
2476
|
+
trust,
|
|
2477
|
+
output: "html"
|
|
2478
|
+
});
|
|
2479
|
+
return null;
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
return err instanceof Error ? err.message : String(err);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
var LEFT = /\\left(?![a-zA-Z])/g;
|
|
2485
|
+
var RIGHT = /\\right(?![a-zA-Z])/g;
|
|
2486
|
+
var NO_INK = /\\label\{[^{}]*\}|\\nonumber(?![a-zA-Z])|\\notag(?![a-zA-Z])/g;
|
|
2487
|
+
function braceBalance(tex) {
|
|
2488
|
+
let depth = 0;
|
|
2489
|
+
for (let i = 0; i < tex.length; i++) {
|
|
2490
|
+
const c = tex[i];
|
|
2491
|
+
if (c === "\\") {
|
|
2492
|
+
i++;
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
if (c === "{") depth++;
|
|
2496
|
+
else if (c === "}") depth--;
|
|
2497
|
+
}
|
|
2498
|
+
return depth;
|
|
2499
|
+
}
|
|
2500
|
+
var count = (tex, re) => (tex.match(re) ?? []).length;
|
|
2501
|
+
function repairTex(input, display) {
|
|
2502
|
+
const repairs = [];
|
|
2503
|
+
let tex = input;
|
|
2504
|
+
let error = texError(tex, display);
|
|
2505
|
+
if (error === null) return { tex, repairs, error };
|
|
2506
|
+
const gone = tex.match(NO_INK) ?? [];
|
|
2507
|
+
if (gone.length) {
|
|
2508
|
+
tex = tex.replace(NO_INK, "").trim();
|
|
2509
|
+
repairs.push(`removed ${gone.join(", ")}, which draw nothing`);
|
|
2510
|
+
error = texError(tex, display);
|
|
2511
|
+
}
|
|
2512
|
+
const lefts = count(tex, LEFT);
|
|
2513
|
+
const rights = count(tex, RIGHT);
|
|
2514
|
+
if (error !== null && lefts !== rights) {
|
|
2515
|
+
const n3 = Math.abs(lefts - rights);
|
|
2516
|
+
tex = lefts > rights ? `${tex} ${"\\right.".repeat(n3)}` : `${"\\left.".repeat(n3)} ${tex}`;
|
|
2517
|
+
repairs.push(
|
|
2518
|
+
`${lefts} \\left against ${rights} \\right: added ${n3} invisible ${lefts > rights ? "\\right." : "\\left."} to balance them`
|
|
2519
|
+
);
|
|
2520
|
+
error = texError(tex, display);
|
|
2521
|
+
}
|
|
2522
|
+
const depth = braceBalance(tex);
|
|
2523
|
+
if (error !== null && depth !== 0) {
|
|
2524
|
+
const n3 = Math.abs(depth);
|
|
2525
|
+
tex = depth > 0 ? `${tex}${"}".repeat(n3)}` : `${"{".repeat(n3)}${tex}`;
|
|
2526
|
+
repairs.push(
|
|
2527
|
+
`${n3} unclosed brace group(s): ${depth > 0 ? "closed at the end" : "opened at the start"}`
|
|
2528
|
+
);
|
|
2529
|
+
error = texError(tex, display);
|
|
2530
|
+
}
|
|
2531
|
+
const undefinedNames = [];
|
|
2532
|
+
for (let round6 = 0; error !== null && round6 < 12; round6++) {
|
|
2533
|
+
const m = /Undefined control sequence: \\([a-zA-Z]+)/.exec(error);
|
|
2534
|
+
if (!m) break;
|
|
2535
|
+
const name = m[1];
|
|
2536
|
+
const next = tex.replace(new RegExp(`\\\\${name}(?![a-zA-Z])`, "g"), `\\operatorname{${name}}`);
|
|
2537
|
+
if (next === tex) break;
|
|
2538
|
+
tex = next;
|
|
2539
|
+
undefinedNames.push(`\\${name}`);
|
|
2540
|
+
error = texError(tex, display);
|
|
2541
|
+
}
|
|
2542
|
+
if (undefinedNames.length) {
|
|
2543
|
+
repairs.push(
|
|
2544
|
+
`${undefinedNames.join(", ")} ${undefinedNames.length === 1 ? "is" : "are"} never defined in the source \u2014 drawn as ${undefinedNames.length === 1 ? "its name" : "their names"} in upright type`
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
return { tex, repairs, error };
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2466
2550
|
// src/emit/archetypes/equation-walk.ts
|
|
2467
2551
|
var TRUST = 'function (c) { return c.command === "\\\\htmlClass"; }';
|
|
2468
2552
|
var OPTS = `{ displayMode: true, trust: ${TRUST}, strict: false, output: "html" }`;
|
|
@@ -2477,6 +2561,9 @@ var SYNONYMS = [
|
|
|
2477
2561
|
function normalise(tex) {
|
|
2478
2562
|
let out = "";
|
|
2479
2563
|
const map = [];
|
|
2564
|
+
const ends = [];
|
|
2565
|
+
const lead = [];
|
|
2566
|
+
let hint = null;
|
|
2480
2567
|
for (let i = 0; i < tex.length; ) {
|
|
2481
2568
|
if (/\s/.test(tex[i])) {
|
|
2482
2569
|
i++;
|
|
@@ -2488,19 +2575,19 @@ function normalise(tex) {
|
|
|
2488
2575
|
const m = new RegExp(`^(?:${re.source})`).exec(rest);
|
|
2489
2576
|
return m ? { len: m[0].length, to } : null;
|
|
2490
2577
|
}).find(Boolean);
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
i
|
|
2497
|
-
|
|
2578
|
+
const len = hit ? hit.len : 1;
|
|
2579
|
+
const chars = hit ? hit.to : tex[i];
|
|
2580
|
+
if (chars === "") hint ??= i;
|
|
2581
|
+
for (const ch of chars) {
|
|
2582
|
+
out += ch;
|
|
2583
|
+
map.push(i);
|
|
2584
|
+
ends.push(i + len);
|
|
2585
|
+
lead.push(hint ?? i);
|
|
2586
|
+
hint = null;
|
|
2498
2587
|
}
|
|
2499
|
-
|
|
2500
|
-
map.push(i);
|
|
2501
|
-
i++;
|
|
2588
|
+
i += len;
|
|
2502
2589
|
}
|
|
2503
|
-
return { text: out, map };
|
|
2590
|
+
return { text: out, map, ends, lead };
|
|
2504
2591
|
}
|
|
2505
2592
|
function locate(tex, term) {
|
|
2506
2593
|
const hay = normalise(tex);
|
|
@@ -2508,58 +2595,131 @@ function locate(tex, term) {
|
|
|
2508
2595
|
if (needle === "") return null;
|
|
2509
2596
|
const at = hay.text.indexOf(needle);
|
|
2510
2597
|
if (at < 0) return null;
|
|
2511
|
-
const start = hay.
|
|
2512
|
-
const
|
|
2513
|
-
const lastOrig = hay.map[lastNorm];
|
|
2514
|
-
let end = lastOrig + 1;
|
|
2515
|
-
while (end < tex.length && (hay.map[lastNorm + 1] ?? tex.length) > end && /\s/.test(tex[end])) {
|
|
2516
|
-
end++;
|
|
2517
|
-
}
|
|
2518
|
-
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
2598
|
+
const start = hay.lead[at];
|
|
2599
|
+
const end = hay.ends[at + needle.length - 1];
|
|
2519
2600
|
return { start, end: Math.min(end, tex.length) };
|
|
2520
2601
|
}
|
|
2521
2602
|
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
2603
|
+
const checked = texError(tex, true) === null;
|
|
2522
2604
|
let parts = [{ text: tex, raw: true }];
|
|
2523
2605
|
const used = [];
|
|
2524
2606
|
const missing = [];
|
|
2607
|
+
const broken = [];
|
|
2608
|
+
const joined = (ps) => ps.map((p) => p.text).join("");
|
|
2525
2609
|
for (const term of terms) {
|
|
2526
2610
|
let placed = false;
|
|
2611
|
+
let found = false;
|
|
2527
2612
|
for (let i = 0; i < parts.length && !placed; i++) {
|
|
2528
2613
|
const part = parts[i];
|
|
2529
2614
|
if (!part.raw) continue;
|
|
2530
2615
|
const at = locate(part.text, term.tex);
|
|
2531
2616
|
if (!at) continue;
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
raw:
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2617
|
+
found = true;
|
|
2618
|
+
const body = `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`;
|
|
2619
|
+
for (const text2 of [body, `{${body}}`]) {
|
|
2620
|
+
const next = parts.toSpliced(
|
|
2621
|
+
i,
|
|
2622
|
+
1,
|
|
2623
|
+
{ text: part.text.slice(0, at.start), raw: true },
|
|
2624
|
+
{ text: text2, raw: false },
|
|
2625
|
+
{ text: part.text.slice(at.end), raw: true }
|
|
2626
|
+
);
|
|
2627
|
+
if (checked && texError(joined(next), true) !== null) continue;
|
|
2628
|
+
parts = next;
|
|
2629
|
+
used.push(term);
|
|
2630
|
+
placed = true;
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (!placed) {
|
|
2635
|
+
missing.push(term);
|
|
2636
|
+
if (found) broken.push(term);
|
|
2544
2637
|
}
|
|
2545
|
-
if (!placed) missing.push(term);
|
|
2546
2638
|
}
|
|
2547
2639
|
if (used.length === 0) {
|
|
2548
2640
|
throw new Error(
|
|
2549
|
-
`equation-walk ${beatId}: none of its ${terms.length} term(s) occur in the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(tex)}`
|
|
2641
|
+
`equation-walk ${beatId}: none of its ${terms.length} term(s) ${broken.length ? "can be highlighted in" : "occur in"} the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(tex)}`
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
return { tex: joined(parts), used, missing, broken };
|
|
2645
|
+
}
|
|
2646
|
+
function droppedTerms(eqId, missing, broken) {
|
|
2647
|
+
return missing.map(
|
|
2648
|
+
(t2) => broken.includes(t2) ? `term ${JSON.stringify(t2.tex)} is in ${eqId} but no span around it parses, so it is not highlighted and its legend row ("${t2.label}") is left out` : `term ${JSON.stringify(t2.tex)} does not occur in ${eqId}, so its legend row ("${t2.label}") is left out`
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
function prepareWalk(eq, terms, beatId) {
|
|
2652
|
+
const fixed = repairTex(eq.tex, true);
|
|
2653
|
+
const notes = fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`);
|
|
2654
|
+
const repairedTerms = terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
2655
|
+
if (fixed.error !== null) {
|
|
2656
|
+
notes.push(
|
|
2657
|
+
`${eq.id} does not parse even after repair (${fixed.error}), so it is shown as its TeX source in plain text \u2014 ${JSON.stringify(eq.tex)}`
|
|
2658
|
+
);
|
|
2659
|
+
const parts = segment(eq.tex, [...terms]);
|
|
2660
|
+
if (parts.used.length === 0) {
|
|
2661
|
+
throw new Error(
|
|
2662
|
+
`equation-walk ${beatId}: none of its ${terms.length} term(s) occur in the equation. Terms: ${terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. Equation: ${JSON.stringify(eq.tex)}`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
notes.push(...droppedTerms(eq.id, parts.missing, []));
|
|
2666
|
+
return {
|
|
2667
|
+
raw: eq.tex,
|
|
2668
|
+
plain: { error: fixed.error },
|
|
2669
|
+
used: parts.used,
|
|
2670
|
+
missing: parts.missing,
|
|
2671
|
+
notes
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
const walk = wrapTerms(fixed.tex, repairedTerms, beatId);
|
|
2675
|
+
const back = (ts) => ts.map((t2) => terms[repairedTerms.indexOf(t2)]);
|
|
2676
|
+
const used = back(walk.used);
|
|
2677
|
+
const missing = back(walk.missing);
|
|
2678
|
+
notes.push(...droppedTerms(eq.id, missing, back(walk.broken)));
|
|
2679
|
+
return { tex: walk.tex, raw: fixed.tex, used, missing, notes };
|
|
2680
|
+
}
|
|
2681
|
+
function segment(tex, terms) {
|
|
2682
|
+
let parts = [{ text: tex }];
|
|
2683
|
+
const used = [];
|
|
2684
|
+
const missing = [];
|
|
2685
|
+
for (const term of terms) {
|
|
2686
|
+
const i = parts.findIndex((p) => !p.term && locate(p.text, term.tex));
|
|
2687
|
+
const part = parts[i];
|
|
2688
|
+
const at = part && locate(part.text, term.tex);
|
|
2689
|
+
if (!part || !at) {
|
|
2690
|
+
missing.push(term);
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
parts = parts.toSpliced(
|
|
2694
|
+
i,
|
|
2695
|
+
1,
|
|
2696
|
+
{ text: part.text.slice(0, at.start) },
|
|
2697
|
+
{ text: part.text.slice(at.start, at.end), term },
|
|
2698
|
+
{ text: part.text.slice(at.end) }
|
|
2550
2699
|
);
|
|
2700
|
+
used.push(term);
|
|
2551
2701
|
}
|
|
2552
|
-
return {
|
|
2702
|
+
return { parts, used, missing };
|
|
2703
|
+
}
|
|
2704
|
+
function plainHtml(tex, terms) {
|
|
2705
|
+
return segment(tex, terms).parts.map(
|
|
2706
|
+
(p) => p.term ? `<span class="term t-${p.term.tone}">${esc(p.text)}</span>` : esc(p.text)
|
|
2707
|
+
).join("");
|
|
2708
|
+
}
|
|
2709
|
+
function chipSetup(sid, term) {
|
|
2710
|
+
const fixed = repairTex(term.tex, false);
|
|
2711
|
+
const el = `document.getElementById("${sid}-chip-${term.tone}")`;
|
|
2712
|
+
return fixed.error === null ? `katex.render('${js(fixed.tex)}', ${el}, ${INLINE_OPTS});` : `${el}.textContent = '${js(term.tex)}';`;
|
|
2553
2713
|
}
|
|
2554
2714
|
function equationSize(tex) {
|
|
2555
2715
|
return tex.length > 120 ? 68 : tex.length > 90 ? 80 : tex.length > 55 ? 92 : 108;
|
|
2556
2716
|
}
|
|
2557
|
-
var
|
|
2717
|
+
var NO_INK2 = /\\(math[a-z]+|text[a-z]*|bm|boldsymbol|displaystyle|textstyle|scriptstyle|left|right|operatorname|limits|nolimits|htmlClass|hspace|phantom|[,;:!])/g;
|
|
2558
2718
|
var GLYPH_EM = 0.9;
|
|
2559
2719
|
function texUnits(tex) {
|
|
2560
2720
|
let ems = 0;
|
|
2561
2721
|
for (const m of tex.matchAll(/\\qquad|\\quad/g)) ems += m[0] === "\\qquad" ? 2 : 1;
|
|
2562
|
-
const glyphs = tex.replace(/\\q?quad/g, "").replace(
|
|
2722
|
+
const glyphs = tex.replace(/\\q?quad/g, "").replace(NO_INK2, "").replace(/[{}\s_^]/g, "");
|
|
2563
2723
|
return ems + glyphs.length * GLYPH_EM;
|
|
2564
2724
|
}
|
|
2565
2725
|
function statements(tex, stacked) {
|
|
@@ -2567,6 +2727,100 @@ function statements(tex, stacked) {
|
|
|
2567
2727
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
2568
2728
|
return parts.length > 0 ? parts : [tex];
|
|
2569
2729
|
}
|
|
2730
|
+
var PLAIN_FONT = 48;
|
|
2731
|
+
function mathFit(sid, targets, shown, raw2, want, floor) {
|
|
2732
|
+
return [
|
|
2733
|
+
`var dsEqFit = (function () {
|
|
2734
|
+
var box = document.getElementById("${sid}-eq");
|
|
2735
|
+
var slide = box && box.parentNode;
|
|
2736
|
+
var bw = box ? box.getBoundingClientRect().width : 0;
|
|
2737
|
+
if (!(bw > 0) || !slide) return "unmeasured";
|
|
2738
|
+
var targets = [${targets.map((id2) => `"${id2}"`).join(", ")}].map(function (id) { return document.getElementById(id); });
|
|
2739
|
+
var TEX = [${shown.map((s) => `'${js(s)}'`).join(", ")}];
|
|
2740
|
+
var spill0 = slide.scrollHeight - slide.clientHeight;
|
|
2741
|
+
function extent(each) {
|
|
2742
|
+
var w = 0;
|
|
2743
|
+
targets.forEach(function (t) {
|
|
2744
|
+
var lo = Infinity, hi = -Infinity;
|
|
2745
|
+
Array.prototype.forEach.call(t.querySelectorAll(".katex-html > .base"), function (b) {
|
|
2746
|
+
var r = b.getBoundingClientRect();
|
|
2747
|
+
if (each) w = Math.max(w, r.width);
|
|
2748
|
+
else { lo = Math.min(lo, r.left); hi = Math.max(hi, r.right); }
|
|
2749
|
+
});
|
|
2750
|
+
if (!each && hi > lo) w = Math.max(w, hi - lo);
|
|
2751
|
+
});
|
|
2752
|
+
return w;
|
|
2753
|
+
}
|
|
2754
|
+
function fits(each) {
|
|
2755
|
+
return extent(each) <= bw + 0.5 && slide.scrollHeight - slide.clientHeight <= Math.max(1, spill0 + 1);
|
|
2756
|
+
}
|
|
2757
|
+
function settle(each, size) {
|
|
2758
|
+
size = Math.max(${floor}, Math.min(${want}, Math.floor(size)));
|
|
2759
|
+
box.style.fontSize = size + "px";
|
|
2760
|
+
while (!fits(each) && size > ${floor}) {
|
|
2761
|
+
size = Math.max(${floor}, size - 2);
|
|
2762
|
+
box.style.fontSize = size + "px";
|
|
2763
|
+
}
|
|
2764
|
+
return fits(each);
|
|
2765
|
+
}
|
|
2766
|
+
if (fits(false)) return "fits";
|
|
2767
|
+
var size = parseFloat(box.style.fontSize);
|
|
2768
|
+
if (settle(false, (size * bw) / extent(false))) return "scaled";
|
|
2769
|
+
targets.forEach(function (t, i) { katex.render("\\\\displaystyle " + TEX[i], t, ${INLINE_OPTS}); });
|
|
2770
|
+
box.className += " eq-broken";
|
|
2771
|
+
box.style.fontSize = "${want}px";
|
|
2772
|
+
var wb = extent(true);
|
|
2773
|
+
if (settle(true, wb > bw ? (${want} * bw) / wb : ${want})) return "broken";
|
|
2774
|
+
box.setAttribute("${UNFIT_ATTR}", '${js(raw2)}');
|
|
2775
|
+
return "unfit";
|
|
2776
|
+
})()`
|
|
2777
|
+
];
|
|
2778
|
+
}
|
|
2779
|
+
function chipFit(sid, terms) {
|
|
2780
|
+
if (terms.length === 0) return [];
|
|
2781
|
+
return [
|
|
2782
|
+
`[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2783
|
+
var chip = document.getElementById("${sid}-chip-" + tone);
|
|
2784
|
+
var r = chip && chip.getBoundingClientRect();
|
|
2785
|
+
if (!r || !(r.height > 0)) return;
|
|
2786
|
+
var k = chip.offsetHeight / r.height;
|
|
2787
|
+
var top = r.top, bottom = r.bottom;
|
|
2788
|
+
Array.prototype.forEach.call(chip.querySelectorAll("span"), function (el) {
|
|
2789
|
+
for (var n = el.firstChild; n; n = n.nextSibling) {
|
|
2790
|
+
if (n.nodeType === 3 && n.nodeValue.trim()) {
|
|
2791
|
+
var q = el.getBoundingClientRect();
|
|
2792
|
+
top = Math.min(top, q.top);
|
|
2793
|
+
bottom = Math.max(bottom, q.bottom);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
var cs = getComputedStyle(chip);
|
|
2799
|
+
if (top < r.top) chip.style.paddingTop = parseFloat(cs.paddingTop) + Math.ceil((r.top - top) * k) + "px";
|
|
2800
|
+
if (bottom > r.bottom) chip.style.paddingBottom = parseFloat(cs.paddingBottom) + Math.ceil((bottom - r.bottom) * k) + "px";
|
|
2801
|
+
})`
|
|
2802
|
+
];
|
|
2803
|
+
}
|
|
2804
|
+
var SWELL = 1.16;
|
|
2805
|
+
var SWELL_EM = 0.06;
|
|
2806
|
+
function swellFit(sid, terms) {
|
|
2807
|
+
return [
|
|
2808
|
+
`var dsSwell = (function () {
|
|
2809
|
+
var out = {};
|
|
2810
|
+
var box = document.getElementById("${sid}-eq");
|
|
2811
|
+
var k = box && box.offsetWidth ? box.getBoundingClientRect().width / box.offsetWidth : 1;
|
|
2812
|
+
var em = box ? parseFloat(getComputedStyle(box).fontSize) : 0;
|
|
2813
|
+
[${terms.map((t2) => `"${t2.tone}"`).join(", ")}].forEach(function (tone) {
|
|
2814
|
+
var w = 0;
|
|
2815
|
+
Array.prototype.forEach.call(document.querySelectorAll("#${sid} .t-" + tone), function (el) {
|
|
2816
|
+
w = Math.max(w, el.getBoundingClientRect().width / k);
|
|
2817
|
+
});
|
|
2818
|
+
out[tone] = w > 0 && em > 0 ? Math.min(${SWELL}, Math.round((1 + (2 * ${SWELL_EM} * em) / w) * 1000) / 1000) : ${SWELL};
|
|
2819
|
+
});
|
|
2820
|
+
return out;
|
|
2821
|
+
})()`
|
|
2822
|
+
];
|
|
2823
|
+
}
|
|
2570
2824
|
function legendRows(sid, terms, theme) {
|
|
2571
2825
|
return terms.map(
|
|
2572
2826
|
(t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
|
|
@@ -2582,7 +2836,10 @@ function legendCss(theme) {
|
|
|
2582
2836
|
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
2583
2837
|
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
2584
2838
|
// are one symbol each and their natural widths differ by a few pixels.
|
|
2585
|
-
|
|
2839
|
+
// `flex:none`: beside a long label the chip used to SHRINK below its own
|
|
2840
|
+
// nowrap TeX, which then ran out of the painted box into the label — on a
|
|
2841
|
+
// HypePaper deck, "pixelcoord" 91px past the chip's right edge.
|
|
2842
|
+
`.chip{display:inline-block;flex:none;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
|
|
2586
2843
|
].join("\n");
|
|
2587
2844
|
}
|
|
2588
2845
|
var equationWalk = (beat, ctx) => {
|
|
@@ -2594,37 +2851,53 @@ var equationWalk = (beat, ctx) => {
|
|
|
2594
2851
|
`equation-walk ${beat.id}: no equation "${p.equationId}" in source ${ctx.source.id}`
|
|
2595
2852
|
);
|
|
2596
2853
|
}
|
|
2597
|
-
const walk =
|
|
2854
|
+
const walk = prepareWalk(eq, p.terms, beat.id);
|
|
2598
2855
|
const terms = walk.used;
|
|
2599
2856
|
const legend = legendRows(sid, terms, theme);
|
|
2600
|
-
const
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2857
|
+
const chips = terms.map((t2) => chipSetup(sid, t2));
|
|
2858
|
+
let eqHtml;
|
|
2859
|
+
let setup;
|
|
2860
|
+
let measure4;
|
|
2861
|
+
if (walk.plain) {
|
|
2862
|
+
eqHtml = `<div class="eq eq-plain" id="${sid}-eq" style="font-size:${PLAIN_FONT}px">${plainHtml(eq.tex, terms)}</div>`;
|
|
2863
|
+
setup = chips;
|
|
2864
|
+
measure4 = [...chipFit(sid, terms), ...swellFit(sid, terms)];
|
|
2865
|
+
} else {
|
|
2866
|
+
const wrapped = walk.tex;
|
|
2867
|
+
const split = statements(wrapped, isPortrait(ctx.format));
|
|
2868
|
+
const stacked = split.length > 1 && split.every((s) => texError(s, true) === null);
|
|
2869
|
+
const raw2 = statements(walk.raw, stacked);
|
|
2870
|
+
const shown = stacked ? split : [wrapped];
|
|
2871
|
+
const longest = raw2.reduce((a, b) => b.length > a.length ? b : a);
|
|
2872
|
+
const want = equationSize(longest);
|
|
2873
|
+
const size = Math.max(
|
|
2874
|
+
MIN_FONT,
|
|
2875
|
+
Math.min(want, Math.floor(contentW(ctx.format) / Math.max(...raw2.map(texUnits))))
|
|
2876
|
+
);
|
|
2877
|
+
const body = shown.length === 1 ? "" : shown.map((_, i) => `<div id="${sid}-eq${i}"></div>`).join("\n ");
|
|
2878
|
+
eqHtml = `<div class="eq${shown.length === 1 ? "" : " eqstack"}" id="${sid}-eq" style="font-size:${size}px">${body}</div>`;
|
|
2879
|
+
const targets = shown.map((_, i) => `${sid}-eq${shown.length === 1 ? "" : i}`);
|
|
2880
|
+
setup = [
|
|
2881
|
+
`var OPTS = ${OPTS};`,
|
|
2882
|
+
...shown.map(
|
|
2883
|
+
(part, i) => `katex.render('${js(part)}', document.getElementById("${targets[i]}"), OPTS);`
|
|
2884
|
+
),
|
|
2885
|
+
...chips
|
|
2886
|
+
];
|
|
2887
|
+
measure4 = [
|
|
2888
|
+
...chipFit(sid, terms),
|
|
2889
|
+
...mathFit(sid, targets, shown, walk.raw, want, MIN_FONT),
|
|
2890
|
+
// Last: it measures each term at the size the fit settled on.
|
|
2891
|
+
...swellFit(sid, terms)
|
|
2892
|
+
];
|
|
2893
|
+
}
|
|
2612
2894
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
2613
2895
|
<div class="eqslide">
|
|
2614
|
-
|
|
2896
|
+
${eqHtml}
|
|
2615
2897
|
<div class="legend">
|
|
2616
2898
|
${legend}
|
|
2617
2899
|
</div>
|
|
2618
2900
|
</div>`;
|
|
2619
|
-
const setup = [
|
|
2620
|
-
`var OPTS = ${OPTS};`,
|
|
2621
|
-
...shown.map(
|
|
2622
|
-
(part, i) => `katex.render('${js(part)}', document.getElementById("${sid}-eq${shown.length === 1 ? "" : i}"), OPTS);`
|
|
2623
|
-
),
|
|
2624
|
-
...terms.map(
|
|
2625
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2626
|
-
)
|
|
2627
|
-
];
|
|
2628
2901
|
const tl = [
|
|
2629
2902
|
...chromeIn(sid, p.eyebrow !== void 0),
|
|
2630
2903
|
tween(`#${sid}-eq`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8)
|
|
@@ -2636,6 +2909,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2636
2909
|
);
|
|
2637
2910
|
const holds = [];
|
|
2638
2911
|
const spot = spotlighter(sid, ".term");
|
|
2912
|
+
const swell = (t2) => raw(`dsSwell.${t2.tone}`);
|
|
2639
2913
|
terms.forEach((term, i) => {
|
|
2640
2914
|
const at = first + i * step;
|
|
2641
2915
|
const colour = theme.tones[term.tone];
|
|
@@ -2652,13 +2926,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2652
2926
|
tween(
|
|
2653
2927
|
`#${sid} .t-${term.tone}`,
|
|
2654
2928
|
{ color: theme.fg, scale: 1 },
|
|
2655
|
-
{ color: colour, scale:
|
|
2929
|
+
{ color: colour, scale: swell(term), duration: 0.5 },
|
|
2656
2930
|
at
|
|
2657
2931
|
)
|
|
2658
2932
|
);
|
|
2659
2933
|
const prev = terms[i - 1];
|
|
2660
2934
|
if (prev) {
|
|
2661
|
-
tl.push(
|
|
2935
|
+
tl.push(
|
|
2936
|
+
tween(`#${sid} .t-${prev.tone}`, { scale: swell(prev) }, { scale: 1, duration: 0.4 }, at)
|
|
2937
|
+
);
|
|
2662
2938
|
}
|
|
2663
2939
|
holds.push(at + 0.6);
|
|
2664
2940
|
});
|
|
@@ -2666,7 +2942,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
2666
2942
|
if (last) {
|
|
2667
2943
|
const at = first + terms.length * step;
|
|
2668
2944
|
tl.push(
|
|
2669
|
-
tween(`#${sid} .t-${last.tone}`, { scale:
|
|
2945
|
+
tween(`#${sid} .t-${last.tone}`, { scale: swell(last) }, { scale: 1, duration: 0.4 }, at),
|
|
2670
2946
|
...spot.restore(at)
|
|
2671
2947
|
);
|
|
2672
2948
|
}
|
|
@@ -2674,6 +2950,8 @@ var equationWalk = (beat, ctx) => {
|
|
|
2674
2950
|
html,
|
|
2675
2951
|
tl,
|
|
2676
2952
|
setup,
|
|
2953
|
+
measure: measure4,
|
|
2954
|
+
...walk.notes.length ? { warnings: walk.notes } : {},
|
|
2677
2955
|
holds: holdsWithin(holds, beat.seconds),
|
|
2678
2956
|
css: [
|
|
2679
2957
|
chromeCss(theme),
|
|
@@ -2693,6 +2971,15 @@ var equationWalk = (beat, ctx) => {
|
|
|
2693
2971
|
// has its margin zeroed above, so without an explicit gap the two lines
|
|
2694
2972
|
// would touch and read as one equation broken mid-expression.
|
|
2695
2973
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
2974
|
+
// Set by `mathFit` when one line will not fit at the floor: the display is
|
|
2975
|
+
// re-rendered inline, where KaTeX breaks after a top-level relation or
|
|
2976
|
+
// operator, and the lines it breaks into are centred and spaced as lines.
|
|
2977
|
+
".eq-broken{line-height:1.5}",
|
|
2978
|
+
// An equation KaTeX refuses, shown as its source. Wraps anywhere, because
|
|
2979
|
+
// TeX source has few spaces and must not run off the slide either. The
|
|
2980
|
+
// deck's own face, not a monospace one: a family the bundle does not
|
|
2981
|
+
// declare falls back silently (invariant 9).
|
|
2982
|
+
".eq-plain{white-space:pre-wrap;overflow-wrap:anywhere}",
|
|
2696
2983
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
2697
2984
|
".term{display:inline-block}",
|
|
2698
2985
|
legendCss(theme),
|
|
@@ -2707,26 +2994,67 @@ var equationWalk = (beat, ctx) => {
|
|
|
2707
2994
|
|
|
2708
2995
|
// src/emit/archetypes/equation-morph.ts
|
|
2709
2996
|
var MORPH_SECONDS = 1.6;
|
|
2710
|
-
|
|
2711
|
-
const { sid, theme } = ctx;
|
|
2997
|
+
function prepareMorph(beat, source) {
|
|
2712
2998
|
const p = beat.params;
|
|
2713
2999
|
const find3 = (id2) => {
|
|
2714
|
-
const eq =
|
|
3000
|
+
const eq = source.equations.find((e) => e.id === id2);
|
|
2715
3001
|
if (!eq)
|
|
2716
|
-
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${
|
|
3002
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${source.id}`);
|
|
2717
3003
|
return eq;
|
|
2718
3004
|
};
|
|
2719
|
-
const
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
|
|
3005
|
+
const notes = [];
|
|
3006
|
+
const drawable = (eq) => {
|
|
3007
|
+
const fixed = repairTex(eq.tex, true);
|
|
3008
|
+
if (fixed.error !== null) {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
`equation-morph ${beat.id}: ${eq.id} does not parse even after repair (${fixed.error}), and a morph cannot be drawn from plain text. Formula: ${JSON.stringify(eq.tex)}`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
notes.push(...fixed.repairs.map((r) => `${eq.id} repaired before drawing: ${r}`));
|
|
3014
|
+
return { id: eq.id, tex: fixed.tex };
|
|
3015
|
+
};
|
|
3016
|
+
const a = drawable(find3(p.fromId));
|
|
3017
|
+
const b = drawable(find3(p.toId));
|
|
3018
|
+
const repaired = p.terms.map((t2) => ({ ...t2, tex: repairTex(t2.tex, false).tex }));
|
|
3019
|
+
const original = (t2) => p.terms[repaired.indexOf(t2)];
|
|
3020
|
+
let keyed = repaired.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
3021
|
+
if (keyed.length === 0) {
|
|
2723
3022
|
throw new Error(
|
|
2724
3023
|
`equation-morph ${beat.id}: none of its ${p.terms.length} term(s) occur in both equations. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. From: ${JSON.stringify(a.tex)}. To: ${JSON.stringify(b.tex)}`
|
|
2725
3024
|
);
|
|
2726
3025
|
}
|
|
3026
|
+
notes.push(
|
|
3027
|
+
...repaired.filter((t2) => !keyed.includes(t2)).map(
|
|
3028
|
+
(t2) => `term ${JSON.stringify(original(t2).tex)} does not occur in both ${a.id} and ${b.id}, so it does not travel and its legend row ("${t2.label}") is left out`
|
|
3029
|
+
)
|
|
3030
|
+
);
|
|
2727
3031
|
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
2728
|
-
|
|
2729
|
-
|
|
3032
|
+
let wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
3033
|
+
let wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
3034
|
+
const paired = keyed.filter((t2) => wa.used.includes(t2) && wb.used.includes(t2));
|
|
3035
|
+
if (paired.length < keyed.length) {
|
|
3036
|
+
const unpaired = keyed.filter((t2) => !paired.includes(t2));
|
|
3037
|
+
notes.push(
|
|
3038
|
+
...unpaired.map(
|
|
3039
|
+
(t2) => `term ${JSON.stringify(original(t2).tex)} cannot be marked on both lines without breaking one, so it does not travel and its legend row ("${t2.label}") is left out`
|
|
3040
|
+
)
|
|
3041
|
+
);
|
|
3042
|
+
if (paired.length === 0) {
|
|
3043
|
+
throw new Error(
|
|
3044
|
+
`equation-morph ${beat.id}: no term can be marked on both lines without breaking one. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}`
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
keyed = paired;
|
|
3048
|
+
wa = wrapTerms(a.tex, keyed, beat.id, cls);
|
|
3049
|
+
wb = wrapTerms(b.tex, keyed, beat.id, cls);
|
|
3050
|
+
}
|
|
3051
|
+
const both = keyed.map(original);
|
|
3052
|
+
return { a, b, wa: wa.tex, wb: wb.tex, both, notes };
|
|
3053
|
+
}
|
|
3054
|
+
var equationMorph = (beat, ctx) => {
|
|
3055
|
+
const { sid, theme } = ctx;
|
|
3056
|
+
const p = beat.params;
|
|
3057
|
+
const { a, b, wa, wb, both, notes } = prepareMorph(beat, ctx.source);
|
|
2730
3058
|
const size = Math.max(
|
|
2731
3059
|
MIN_FONT,
|
|
2732
3060
|
Math.min(
|
|
@@ -2748,9 +3076,7 @@ var equationMorph = (beat, ctx) => {
|
|
|
2748
3076
|
`var OPTS = ${OPTS};`,
|
|
2749
3077
|
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
2750
3078
|
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
2751
|
-
...both.map(
|
|
2752
|
-
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
2753
|
-
)
|
|
3079
|
+
...both.map((t2) => chipSetup(sid, t2))
|
|
2754
3080
|
];
|
|
2755
3081
|
const first = 1.8;
|
|
2756
3082
|
const at = Math.round(
|
|
@@ -2783,8 +3109,11 @@ var equationMorph = (beat, ctx) => {
|
|
|
2783
3109
|
setup,
|
|
2784
3110
|
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
2785
3111
|
// the ready gate, and the plugin tween above finds it on the host.
|
|
2786
|
-
|
|
3112
|
+
// The chips are fitted before the plan is built: growing one moves the
|
|
3113
|
+
// legend, and nothing the morph measures may move after it has measured.
|
|
3114
|
+
measure: [...chipFit(sid, both), `DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
2787
3115
|
plugins: ["dsMorph"],
|
|
3116
|
+
...notes.length ? { warnings: notes } : {},
|
|
2788
3117
|
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
2789
3118
|
css: [
|
|
2790
3119
|
chromeCss(theme),
|
|
@@ -3365,16 +3694,16 @@ var lineChart = (beat, ctx) => {
|
|
|
3365
3694
|
</svg>${readout}
|
|
3366
3695
|
</div>`;
|
|
3367
3696
|
const draw2 = 0.8;
|
|
3368
|
-
const
|
|
3369
|
-
const idealStep = Math.min(0.45, DRAW_SECONDS /
|
|
3697
|
+
const count2 = p.points.length;
|
|
3698
|
+
const idealStep = Math.min(0.45, DRAW_SECONDS / count2);
|
|
3370
3699
|
const spine = draw2 + SEPARATE + RESHAPE_SECONDS + (p.readout ? 0.8 : 0) + 0.15;
|
|
3371
|
-
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length,
|
|
3700
|
+
const shownDeltas = deltas ? Math.min((p.deltas ?? []).length, count2 - 1) : 0;
|
|
3372
3701
|
const tailAfter = (s) => Math.max(
|
|
3373
|
-
s *
|
|
3702
|
+
s * count2 + 0.4,
|
|
3374
3703
|
// the ring: its walk, then the 0.4s it takes to leave
|
|
3375
|
-
0.3 + s * (
|
|
3704
|
+
0.3 + s * (count2 - 1),
|
|
3376
3705
|
// the dots
|
|
3377
|
-
0.5 + s * (
|
|
3706
|
+
0.5 + s * (count2 - 1),
|
|
3378
3707
|
...shownDeltas > 0 ? [0.95 + s * (shownDeltas - 1)] : []
|
|
3379
3708
|
);
|
|
3380
3709
|
const room = beat.seconds - spine;
|
|
@@ -3437,8 +3766,8 @@ var lineChart = (beat, ctx) => {
|
|
|
3437
3766
|
)
|
|
3438
3767
|
);
|
|
3439
3768
|
}
|
|
3440
|
-
const walk = cmp ? sec(step *
|
|
3441
|
-
if (
|
|
3769
|
+
const walk = cmp ? sec(step * count2) : DRAW_SECONDS;
|
|
3770
|
+
if (count2 > 1) {
|
|
3442
3771
|
const route = p.points.map((pt, i) => ({ x: nv(x(i) - first.x), y: nv(y(pt.y) - first.y) }));
|
|
3443
3772
|
tl.push(
|
|
3444
3773
|
tween(`#${sid}-ring`, { opacity: 0 }, { opacity: 1, duration: 0.25 }, settled),
|
|
@@ -3453,7 +3782,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3453
3782
|
)
|
|
3454
3783
|
);
|
|
3455
3784
|
}
|
|
3456
|
-
const drawn = settled + (cmp ? tailAfter(step) : step *
|
|
3785
|
+
const drawn = settled + (cmp ? tailAfter(step) : step * count2 + 0.4);
|
|
3457
3786
|
const holds = cmp ? [lift2, drawn] : [drawn];
|
|
3458
3787
|
if (p.readout) {
|
|
3459
3788
|
tl.push(
|
|
@@ -3469,7 +3798,7 @@ var lineChart = (beat, ctx) => {
|
|
|
3469
3798
|
return {
|
|
3470
3799
|
...lineChart({ ...beat, params: { ...p, compare: void 0 } }, ctx),
|
|
3471
3800
|
warnings: [
|
|
3472
|
-
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${
|
|
3801
|
+
`line-chart ${beat.id}: a comparison against "${cmp.label}" over ${count2} points needs ${need}s and the beat is ${beat.seconds}s, so the chart was drawn without it. Lengthen the beat to keep the comparison.`
|
|
3473
3802
|
]
|
|
3474
3803
|
};
|
|
3475
3804
|
}
|
|
@@ -3549,7 +3878,7 @@ var V_BOX_H = 268;
|
|
|
3549
3878
|
var V_LOOP_SIDE = 56;
|
|
3550
3879
|
var V_LOOP_PAD = 18;
|
|
3551
3880
|
function columnLayout(stageW, stageH, stages, face, loop) {
|
|
3552
|
-
const
|
|
3881
|
+
const count2 = stages.length;
|
|
3553
3882
|
const loopW = loop ? Math.max(240, Math.round(stageW * 0.32)) : 0;
|
|
3554
3883
|
const boxX = M;
|
|
3555
3884
|
const boxW = stageW - 2 * M - (loop ? V_LOOP_SIDE + V_LOOP_PAD + loopW : 0);
|
|
@@ -3566,11 +3895,11 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3566
3895
|
return Math.ceil(2 * PAD_Y2 + label + (note > 0 ? NOTE_TOP + note * NOTE * NOTE_LH : 0));
|
|
3567
3896
|
})
|
|
3568
3897
|
);
|
|
3569
|
-
const room = (stageH - 2 * M - (
|
|
3898
|
+
const room = (stageH - 2 * M - (count2 - 1) * V_GAP) / count2;
|
|
3570
3899
|
const boxH = Math.max(need, Math.min(room, V_BOX_H));
|
|
3571
3900
|
return {
|
|
3572
3901
|
size,
|
|
3573
|
-
boxes: Array.from({ length:
|
|
3902
|
+
boxes: Array.from({ length: count2 }, (_, i) => ({ x: M + i * (boxH + V_GAP), w: boxH })),
|
|
3574
3903
|
boxH,
|
|
3575
3904
|
boxX,
|
|
3576
3905
|
boxW,
|
|
@@ -3582,7 +3911,7 @@ function columnLayout(stageW, stageH, stages, face, loop) {
|
|
|
3582
3911
|
loopLines: loop ? wrap(loop.label, NOTE, loopW, 500, 0, face) : [],
|
|
3583
3912
|
loopX: boxX + boxW + V_LOOP_SIDE,
|
|
3584
3913
|
loopW,
|
|
3585
|
-
svgH: 2 * M +
|
|
3914
|
+
svgH: 2 * M + count2 * boxH + (count2 - 1) * V_GAP
|
|
3586
3915
|
};
|
|
3587
3916
|
}
|
|
3588
3917
|
function balance(label, k, face) {
|
|
@@ -4300,8 +4629,8 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4300
4629
|
if (inline2.fits) return inline2;
|
|
4301
4630
|
return inline2.wide && inline2.blockH < stacked.blockH ? inline2 : stacked;
|
|
4302
4631
|
}
|
|
4303
|
-
function labelWeight(i,
|
|
4304
|
-
return i ===
|
|
4632
|
+
function labelWeight(i, count2) {
|
|
4633
|
+
return i === count2 - 1 ? 700 : 600;
|
|
4305
4634
|
}
|
|
4306
4635
|
function floorFor(p, format) {
|
|
4307
4636
|
if (!p.tilt) return MIN_FONT;
|
|
@@ -4311,7 +4640,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4311
4640
|
const width = contentW(format);
|
|
4312
4641
|
const boxH = contentH(format);
|
|
4313
4642
|
const floor = floorFor(p, format);
|
|
4314
|
-
const
|
|
4643
|
+
const count2 = p.layers.length;
|
|
4315
4644
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4316
4645
|
const riseMax = RISE_MAX[k];
|
|
4317
4646
|
const syMax = SY_MAX[k];
|
|
@@ -4319,7 +4648,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4319
4648
|
const noteW = (l) => inline2 && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4320
4649
|
const want = Math.max(
|
|
4321
4650
|
...p.layers.map(
|
|
4322
|
-
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i,
|
|
4651
|
+
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count2), 0, false, face) + noteW(l)
|
|
4323
4652
|
)
|
|
4324
4653
|
);
|
|
4325
4654
|
const colCap = inline2 ? width * 0.56 : width * 0.5;
|
|
@@ -4328,7 +4657,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4328
4657
|
const labelX = width - colW;
|
|
4329
4658
|
const labelRoom = Math.min(
|
|
4330
4659
|
...p.layers.map(
|
|
4331
|
-
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i,
|
|
4660
|
+
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count2), 0, false, face))
|
|
4332
4661
|
)
|
|
4333
4662
|
);
|
|
4334
4663
|
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
@@ -4336,7 +4665,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4336
4665
|
const nw = noteW(l);
|
|
4337
4666
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
4338
4667
|
return {
|
|
4339
|
-
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i,
|
|
4668
|
+
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count2), 0, face),
|
|
4340
4669
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4341
4670
|
// short line" — and wrapping one would put its second line under the label.
|
|
4342
4671
|
note: l.note === void 0 ? [] : inline2 ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
@@ -4354,13 +4683,13 @@ function solve2(p, format, inline2, face) {
|
|
|
4354
4683
|
const noteH = noteHeight(p.note, noteWidth(format), 28, face);
|
|
4355
4684
|
const free = boxH - chromeH - 20 - noteH;
|
|
4356
4685
|
const avail = Math.max(300, free);
|
|
4357
|
-
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1,
|
|
4686
|
+
const room = (avail - 2 * pad - syMax - tMax) / Math.max(1, count2 - 1);
|
|
4358
4687
|
const rise = clamp(room, RISE_MIN, riseMax);
|
|
4359
4688
|
const sy = clamp(rise * 0.62, 46, syMax);
|
|
4360
4689
|
const t2 = clamp(rise * 0.14, 10, tMax);
|
|
4361
4690
|
const span = width - NUM_W - GAP3 - colW;
|
|
4362
4691
|
const sx = clamp(span * 0.2, 110, 220);
|
|
4363
|
-
const height = 2 * pad + (
|
|
4692
|
+
const height = 2 * pad + (count2 - 1) * rise + sy + t2;
|
|
4364
4693
|
return {
|
|
4365
4694
|
// A rise shorter than a label block prints the note through the label below.
|
|
4366
4695
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
@@ -4385,7 +4714,7 @@ function solve2(p, format, inline2, face) {
|
|
|
4385
4714
|
labelSize,
|
|
4386
4715
|
blockH,
|
|
4387
4716
|
pad,
|
|
4388
|
-
yBase: pad + (
|
|
4717
|
+
yBase: pad + (count2 - 1) * rise + sy,
|
|
4389
4718
|
lines
|
|
4390
4719
|
};
|
|
4391
4720
|
}
|
|
@@ -4402,11 +4731,11 @@ var stack = (beat, ctx) => {
|
|
|
4402
4731
|
const p = beat.params;
|
|
4403
4732
|
const face = faceOf(ctx.theme.fontStack);
|
|
4404
4733
|
const L = stackLayout(p, ctx.format, face);
|
|
4405
|
-
const
|
|
4406
|
-
const last =
|
|
4734
|
+
const count2 = p.layers.length;
|
|
4735
|
+
const last = count2 - 1;
|
|
4407
4736
|
if (!L.fits) {
|
|
4408
4737
|
throw new Error(
|
|
4409
|
-
`stack ${beat.id}: ${
|
|
4738
|
+
`stack ${beat.id}: ${count2} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${Math.round(L.floor)}px floor and with the notes already moved beside their labels. Nothing here may be set smaller, so the lever is upstream of this beat: drop a layer, or shorten the notes.`
|
|
4410
4739
|
);
|
|
4411
4740
|
}
|
|
4412
4741
|
const parts = {};
|
|
@@ -4434,7 +4763,7 @@ var stack = (beat, ctx) => {
|
|
|
4434
4763
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
4435
4764
|
{
|
|
4436
4765
|
size: L.labelSize,
|
|
4437
|
-
weight: labelWeight(i,
|
|
4766
|
+
weight: labelWeight(i, count2),
|
|
4438
4767
|
fill: top ? theme.tones.b : theme.fg,
|
|
4439
4768
|
maxWidth: block.labelMaxW,
|
|
4440
4769
|
lineHeight: 1.16,
|
|
@@ -4488,10 +4817,10 @@ var stack = (beat, ctx) => {
|
|
|
4488
4817
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4489
4818
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4490
4819
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe3)}</div>${noteHtml}`;
|
|
4491
|
-
const centre2 = (
|
|
4820
|
+
const centre2 = (count2 - 1) / 2;
|
|
4492
4821
|
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4493
4822
|
const first = 0.9;
|
|
4494
|
-
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) /
|
|
4823
|
+
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count2));
|
|
4495
4824
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
4496
4825
|
const holds = [];
|
|
4497
4826
|
const spot = spotlighter(sid);
|
|
@@ -4523,11 +4852,11 @@ var stack = (beat, ctx) => {
|
|
|
4523
4852
|
);
|
|
4524
4853
|
}
|
|
4525
4854
|
if (p.note) {
|
|
4526
|
-
const at = first +
|
|
4855
|
+
const at = first + count2 * step;
|
|
4527
4856
|
tl.push(tween(`#${sid}-note`, { opacity: 0 }, { opacity: 1, duration: 0.6 }, at));
|
|
4528
4857
|
holds.push(at + 0.7);
|
|
4529
4858
|
}
|
|
4530
|
-
if (
|
|
4859
|
+
if (count2 > 1) tl.push(...spot.restore(first + count2 * step));
|
|
4531
4860
|
return {
|
|
4532
4861
|
html,
|
|
4533
4862
|
parts,
|
|
@@ -5829,8 +6158,8 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5829
6158
|
const text2 = beat.narration?.trim();
|
|
5830
6159
|
if (!text2) continue;
|
|
5831
6160
|
const segments2 = [];
|
|
5832
|
-
const
|
|
5833
|
-
const plan = planSegments(text2, Math.min(
|
|
6161
|
+
const count2 = stopsFor(beat, source, format, `s${i + 1}`, theme);
|
|
6162
|
+
const plan = planSegments(text2, Math.min(count2, paced.speakingStops));
|
|
5834
6163
|
for (const [stop, line2] of plan.entries()) {
|
|
5835
6164
|
if (!line2) continue;
|
|
5836
6165
|
const speech = await synthesize(line2, {
|
|
@@ -5852,7 +6181,7 @@ async function narrate(storyboard, source, prefs, opts) {
|
|
|
5852
6181
|
}
|
|
5853
6182
|
if (segments2.length > 0) {
|
|
5854
6183
|
beats[beat.id] = segments2;
|
|
5855
|
-
staged[beat.id] =
|
|
6184
|
+
staged[beat.id] = count2;
|
|
5856
6185
|
}
|
|
5857
6186
|
}
|
|
5858
6187
|
return {
|
|
@@ -6722,11 +7051,11 @@ function stageScene(scene, speed) {
|
|
|
6722
7051
|
function speechPlan(open, holds, segments2) {
|
|
6723
7052
|
const starts = [];
|
|
6724
7053
|
let at = open;
|
|
6725
|
-
for (const [i,
|
|
6726
|
-
const hold = holds[Math.min(
|
|
7054
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7055
|
+
const hold = holds[Math.min(segment2.stop, holds.length - 1)] ?? 0;
|
|
6727
7056
|
if (i > 0) at = Math.max(at, hold);
|
|
6728
7057
|
starts.push(at);
|
|
6729
|
-
at +=
|
|
7058
|
+
at += segment2.seconds;
|
|
6730
7059
|
}
|
|
6731
7060
|
return { starts, end: at };
|
|
6732
7061
|
}
|
|
@@ -6951,10 +7280,10 @@ function pack(words2, width) {
|
|
|
6951
7280
|
function splitCue(cue, max = CUE_MAX_CHARS) {
|
|
6952
7281
|
if (cue.text.length <= max) return [cue];
|
|
6953
7282
|
const words2 = cue.text.split(/\s+/).filter(Boolean);
|
|
6954
|
-
const
|
|
7283
|
+
const count2 = pack(words2, max).length;
|
|
6955
7284
|
let width = max;
|
|
6956
|
-
for (let w = Math.ceil(cue.text.length /
|
|
6957
|
-
if (pack(words2, w).length <=
|
|
7285
|
+
for (let w = Math.ceil(cue.text.length / count2); w < max; w++) {
|
|
7286
|
+
if (pack(words2, w).length <= count2) {
|
|
6958
7287
|
width = w;
|
|
6959
7288
|
break;
|
|
6960
7289
|
}
|
|
@@ -7046,17 +7375,17 @@ function place(scenes, spoken) {
|
|
|
7046
7375
|
);
|
|
7047
7376
|
}
|
|
7048
7377
|
const { starts } = speechPlan(scene.open, scene.holds, segments2);
|
|
7049
|
-
for (const [i,
|
|
7050
|
-
const index = Math.min(
|
|
7378
|
+
for (const [i, segment2] of segments2.entries()) {
|
|
7379
|
+
const index = Math.min(segment2.stop, scene.holds.length - 1);
|
|
7051
7380
|
out.push({
|
|
7052
|
-
id: `${scene.id}.${
|
|
7381
|
+
id: `${scene.id}.${segment2.stop}`,
|
|
7053
7382
|
scene: scene.id,
|
|
7054
|
-
stop:
|
|
7055
|
-
audio:
|
|
7383
|
+
stop: segment2.stop,
|
|
7384
|
+
audio: segment2.audio,
|
|
7056
7385
|
hold: round5(scene.start + scene.holds[index]),
|
|
7057
7386
|
start: round5(scene.start + starts[i]),
|
|
7058
|
-
duration:
|
|
7059
|
-
cues:
|
|
7387
|
+
duration: segment2.seconds,
|
|
7388
|
+
cues: segment2.cues.flatMap((c) => splitCue(c, cueMax(c.text)))
|
|
7060
7389
|
});
|
|
7061
7390
|
}
|
|
7062
7391
|
}
|
|
@@ -7133,6 +7462,15 @@ function planTiming(input) {
|
|
|
7133
7462
|
// `emitDeck` and `writeTiming` are handed the same `format`, so this cannot
|
|
7134
7463
|
// disagree with the box the archetypes actually drew into.
|
|
7135
7464
|
captionReserve: format.captionReserve ?? 0,
|
|
7465
|
+
// An unbudgeted format's limit is Infinity, which JSON writes as `null`.
|
|
7466
|
+
// Left out instead: recorded as a format with no limit, never with a limit
|
|
7467
|
+
// of null or zero.
|
|
7468
|
+
format: {
|
|
7469
|
+
id: format.id,
|
|
7470
|
+
minWeight: format.minWeight,
|
|
7471
|
+
...Number.isFinite(format.maxSeconds) && { maxSeconds: format.maxSeconds },
|
|
7472
|
+
...format.warnSeconds !== void 0 && { warnSeconds: format.warnSeconds }
|
|
7473
|
+
},
|
|
7136
7474
|
scenes,
|
|
7137
7475
|
segments: segments2
|
|
7138
7476
|
};
|
|
@@ -7159,15 +7497,15 @@ function framePlan(timing, fps) {
|
|
|
7159
7497
|
);
|
|
7160
7498
|
}
|
|
7161
7499
|
pieces.push({ from: first, motion: last - first, freeze: 0 });
|
|
7162
|
-
for (const
|
|
7163
|
-
const at = f(
|
|
7500
|
+
for (const segment2 of segments2) {
|
|
7501
|
+
const at = f(segment2.start);
|
|
7164
7502
|
audio.push({
|
|
7165
|
-
id:
|
|
7166
|
-
audio:
|
|
7503
|
+
id: segment2.id,
|
|
7504
|
+
audio: segment2.audio,
|
|
7167
7505
|
startFrame: at,
|
|
7168
7506
|
delayMs: Math.round(at / fps * 1e3)
|
|
7169
7507
|
});
|
|
7170
|
-
for (const cue of
|
|
7508
|
+
for (const cue of segment2.cues) {
|
|
7171
7509
|
const start = at / fps + cue.start;
|
|
7172
7510
|
const end = at / fps + cue.end;
|
|
7173
7511
|
if (end > start) cues.push({ start, end, text: cue.text });
|
|
@@ -7732,6 +8070,22 @@ async function resolveChrome(need = "open the deck with") {
|
|
|
7732
8070
|
async function chromePath(need = "open the deck with") {
|
|
7733
8071
|
return (await resolveChrome(need)).path;
|
|
7734
8072
|
}
|
|
8073
|
+
function sandboxMissing(err) {
|
|
8074
|
+
return /No usable sandbox/.test(err instanceof Error ? err.message : String(err));
|
|
8075
|
+
}
|
|
8076
|
+
var SANDBOX_FIX = "`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`, or give Chrome an AppArmor profile";
|
|
8077
|
+
async function launchOwnPage(launch, args, what) {
|
|
8078
|
+
try {
|
|
8079
|
+
return await launch(args);
|
|
8080
|
+
} catch (err) {
|
|
8081
|
+
if (!sandboxMissing(err)) throw err;
|
|
8082
|
+
process.stderr.write(
|
|
8083
|
+
`chrome: this host gives Chrome no sandbox, so ${what} runs without one, as hyperframes' render always does. To keep the sandbox: ${SANDBOX_FIX}.
|
|
8084
|
+
`
|
|
8085
|
+
);
|
|
8086
|
+
return launch([...args, "--no-sandbox"]);
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
7735
8089
|
|
|
7736
8090
|
// src/source/assets.ts
|
|
7737
8091
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -8703,6 +9057,14 @@ async function readInBrowser(html, timeoutMs) {
|
|
|
8703
9057
|
// happens to be that run.
|
|
8704
9058
|
"--host-resolver-rules=MAP * ~NOTFOUND"
|
|
8705
9059
|
]
|
|
9060
|
+
// No `--no-sandbox` fallback here, unlike `launchOwnPage`: this is a page
|
|
9061
|
+
// from the web, and a renderer bug it triggers is exactly what the sandbox
|
|
9062
|
+
// contains. Refused, with the fix, rather than Chrome's raw FATAL.
|
|
9063
|
+
}).catch((err) => {
|
|
9064
|
+
if (!sandboxMissing(err)) throw err;
|
|
9065
|
+
throw new Error(
|
|
9066
|
+
`Chrome has no usable sandbox on this host, and a page from the web is not opened without one. ${SANDBOX_FIX}, then ingest again \u2014 or save the page and ingest the file.`
|
|
9067
|
+
);
|
|
8706
9068
|
});
|
|
8707
9069
|
try {
|
|
8708
9070
|
const page = await browser.newPage();
|
|
@@ -10059,8 +10421,13 @@ function renderSource(source) {
|
|
|
10059
10421
|
}
|
|
10060
10422
|
}
|
|
10061
10423
|
out.push("", "== EQUATIONS ==");
|
|
10062
|
-
for (const e of source.equations)
|
|
10063
|
-
|
|
10424
|
+
for (const e of source.equations) {
|
|
10425
|
+
const fixed = repairTex(e.tex, true);
|
|
10426
|
+
const note = fixed.error === null ? "" : " (KaTeX cannot draw this one: a walk shows it as plain source, a morph cannot use it)";
|
|
10427
|
+
out.push(
|
|
10428
|
+
`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${fixed.error === null ? fixed.tex : e.tex}${note}`
|
|
10429
|
+
);
|
|
10430
|
+
}
|
|
10064
10431
|
if (!source.equations.length)
|
|
10065
10432
|
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
10066
10433
|
out.push("", "== TABLES ==");
|
|
@@ -10592,10 +10959,10 @@ function drawSvg(req) {
|
|
|
10592
10959
|
const seed = createHash7("sha256").update([req.prompt, req.style, req.aspect].join("|")).digest();
|
|
10593
10960
|
const rand = mulberry32(seed.readUInt32BE(0));
|
|
10594
10961
|
const accent = ACCENTS[Math.floor(rand() * ACCENTS.length)];
|
|
10595
|
-
const
|
|
10962
|
+
const count2 = 6 + Math.floor(rand() * 5);
|
|
10596
10963
|
const unit = Math.min(W, H);
|
|
10597
10964
|
const parts = [`<rect width="${W}" height="${H}" fill="${PAPER}"/>`];
|
|
10598
|
-
for (let i = 0; i <
|
|
10965
|
+
for (let i = 0; i < count2; i++) {
|
|
10599
10966
|
const kind = Math.floor(rand() * 3);
|
|
10600
10967
|
const cx = Math.round(W * (0.15 + 0.7 * rand()));
|
|
10601
10968
|
const cy = Math.round(H * (0.15 + 0.7 * rand()));
|
|
@@ -10886,7 +11253,7 @@ ${bands}
|
|
|
10886
11253
|
</div>
|
|
10887
11254
|
`;
|
|
10888
11255
|
}
|
|
10889
|
-
function measure3(
|
|
11256
|
+
function measure3(count2) {
|
|
10890
11257
|
const doc = document;
|
|
10891
11258
|
const probe3 = doc.getElementById("c0");
|
|
10892
11259
|
if (probe3) {
|
|
@@ -10899,7 +11266,7 @@ function measure3(count) {
|
|
|
10899
11266
|
probe3.classList.remove("on");
|
|
10900
11267
|
}
|
|
10901
11268
|
const rects = [];
|
|
10902
|
-
for (let i = 0; i <
|
|
11269
|
+
for (let i = 0; i < count2; i++) {
|
|
10903
11270
|
const el = doc.getElementById(`c${i}`);
|
|
10904
11271
|
if (!el) continue;
|
|
10905
11272
|
el.classList.add("on");
|
|
@@ -10927,11 +11294,12 @@ async function renderCaptions(cues, style, deck, work2) {
|
|
|
10927
11294
|
await mkdir7(work2, { recursive: true });
|
|
10928
11295
|
await writeFile9(page, captionPage(cues, style, href));
|
|
10929
11296
|
const { default: puppeteer } = await import("puppeteer-core");
|
|
10930
|
-
const
|
|
10931
|
-
|
|
10932
|
-
headless: true,
|
|
10933
|
-
|
|
10934
|
-
|
|
11297
|
+
const executablePath = await chromePath(CAPTION_NEED);
|
|
11298
|
+
const browser = await launchOwnPage(
|
|
11299
|
+
(args) => puppeteer.launch({ executablePath, headless: true, args }),
|
|
11300
|
+
["--force-device-scale-factor=1", "--hide-scrollbars"],
|
|
11301
|
+
"the caption band"
|
|
11302
|
+
);
|
|
10935
11303
|
try {
|
|
10936
11304
|
const tab = await browser.newPage();
|
|
10937
11305
|
await tab.setViewport({ width: style.width, height: style.height, deviceScaleFactor: 1 });
|
|
@@ -11356,10 +11724,10 @@ function safeEntryPath(name) {
|
|
|
11356
11724
|
const segments2 = name.replace(/\\/g, "/").split("/");
|
|
11357
11725
|
if (/^[a-zA-Z]:/.test(name) || name.startsWith("/") || name.startsWith("\\")) return null;
|
|
11358
11726
|
const kept = [];
|
|
11359
|
-
for (const
|
|
11360
|
-
if (
|
|
11361
|
-
if (
|
|
11362
|
-
kept.push(
|
|
11727
|
+
for (const segment2 of segments2) {
|
|
11728
|
+
if (segment2 === "" || segment2 === ".") continue;
|
|
11729
|
+
if (segment2 === "..") return null;
|
|
11730
|
+
kept.push(segment2);
|
|
11363
11731
|
}
|
|
11364
11732
|
if (kept.length === 0) return null;
|
|
11365
11733
|
const path2 = kept.join("/");
|