@jokerized/decksmith 0.9.2 → 0.9.3
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/dist/cli.js +136 -52
- package/dist/index.js +136 -52
- package/dist/mcp.js +132 -49
- package/dist/types/source/fonts.d.ts +22 -0
- package/dist/types/verify/index.d.ts +10 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1814,7 +1814,11 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1814
1814
|
await mkdir3(dir, { recursive: true });
|
|
1815
1815
|
const cssPath = join3(dir, "fonts.css");
|
|
1816
1816
|
const cached2 = await readFile3(cssPath, "utf8").catch(() => "");
|
|
1817
|
-
if (cached2.startsWith(stamp))
|
|
1817
|
+
if (cached2.startsWith(stamp)) {
|
|
1818
|
+
const css2 = withInterMetrics(cached2);
|
|
1819
|
+
if (css2 !== cached2) await writeFile3(cssPath, css2);
|
|
1820
|
+
return { family, css: css2, files: localNames(css2) };
|
|
1821
|
+
}
|
|
1818
1822
|
const res = await fetch(
|
|
1819
1823
|
`https://fonts.googleapis.com/css2?${families.map((f) => `family=${f.replaceAll(" ", "+")}:wght@400;500;700`).join("&")}&text=${encodeURIComponent(text2)}&display=block`,
|
|
1820
1824
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1832,11 +1836,19 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1832
1836
|
css = css.replaceAll(url2, name);
|
|
1833
1837
|
files.push(name);
|
|
1834
1838
|
}
|
|
1835
|
-
css = `${stamp}
|
|
1836
|
-
${css}
|
|
1839
|
+
css = withInterMetrics(`${stamp}
|
|
1840
|
+
${css}`);
|
|
1837
1841
|
await writeFile3(cssPath, css);
|
|
1838
1842
|
return { family, css, files };
|
|
1839
1843
|
}
|
|
1844
|
+
var INTER_METRICS = "ascent-override: 96.875%;\n descent-override: 24.1211%;\n line-gap-override: 0%";
|
|
1845
|
+
function withInterMetrics(css) {
|
|
1846
|
+
return css.replace(
|
|
1847
|
+
/@font-face\s*\{(?![^}]*ascent-override)/g,
|
|
1848
|
+
`@font-face {
|
|
1849
|
+
${INTER_METRICS};`
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1840
1852
|
function localNames(css) {
|
|
1841
1853
|
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
1842
1854
|
}
|
|
@@ -6832,7 +6844,6 @@ var READOUT_W = 460;
|
|
|
6832
6844
|
var READOUT_LH = 1.35;
|
|
6833
6845
|
var TALL_ASPECT = 1.15;
|
|
6834
6846
|
var RESHAPE_SECONDS = 1.2;
|
|
6835
|
-
var GHOST_OPACITY = 0.28;
|
|
6836
6847
|
var DRAW_SECONDS = 1.8;
|
|
6837
6848
|
var DRAW_FLOOR = 1;
|
|
6838
6849
|
var SEPARATE = 0.5;
|
|
@@ -6909,6 +6920,14 @@ var lineChart = (beat2, ctx) => {
|
|
|
6909
6920
|
return new Set(kept);
|
|
6910
6921
|
};
|
|
6911
6922
|
const LABEL_SIZE3 = 40;
|
|
6923
|
+
const RUN_ABOVE = Math.ceil(LABEL_SIZE3 * 0.969);
|
|
6924
|
+
const RUN_BELOW = Math.ceil(LABEL_SIZE3 * 0.242);
|
|
6925
|
+
const runBox = (left, by, w) => ({
|
|
6926
|
+
x: left,
|
|
6927
|
+
y: by - RUN_ABOVE,
|
|
6928
|
+
w,
|
|
6929
|
+
h: RUN_ABOVE + RUN_BELOW
|
|
6930
|
+
});
|
|
6912
6931
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
6913
6932
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
6914
6933
|
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
@@ -6923,17 +6942,47 @@ var lineChart = (beat2, ctx) => {
|
|
|
6923
6942
|
const dots = p.points.map(
|
|
6924
6943
|
(pt, i) => `<circle class="dot" cx="${n2(x(i))}" cy="${n2(y(pt.y))}" r="${i === p.points.length - 1 ? 11 : 9}" fill="${i === p.points.length - 1 ? theme.tones.b : theme.accent}" />`
|
|
6925
6944
|
).join("");
|
|
6945
|
+
const labelBox = (cx, cy, text2, anchorStart = false) => {
|
|
6946
|
+
const w = runW(text2);
|
|
6947
|
+
return runBox(anchorStart ? cx : cx - w / 2, cy, w);
|
|
6948
|
+
};
|
|
6949
|
+
const curveBand = (pts, x0, x1) => {
|
|
6950
|
+
const ys = [];
|
|
6951
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
6952
|
+
for (let i = 0; i < pts.length; i++) {
|
|
6953
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
6954
|
+
}
|
|
6955
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
6956
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
6957
|
+
for (const edge of [x0, x1]) {
|
|
6958
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
6959
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
6964
|
+
};
|
|
6965
|
+
const drawnThrough = (series, b) => {
|
|
6966
|
+
const [top, bottom] = curveBand(series, b.x, b.x + b.w);
|
|
6967
|
+
return Math.min(b.y + b.h, bottom) - Math.max(b.y, top) > 8;
|
|
6968
|
+
};
|
|
6969
|
+
const valueBy = (i) => {
|
|
6970
|
+
const v = p.points[i]?.y ?? 0;
|
|
6971
|
+
const over = Math.max(44, y(v) - 26);
|
|
6972
|
+
if (!cmp) return over;
|
|
6973
|
+
const box2 = (by) => labelBox(x(i), by, String(v), i === 0);
|
|
6974
|
+
if (!drawnThrough(cmp.points, box2(over))) return over;
|
|
6975
|
+
const under = y(v) + 16 + RUN_ABOVE;
|
|
6976
|
+
if (under > H - PAD2.b + 4) return over;
|
|
6977
|
+
return drawnThrough(cmp.points, box2(under)) || drawnThrough(p.points, box2(under)) ? over : under;
|
|
6978
|
+
};
|
|
6926
6979
|
const shownValues = fitIndices((i) => runW(String(p.points[i]?.y ?? "")), true);
|
|
6927
6980
|
const showsValue = (i) => shownValues.has(i);
|
|
6928
6981
|
const values = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => {
|
|
6929
6982
|
const anchor = i === 0 ? ' text-anchor="start"' : "";
|
|
6930
|
-
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(
|
|
6983
|
+
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(valueBy(i))}">${esc(String(pt.y))}</text>`;
|
|
6931
6984
|
}).join("");
|
|
6932
|
-
const
|
|
6933
|
-
const w = runW(text2);
|
|
6934
|
-
return { x: anchorStart ? cx : cx - w / 2, y: cy - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
6935
|
-
};
|
|
6936
|
-
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), Math.max(44, y(pt.y) - 26), String(pt.y), i === 0));
|
|
6985
|
+
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), valueBy(i), String(pt.y), i === 0));
|
|
6937
6986
|
const overlaps = (a, b) => Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 8 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 8;
|
|
6938
6987
|
const deltaBoxes = (p.deltas ?? []).slice(0, p.points.length - 1).map((d, i) => {
|
|
6939
6988
|
const a = p.points[i];
|
|
@@ -6961,40 +7010,22 @@ var lineChart = (beat2, ctx) => {
|
|
|
6961
7010
|
lineTag("line", basePath, ' opacity="0"'),
|
|
6962
7011
|
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
6963
7012
|
].join("\n ") : lineTag("line", path2);
|
|
6964
|
-
const curveBand = (pts, x0, x1) => {
|
|
6965
|
-
const ys = [];
|
|
6966
|
-
const py = (i) => y(pts[i]?.y ?? 0);
|
|
6967
|
-
for (let i = 0; i < pts.length; i++) {
|
|
6968
|
-
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
6969
|
-
}
|
|
6970
|
-
for (let i = 0; i + 1 < pts.length; i++) {
|
|
6971
|
-
const [ax, bx] = [x(i), x(i + 1)];
|
|
6972
|
-
for (const edge of [x0, x1]) {
|
|
6973
|
-
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
6974
|
-
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
6975
|
-
}
|
|
6976
|
-
}
|
|
6977
|
-
}
|
|
6978
|
-
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
6979
|
-
};
|
|
6980
7013
|
const fixedBoxes = [
|
|
6981
7014
|
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
6982
7015
|
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
6983
7016
|
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
6987
|
-
y: H - 16 - LABEL_SIZE3,
|
|
6988
|
-
w: nameW(p.xLabel),
|
|
6989
|
-
h: LABEL_SIZE3
|
|
6990
|
-
},
|
|
7017
|
+
runBox(0, 42, nameW(p.yLabel)),
|
|
7018
|
+
runBox(PAD2.l + plotW / 2 - nameW(p.xLabel) / 2, H - 16, nameW(p.xLabel)),
|
|
6991
7019
|
...ticks.map((v) => {
|
|
6992
7020
|
const w = runW(v.toFixed(scale.decimals));
|
|
6993
|
-
return
|
|
7021
|
+
return runBox(PAD2.l - 22 - w, y(v) + 13, w);
|
|
6994
7022
|
}),
|
|
6995
7023
|
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
6996
7024
|
];
|
|
6997
7025
|
const lastI = p.points.length - 1;
|
|
7026
|
+
const LINE = RUN_ABOVE + RUN_BELOW;
|
|
7027
|
+
const SWATCH = 48;
|
|
7028
|
+
const SWATCH_GAP = 16;
|
|
6998
7029
|
const ghost = (() => {
|
|
6999
7030
|
if (!cmp) return void 0;
|
|
7000
7031
|
const w = ghostW(cmp.label);
|
|
@@ -7003,6 +7034,18 @@ var lineChart = (beat2, ctx) => {
|
|
|
7003
7034
|
`line-chart ${beat2.id}: the compare label "${cmp.label}" is ${Math.ceil(w)}px against the ${Math.floor(plotW)}px of plot it is set in. Shorten it.`
|
|
7004
7035
|
);
|
|
7005
7036
|
}
|
|
7037
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
7038
|
+
const gapTo = (series, b) => {
|
|
7039
|
+
const [top, bottom] = curveBand(series, b.x, b.x + b.w);
|
|
7040
|
+
return Math.max(0, top - (b.y + b.h), b.y - bottom);
|
|
7041
|
+
};
|
|
7042
|
+
const clearOfText = (b) => !fixedBoxes.some((f) => overlaps(b, f)) && !valueBoxes.some((v) => overlaps(b, v)) && !drawnDeltas.some((d) => d !== null && overlaps(b, d));
|
|
7043
|
+
const stacked = (b) => valueBoxes.some(
|
|
7044
|
+
(v) => Math.min(b.x + b.w, v.x + v.w) - Math.max(b.x, v.x) > 0 && Math.max(v.y - (b.y + b.h), b.y - (v.y + v.h)) < LINE
|
|
7045
|
+
);
|
|
7046
|
+
const crossed = (b) => [p.points, cmp.points].some((series) => drawnThrough(series, b));
|
|
7047
|
+
const owned = (b) => gapTo(p.points, b) - gapTo(cmp.points, b) >= LINE;
|
|
7048
|
+
const honest = (b) => clearOfText(b) && !stacked(b) && !crossed(b) && owned(b);
|
|
7006
7049
|
const atEnd = (i, start) => {
|
|
7007
7050
|
const cy = y(cmp.points[i]?.y ?? 0);
|
|
7008
7051
|
const away = Math.max(44, cy - 26);
|
|
@@ -7010,20 +7053,42 @@ var lineChart = (beat2, ctx) => {
|
|
|
7010
7053
|
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
7011
7054
|
return sides.map((by) => ({ i, start, by }));
|
|
7012
7055
|
};
|
|
7013
|
-
const
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7056
|
+
const end2 = [...atEnd(lastI, false), ...atEnd(0, true)].find(
|
|
7057
|
+
(c) => honest(runBox(c.start ? x(c.i) : x(c.i) - w, c.by, w))
|
|
7058
|
+
);
|
|
7059
|
+
if (end2) {
|
|
7060
|
+
return { kind: "label", anchor: end2.start ? "start" : "end", x: x(end2.i), by: end2.by };
|
|
7061
|
+
}
|
|
7062
|
+
const yAt = (series, at2) => curveBand(series, at2, at2)[0];
|
|
7063
|
+
const sep4 = (at2) => yAt(p.points, at2) - yAt(cmp.points, at2);
|
|
7064
|
+
const centres = p.points.flatMap(
|
|
7065
|
+
(_, i) => i < lastI ? [x(i), (x(i) + x(i + 1)) / 2] : [x(i)]
|
|
7066
|
+
);
|
|
7067
|
+
const along = centres.map((cx) => {
|
|
7068
|
+
const left = Math.min(Math.max(cx - w / 2, PAD2.l), width - padR - w);
|
|
7069
|
+
const side = Math.sign(sep4(left + w / 2));
|
|
7070
|
+
const cols = [
|
|
7071
|
+
left,
|
|
7072
|
+
left + w,
|
|
7073
|
+
...p.points.map((_, i) => x(i)).filter((v) => v > left && v < left + w)
|
|
7074
|
+
];
|
|
7075
|
+
const narrowest = Math.min(...cols.map((c) => side * sep4(c)));
|
|
7076
|
+
const [top, bottom] = curveBand(cmp.points, left, left + w);
|
|
7077
|
+
const by = side > 0 ? top - 16 - RUN_BELOW : bottom + 16 + RUN_ABOVE;
|
|
7078
|
+
return { left, by, narrowest, side };
|
|
7079
|
+
}).filter((c) => c.side !== 0 && c.by >= 44 && c.by <= H - PAD2.b + 4).sort((a, b) => b.narrowest - a.narrowest).find((c) => honest(runBox(c.left, c.by, w)));
|
|
7080
|
+
if (along) return { kind: "label", anchor: "middle", x: along.left + w / 2, by: along.by };
|
|
7081
|
+
const kw = SWATCH + SWATCH_GAP + w;
|
|
7082
|
+
const right = width - padR;
|
|
7083
|
+
const key = [42, H - 16].find((by) => {
|
|
7084
|
+
const b = runBox(right - kw, by, kw);
|
|
7085
|
+
return clearOfText(b) && !stacked(b) && !crossed(b);
|
|
7023
7086
|
});
|
|
7087
|
+
return key === void 0 ? void 0 : { kind: "key", left: right - kw, right, by: key };
|
|
7024
7088
|
})();
|
|
7025
|
-
const ghostLabel = cmp
|
|
7026
|
-
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.
|
|
7089
|
+
const ghostLabel = !cmp || !ghost ? "" : ghost.kind === "label" ? `
|
|
7090
|
+
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.anchor}" x="${n2(ghost.x)}" y="${n2(ghost.by)}">${esc(cmp.label)}</text>` : `
|
|
7091
|
+
<g class="ghostkey" id="${sid}-ghostlab"><line x1="${n2(ghost.left)}" y1="${n2(ghost.by - 14)}" x2="${n2(ghost.left + SWATCH)}" y2="${n2(ghost.by - 14)}" stroke="${theme.dim}" stroke-width="5" stroke-linecap="round" /><text class="ghostlab" text-anchor="end" x="${n2(ghost.right)}" y="${n2(ghost.by)}">${esc(cmp.label)}</text></g>`;
|
|
7027
7092
|
const readout = p.readout ? `
|
|
7028
7093
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
7029
7094
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -7085,7 +7150,12 @@ var lineChart = (beat2, ctx) => {
|
|
|
7085
7150
|
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
7086
7151
|
// separating from itself rather than as a second line arriving.
|
|
7087
7152
|
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
7088
|
-
tween(
|
|
7153
|
+
tween(
|
|
7154
|
+
`#${sid}-base`,
|
|
7155
|
+
{ stroke: theme.accent },
|
|
7156
|
+
{ stroke: theme.dim, duration: SEPARATE },
|
|
7157
|
+
lift2
|
|
7158
|
+
),
|
|
7089
7159
|
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
7090
7160
|
);
|
|
7091
7161
|
}
|
|
@@ -11628,8 +11698,11 @@ function closeSafe(code) {
|
|
|
11628
11698
|
}
|
|
11629
11699
|
function sceneHtml(sid, start, clip3, label, scene, transit) {
|
|
11630
11700
|
const setup = [...mathSetup(sid, scene.html), ...scene.setup ?? []].map(statement).join("\n ");
|
|
11701
|
+
const measure4 = (scene.measure ?? []).map(statement);
|
|
11702
|
+
const laps = lapSetup(sid, setup + measure4.join(""));
|
|
11631
11703
|
const build = (pad) => [
|
|
11632
|
-
...
|
|
11704
|
+
...measure4,
|
|
11705
|
+
...laps,
|
|
11633
11706
|
`window.__timelines = window.__timelines || {};`,
|
|
11634
11707
|
`// Each scene owns a paused timeline under its own composition id, with`,
|
|
11635
11708
|
`// times relative to its start \u2014 one absolute root timeline yields a`,
|
|
@@ -11674,6 +11747,15 @@ function mathSetup(sid, html) {
|
|
|
11674
11747
|
})`
|
|
11675
11748
|
];
|
|
11676
11749
|
}
|
|
11750
|
+
function lapSetup(sid, script) {
|
|
11751
|
+
if (!script.includes("katex.render")) return [];
|
|
11752
|
+
const laps = [".rlap", ".llap", ".clap"].map((c) => `#${sid} .katex ${c} *`).join(", ");
|
|
11753
|
+
return [
|
|
11754
|
+
statement(`Array.prototype.forEach.call(document.querySelectorAll('${laps}'), function (el) {
|
|
11755
|
+
el.setAttribute("data-layout-allow-overlap", "");
|
|
11756
|
+
})`)
|
|
11757
|
+
];
|
|
11758
|
+
}
|
|
11677
11759
|
function statement(s) {
|
|
11678
11760
|
const line2 = s.trim();
|
|
11679
11761
|
return line2.endsWith(";") ? line2 : `${line2};`;
|
|
@@ -18016,7 +18098,8 @@ var FONTSOURCE = {
|
|
|
18016
18098
|
async function vendorFace(face, dir) {
|
|
18017
18099
|
return vendorFontsource(FONTSOURCE[face], familyOf(face), dir);
|
|
18018
18100
|
}
|
|
18019
|
-
var PACK_DISPLAY =
|
|
18101
|
+
var PACK_DISPLAY = `font-display: block;
|
|
18102
|
+
${INTER_METRICS}`;
|
|
18020
18103
|
async function vendorFontsource(pkgName, family, dir) {
|
|
18021
18104
|
const pkg = dirname(createRequire2(import.meta.url).resolve(pkgName));
|
|
18022
18105
|
const css = await readFile11(join11(pkg, "index.css"), "utf8");
|
|
@@ -21697,12 +21780,13 @@ function scanPaperArc(storyboard, prefs) {
|
|
|
21697
21780
|
}));
|
|
21698
21781
|
}
|
|
21699
21782
|
function scanNarrationDrift(storyboard, narration) {
|
|
21700
|
-
const
|
|
21783
|
+
const sentences = (s) => splitSentences((s ?? "").replace(/\s+/g, " ").trim()).join("\n");
|
|
21701
21784
|
const stale = [];
|
|
21702
21785
|
for (const beat2 of storyboard.beats) {
|
|
21703
21786
|
const segments2 = narration.beats[beat2.id];
|
|
21704
|
-
|
|
21705
|
-
if (
|
|
21787
|
+
const said = sentences(beat2.narration);
|
|
21788
|
+
if (!segments2?.length || !said) continue;
|
|
21789
|
+
if (sentences(segments2.map((s) => s.text).join(" ")) !== said) stale.push(beat2.id);
|
|
21706
21790
|
}
|
|
21707
21791
|
if (!stale.length) return [];
|
|
21708
21792
|
return [
|
package/dist/index.js
CHANGED
|
@@ -1811,7 +1811,11 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1811
1811
|
await mkdir3(dir, { recursive: true });
|
|
1812
1812
|
const cssPath = join3(dir, "fonts.css");
|
|
1813
1813
|
const cached2 = await readFile3(cssPath, "utf8").catch(() => "");
|
|
1814
|
-
if (cached2.startsWith(stamp))
|
|
1814
|
+
if (cached2.startsWith(stamp)) {
|
|
1815
|
+
const css2 = withInterMetrics(cached2);
|
|
1816
|
+
if (css2 !== cached2) await writeFile3(cssPath, css2);
|
|
1817
|
+
return { family, css: css2, files: localNames(css2) };
|
|
1818
|
+
}
|
|
1815
1819
|
const res = await fetch(
|
|
1816
1820
|
`https://fonts.googleapis.com/css2?${families.map((f) => `family=${f.replaceAll(" ", "+")}:wght@400;500;700`).join("&")}&text=${encodeURIComponent(text2)}&display=block`,
|
|
1817
1821
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1829,11 +1833,19 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1829
1833
|
css = css.replaceAll(url, name);
|
|
1830
1834
|
files.push(name);
|
|
1831
1835
|
}
|
|
1832
|
-
css = `${stamp}
|
|
1833
|
-
${css}
|
|
1836
|
+
css = withInterMetrics(`${stamp}
|
|
1837
|
+
${css}`);
|
|
1834
1838
|
await writeFile3(cssPath, css);
|
|
1835
1839
|
return { family, css, files };
|
|
1836
1840
|
}
|
|
1841
|
+
var INTER_METRICS = "ascent-override: 96.875%;\n descent-override: 24.1211%;\n line-gap-override: 0%";
|
|
1842
|
+
function withInterMetrics(css) {
|
|
1843
|
+
return css.replace(
|
|
1844
|
+
/@font-face\s*\{(?![^}]*ascent-override)/g,
|
|
1845
|
+
`@font-face {
|
|
1846
|
+
${INTER_METRICS};`
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1837
1849
|
function localNames(css) {
|
|
1838
1850
|
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
1839
1851
|
}
|
|
@@ -2321,7 +2333,8 @@ var FONTSOURCE = {
|
|
|
2321
2333
|
async function vendorFace(face, dir) {
|
|
2322
2334
|
return vendorFontsource(FONTSOURCE[face], familyOf(face), dir);
|
|
2323
2335
|
}
|
|
2324
|
-
var PACK_DISPLAY =
|
|
2336
|
+
var PACK_DISPLAY = `font-display: block;
|
|
2337
|
+
${INTER_METRICS}`;
|
|
2325
2338
|
async function vendorFontsource(pkgName, family, dir) {
|
|
2326
2339
|
const pkg = dirname(createRequire(import.meta.url).resolve(pkgName));
|
|
2327
2340
|
const css = await readFile4(join4(pkg, "index.css"), "utf8");
|
|
@@ -6992,7 +7005,6 @@ var READOUT_W = 460;
|
|
|
6992
7005
|
var READOUT_LH = 1.35;
|
|
6993
7006
|
var TALL_ASPECT = 1.15;
|
|
6994
7007
|
var RESHAPE_SECONDS = 1.2;
|
|
6995
|
-
var GHOST_OPACITY = 0.28;
|
|
6996
7008
|
var DRAW_SECONDS = 1.8;
|
|
6997
7009
|
var DRAW_FLOOR = 1;
|
|
6998
7010
|
var SEPARATE = 0.5;
|
|
@@ -7069,6 +7081,14 @@ var lineChart = (beat2, ctx) => {
|
|
|
7069
7081
|
return new Set(kept);
|
|
7070
7082
|
};
|
|
7071
7083
|
const LABEL_SIZE3 = 40;
|
|
7084
|
+
const RUN_ABOVE = Math.ceil(LABEL_SIZE3 * 0.969);
|
|
7085
|
+
const RUN_BELOW = Math.ceil(LABEL_SIZE3 * 0.242);
|
|
7086
|
+
const runBox = (left, by, w) => ({
|
|
7087
|
+
x: left,
|
|
7088
|
+
y: by - RUN_ABOVE,
|
|
7089
|
+
w,
|
|
7090
|
+
h: RUN_ABOVE + RUN_BELOW
|
|
7091
|
+
});
|
|
7072
7092
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
7073
7093
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
7074
7094
|
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
@@ -7083,17 +7103,47 @@ var lineChart = (beat2, ctx) => {
|
|
|
7083
7103
|
const dots = p.points.map(
|
|
7084
7104
|
(pt, i) => `<circle class="dot" cx="${n2(x(i))}" cy="${n2(y(pt.y))}" r="${i === p.points.length - 1 ? 11 : 9}" fill="${i === p.points.length - 1 ? theme.tones.b : theme.accent}" />`
|
|
7085
7105
|
).join("");
|
|
7106
|
+
const labelBox = (cx, cy, text2, anchorStart = false) => {
|
|
7107
|
+
const w = runW(text2);
|
|
7108
|
+
return runBox(anchorStart ? cx : cx - w / 2, cy, w);
|
|
7109
|
+
};
|
|
7110
|
+
const curveBand = (pts, x0, x1) => {
|
|
7111
|
+
const ys = [];
|
|
7112
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
7113
|
+
for (let i = 0; i < pts.length; i++) {
|
|
7114
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
7115
|
+
}
|
|
7116
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
7117
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
7118
|
+
for (const edge of [x0, x1]) {
|
|
7119
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
7120
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
7125
|
+
};
|
|
7126
|
+
const drawnThrough = (series, b) => {
|
|
7127
|
+
const [top, bottom] = curveBand(series, b.x, b.x + b.w);
|
|
7128
|
+
return Math.min(b.y + b.h, bottom) - Math.max(b.y, top) > 8;
|
|
7129
|
+
};
|
|
7130
|
+
const valueBy = (i) => {
|
|
7131
|
+
const v = p.points[i]?.y ?? 0;
|
|
7132
|
+
const over = Math.max(44, y(v) - 26);
|
|
7133
|
+
if (!cmp) return over;
|
|
7134
|
+
const box2 = (by) => labelBox(x(i), by, String(v), i === 0);
|
|
7135
|
+
if (!drawnThrough(cmp.points, box2(over))) return over;
|
|
7136
|
+
const under = y(v) + 16 + RUN_ABOVE;
|
|
7137
|
+
if (under > H - PAD2.b + 4) return over;
|
|
7138
|
+
return drawnThrough(cmp.points, box2(under)) || drawnThrough(p.points, box2(under)) ? over : under;
|
|
7139
|
+
};
|
|
7086
7140
|
const shownValues = fitIndices((i) => runW(String(p.points[i]?.y ?? "")), true);
|
|
7087
7141
|
const showsValue = (i) => shownValues.has(i);
|
|
7088
7142
|
const values = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => {
|
|
7089
7143
|
const anchor = i === 0 ? ' text-anchor="start"' : "";
|
|
7090
|
-
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(
|
|
7144
|
+
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(valueBy(i))}">${esc(String(pt.y))}</text>`;
|
|
7091
7145
|
}).join("");
|
|
7092
|
-
const
|
|
7093
|
-
const w = runW(text2);
|
|
7094
|
-
return { x: anchorStart ? cx : cx - w / 2, y: cy - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
7095
|
-
};
|
|
7096
|
-
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), Math.max(44, y(pt.y) - 26), String(pt.y), i === 0));
|
|
7146
|
+
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), valueBy(i), String(pt.y), i === 0));
|
|
7097
7147
|
const overlaps = (a, b) => Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 8 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 8;
|
|
7098
7148
|
const deltaBoxes = (p.deltas ?? []).slice(0, p.points.length - 1).map((d, i) => {
|
|
7099
7149
|
const a = p.points[i];
|
|
@@ -7121,40 +7171,22 @@ var lineChart = (beat2, ctx) => {
|
|
|
7121
7171
|
lineTag("line", basePath, ' opacity="0"'),
|
|
7122
7172
|
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
7123
7173
|
].join("\n ") : lineTag("line", path2);
|
|
7124
|
-
const curveBand = (pts, x0, x1) => {
|
|
7125
|
-
const ys = [];
|
|
7126
|
-
const py = (i) => y(pts[i]?.y ?? 0);
|
|
7127
|
-
for (let i = 0; i < pts.length; i++) {
|
|
7128
|
-
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
7129
|
-
}
|
|
7130
|
-
for (let i = 0; i + 1 < pts.length; i++) {
|
|
7131
|
-
const [ax, bx] = [x(i), x(i + 1)];
|
|
7132
|
-
for (const edge of [x0, x1]) {
|
|
7133
|
-
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
7134
|
-
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
7135
|
-
}
|
|
7136
|
-
}
|
|
7137
|
-
}
|
|
7138
|
-
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
7139
|
-
};
|
|
7140
7174
|
const fixedBoxes = [
|
|
7141
7175
|
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
7142
7176
|
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
7143
7177
|
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
7147
|
-
y: H - 16 - LABEL_SIZE3,
|
|
7148
|
-
w: nameW(p.xLabel),
|
|
7149
|
-
h: LABEL_SIZE3
|
|
7150
|
-
},
|
|
7178
|
+
runBox(0, 42, nameW(p.yLabel)),
|
|
7179
|
+
runBox(PAD2.l + plotW / 2 - nameW(p.xLabel) / 2, H - 16, nameW(p.xLabel)),
|
|
7151
7180
|
...ticks.map((v) => {
|
|
7152
7181
|
const w = runW(v.toFixed(scale.decimals));
|
|
7153
|
-
return
|
|
7182
|
+
return runBox(PAD2.l - 22 - w, y(v) + 13, w);
|
|
7154
7183
|
}),
|
|
7155
7184
|
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
7156
7185
|
];
|
|
7157
7186
|
const lastI = p.points.length - 1;
|
|
7187
|
+
const LINE = RUN_ABOVE + RUN_BELOW;
|
|
7188
|
+
const SWATCH = 48;
|
|
7189
|
+
const SWATCH_GAP = 16;
|
|
7158
7190
|
const ghost = (() => {
|
|
7159
7191
|
if (!cmp) return void 0;
|
|
7160
7192
|
const w = ghostW(cmp.label);
|
|
@@ -7163,6 +7195,18 @@ var lineChart = (beat2, ctx) => {
|
|
|
7163
7195
|
`line-chart ${beat2.id}: the compare label "${cmp.label}" is ${Math.ceil(w)}px against the ${Math.floor(plotW)}px of plot it is set in. Shorten it.`
|
|
7164
7196
|
);
|
|
7165
7197
|
}
|
|
7198
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
7199
|
+
const gapTo = (series, b) => {
|
|
7200
|
+
const [top, bottom] = curveBand(series, b.x, b.x + b.w);
|
|
7201
|
+
return Math.max(0, top - (b.y + b.h), b.y - bottom);
|
|
7202
|
+
};
|
|
7203
|
+
const clearOfText = (b) => !fixedBoxes.some((f) => overlaps(b, f)) && !valueBoxes.some((v) => overlaps(b, v)) && !drawnDeltas.some((d) => d !== null && overlaps(b, d));
|
|
7204
|
+
const stacked = (b) => valueBoxes.some(
|
|
7205
|
+
(v) => Math.min(b.x + b.w, v.x + v.w) - Math.max(b.x, v.x) > 0 && Math.max(v.y - (b.y + b.h), b.y - (v.y + v.h)) < LINE
|
|
7206
|
+
);
|
|
7207
|
+
const crossed = (b) => [p.points, cmp.points].some((series) => drawnThrough(series, b));
|
|
7208
|
+
const owned = (b) => gapTo(p.points, b) - gapTo(cmp.points, b) >= LINE;
|
|
7209
|
+
const honest = (b) => clearOfText(b) && !stacked(b) && !crossed(b) && owned(b);
|
|
7166
7210
|
const atEnd = (i, start) => {
|
|
7167
7211
|
const cy = y(cmp.points[i]?.y ?? 0);
|
|
7168
7212
|
const away = Math.max(44, cy - 26);
|
|
@@ -7170,20 +7214,42 @@ var lineChart = (beat2, ctx) => {
|
|
|
7170
7214
|
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
7171
7215
|
return sides.map((by) => ({ i, start, by }));
|
|
7172
7216
|
};
|
|
7173
|
-
const
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7217
|
+
const end2 = [...atEnd(lastI, false), ...atEnd(0, true)].find(
|
|
7218
|
+
(c) => honest(runBox(c.start ? x(c.i) : x(c.i) - w, c.by, w))
|
|
7219
|
+
);
|
|
7220
|
+
if (end2) {
|
|
7221
|
+
return { kind: "label", anchor: end2.start ? "start" : "end", x: x(end2.i), by: end2.by };
|
|
7222
|
+
}
|
|
7223
|
+
const yAt = (series, at2) => curveBand(series, at2, at2)[0];
|
|
7224
|
+
const sep4 = (at2) => yAt(p.points, at2) - yAt(cmp.points, at2);
|
|
7225
|
+
const centres = p.points.flatMap(
|
|
7226
|
+
(_, i) => i < lastI ? [x(i), (x(i) + x(i + 1)) / 2] : [x(i)]
|
|
7227
|
+
);
|
|
7228
|
+
const along = centres.map((cx) => {
|
|
7229
|
+
const left = Math.min(Math.max(cx - w / 2, PAD2.l), width - padR - w);
|
|
7230
|
+
const side = Math.sign(sep4(left + w / 2));
|
|
7231
|
+
const cols = [
|
|
7232
|
+
left,
|
|
7233
|
+
left + w,
|
|
7234
|
+
...p.points.map((_, i) => x(i)).filter((v) => v > left && v < left + w)
|
|
7235
|
+
];
|
|
7236
|
+
const narrowest = Math.min(...cols.map((c) => side * sep4(c)));
|
|
7237
|
+
const [top, bottom] = curveBand(cmp.points, left, left + w);
|
|
7238
|
+
const by = side > 0 ? top - 16 - RUN_BELOW : bottom + 16 + RUN_ABOVE;
|
|
7239
|
+
return { left, by, narrowest, side };
|
|
7240
|
+
}).filter((c) => c.side !== 0 && c.by >= 44 && c.by <= H - PAD2.b + 4).sort((a, b) => b.narrowest - a.narrowest).find((c) => honest(runBox(c.left, c.by, w)));
|
|
7241
|
+
if (along) return { kind: "label", anchor: "middle", x: along.left + w / 2, by: along.by };
|
|
7242
|
+
const kw = SWATCH + SWATCH_GAP + w;
|
|
7243
|
+
const right = width - padR;
|
|
7244
|
+
const key = [42, H - 16].find((by) => {
|
|
7245
|
+
const b = runBox(right - kw, by, kw);
|
|
7246
|
+
return clearOfText(b) && !stacked(b) && !crossed(b);
|
|
7183
7247
|
});
|
|
7248
|
+
return key === void 0 ? void 0 : { kind: "key", left: right - kw, right, by: key };
|
|
7184
7249
|
})();
|
|
7185
|
-
const ghostLabel = cmp
|
|
7186
|
-
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.
|
|
7250
|
+
const ghostLabel = !cmp || !ghost ? "" : ghost.kind === "label" ? `
|
|
7251
|
+
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.anchor}" x="${n2(ghost.x)}" y="${n2(ghost.by)}">${esc(cmp.label)}</text>` : `
|
|
7252
|
+
<g class="ghostkey" id="${sid}-ghostlab"><line x1="${n2(ghost.left)}" y1="${n2(ghost.by - 14)}" x2="${n2(ghost.left + SWATCH)}" y2="${n2(ghost.by - 14)}" stroke="${theme.dim}" stroke-width="5" stroke-linecap="round" /><text class="ghostlab" text-anchor="end" x="${n2(ghost.right)}" y="${n2(ghost.by)}">${esc(cmp.label)}</text></g>`;
|
|
7187
7253
|
const readout = p.readout ? `
|
|
7188
7254
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
7189
7255
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -7245,7 +7311,12 @@ var lineChart = (beat2, ctx) => {
|
|
|
7245
7311
|
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
7246
7312
|
// separating from itself rather than as a second line arriving.
|
|
7247
7313
|
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
7248
|
-
tween(
|
|
7314
|
+
tween(
|
|
7315
|
+
`#${sid}-base`,
|
|
7316
|
+
{ stroke: theme.accent },
|
|
7317
|
+
{ stroke: theme.dim, duration: SEPARATE },
|
|
7318
|
+
lift2
|
|
7319
|
+
),
|
|
7249
7320
|
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
7250
7321
|
);
|
|
7251
7322
|
}
|
|
@@ -11801,8 +11872,11 @@ function closeSafe(code) {
|
|
|
11801
11872
|
}
|
|
11802
11873
|
function sceneHtml(sid, start, clip3, label, scene, transit) {
|
|
11803
11874
|
const setup = [...mathSetup(sid, scene.html), ...scene.setup ?? []].map(statement).join("\n ");
|
|
11875
|
+
const measure4 = (scene.measure ?? []).map(statement);
|
|
11876
|
+
const laps = lapSetup(sid, setup + measure4.join(""));
|
|
11804
11877
|
const build = (pad) => [
|
|
11805
|
-
...
|
|
11878
|
+
...measure4,
|
|
11879
|
+
...laps,
|
|
11806
11880
|
`window.__timelines = window.__timelines || {};`,
|
|
11807
11881
|
`// Each scene owns a paused timeline under its own composition id, with`,
|
|
11808
11882
|
`// times relative to its start \u2014 one absolute root timeline yields a`,
|
|
@@ -11847,6 +11921,15 @@ function mathSetup(sid, html) {
|
|
|
11847
11921
|
})`
|
|
11848
11922
|
];
|
|
11849
11923
|
}
|
|
11924
|
+
function lapSetup(sid, script) {
|
|
11925
|
+
if (!script.includes("katex.render")) return [];
|
|
11926
|
+
const laps = [".rlap", ".llap", ".clap"].map((c) => `#${sid} .katex ${c} *`).join(", ");
|
|
11927
|
+
return [
|
|
11928
|
+
statement(`Array.prototype.forEach.call(document.querySelectorAll('${laps}'), function (el) {
|
|
11929
|
+
el.setAttribute("data-layout-allow-overlap", "");
|
|
11930
|
+
})`)
|
|
11931
|
+
];
|
|
11932
|
+
}
|
|
11850
11933
|
function statement(s) {
|
|
11851
11934
|
const line2 = s.trim();
|
|
11852
11935
|
return line2.endsWith(";") ? line2 : `${line2};`;
|
|
@@ -18762,12 +18845,13 @@ function scanPaperArc(storyboard, prefs) {
|
|
|
18762
18845
|
}));
|
|
18763
18846
|
}
|
|
18764
18847
|
function scanNarrationDrift(storyboard, narration) {
|
|
18765
|
-
const
|
|
18848
|
+
const sentences = (s) => splitSentences((s ?? "").replace(/\s+/g, " ").trim()).join("\n");
|
|
18766
18849
|
const stale = [];
|
|
18767
18850
|
for (const beat2 of storyboard.beats) {
|
|
18768
18851
|
const segments2 = narration.beats[beat2.id];
|
|
18769
|
-
|
|
18770
|
-
if (
|
|
18852
|
+
const said = sentences(beat2.narration);
|
|
18853
|
+
if (!segments2?.length || !said) continue;
|
|
18854
|
+
if (sentences(segments2.map((s) => s.text).join(" ")) !== said) stale.push(beat2.id);
|
|
18771
18855
|
}
|
|
18772
18856
|
if (!stale.length) return [];
|
|
18773
18857
|
return [
|
package/dist/mcp.js
CHANGED
|
@@ -1655,7 +1655,11 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1655
1655
|
await mkdir2(dir, { recursive: true });
|
|
1656
1656
|
const cssPath = join3(dir, "fonts.css");
|
|
1657
1657
|
const cached2 = await readFile2(cssPath, "utf8").catch(() => "");
|
|
1658
|
-
if (cached2.startsWith(stamp))
|
|
1658
|
+
if (cached2.startsWith(stamp)) {
|
|
1659
|
+
const css2 = withInterMetrics(cached2);
|
|
1660
|
+
if (css2 !== cached2) await writeFile2(cssPath, css2);
|
|
1661
|
+
return { family, css: css2, files: localNames(css2) };
|
|
1662
|
+
}
|
|
1659
1663
|
const res = await fetch(
|
|
1660
1664
|
`https://fonts.googleapis.com/css2?${families.map((f) => `family=${f.replaceAll(" ", "+")}:wght@400;500;700`).join("&")}&text=${encodeURIComponent(text2)}&display=block`,
|
|
1661
1665
|
{ headers: { "User-Agent": UA } }
|
|
@@ -1673,11 +1677,19 @@ ${text2}`).digest("hex").slice(0, 16)} */`;
|
|
|
1673
1677
|
css = css.replaceAll(url, name);
|
|
1674
1678
|
files.push(name);
|
|
1675
1679
|
}
|
|
1676
|
-
css = `${stamp}
|
|
1677
|
-
${css}
|
|
1680
|
+
css = withInterMetrics(`${stamp}
|
|
1681
|
+
${css}`);
|
|
1678
1682
|
await writeFile2(cssPath, css);
|
|
1679
1683
|
return { family, css, files };
|
|
1680
1684
|
}
|
|
1685
|
+
var INTER_METRICS = "ascent-override: 96.875%;\n descent-override: 24.1211%;\n line-gap-override: 0%";
|
|
1686
|
+
function withInterMetrics(css) {
|
|
1687
|
+
return css.replace(
|
|
1688
|
+
/@font-face\s*\{(?![^}]*ascent-override)/g,
|
|
1689
|
+
`@font-face {
|
|
1690
|
+
${INTER_METRICS};`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1681
1693
|
function localNames(css) {
|
|
1682
1694
|
return [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1] ?? "");
|
|
1683
1695
|
}
|
|
@@ -2165,7 +2177,8 @@ var FONTSOURCE = {
|
|
|
2165
2177
|
async function vendorFace(face, dir) {
|
|
2166
2178
|
return vendorFontsource(FONTSOURCE[face], familyOf(face), dir);
|
|
2167
2179
|
}
|
|
2168
|
-
var PACK_DISPLAY =
|
|
2180
|
+
var PACK_DISPLAY = `font-display: block;
|
|
2181
|
+
${INTER_METRICS}`;
|
|
2169
2182
|
async function vendorFontsource(pkgName, family, dir) {
|
|
2170
2183
|
const pkg = dirname2(createRequire3(import.meta.url).resolve(pkgName));
|
|
2171
2184
|
const css = await readFile3(join4(pkg, "index.css"), "utf8");
|
|
@@ -6036,7 +6049,6 @@ var READOUT_W = 460;
|
|
|
6036
6049
|
var READOUT_LH = 1.35;
|
|
6037
6050
|
var TALL_ASPECT = 1.15;
|
|
6038
6051
|
var RESHAPE_SECONDS = 1.2;
|
|
6039
|
-
var GHOST_OPACITY = 0.28;
|
|
6040
6052
|
var DRAW_SECONDS = 1.8;
|
|
6041
6053
|
var DRAW_FLOOR = 1;
|
|
6042
6054
|
var SEPARATE = 0.5;
|
|
@@ -6113,6 +6125,14 @@ var lineChart = (beat, ctx) => {
|
|
|
6113
6125
|
return new Set(kept);
|
|
6114
6126
|
};
|
|
6115
6127
|
const LABEL_SIZE3 = 40;
|
|
6128
|
+
const RUN_ABOVE = Math.ceil(LABEL_SIZE3 * 0.969);
|
|
6129
|
+
const RUN_BELOW = Math.ceil(LABEL_SIZE3 * 0.242);
|
|
6130
|
+
const runBox = (left, by, w) => ({
|
|
6131
|
+
x: left,
|
|
6132
|
+
y: by - RUN_ABOVE,
|
|
6133
|
+
w,
|
|
6134
|
+
h: RUN_ABOVE + RUN_BELOW
|
|
6135
|
+
});
|
|
6116
6136
|
const runW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
6117
6137
|
const catW = (s) => textWidth(s, LABEL_SIZE3, 400, 0, false, face);
|
|
6118
6138
|
const nameW = (s) => textWidth(s, LABEL_SIZE3, 500, 0, false, face);
|
|
@@ -6127,17 +6147,47 @@ var lineChart = (beat, ctx) => {
|
|
|
6127
6147
|
const dots = p.points.map(
|
|
6128
6148
|
(pt, i) => `<circle class="dot" cx="${n2(x(i))}" cy="${n2(y(pt.y))}" r="${i === p.points.length - 1 ? 11 : 9}" fill="${i === p.points.length - 1 ? theme.tones.b : theme.accent}" />`
|
|
6129
6149
|
).join("");
|
|
6150
|
+
const labelBox = (cx, cy, text2, anchorStart = false) => {
|
|
6151
|
+
const w = runW(text2);
|
|
6152
|
+
return runBox(anchorStart ? cx : cx - w / 2, cy, w);
|
|
6153
|
+
};
|
|
6154
|
+
const curveBand = (pts, x0, x1) => {
|
|
6155
|
+
const ys = [];
|
|
6156
|
+
const py = (i) => y(pts[i]?.y ?? 0);
|
|
6157
|
+
for (let i = 0; i < pts.length; i++) {
|
|
6158
|
+
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
6159
|
+
}
|
|
6160
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
6161
|
+
const [ax, bx] = [x(i), x(i + 1)];
|
|
6162
|
+
for (const edge of [x0, x1]) {
|
|
6163
|
+
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
6164
|
+
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
6169
|
+
};
|
|
6170
|
+
const drawnThrough = (series, b) => {
|
|
6171
|
+
const [top, bottom] = curveBand(series, b.x, b.x + b.w);
|
|
6172
|
+
return Math.min(b.y + b.h, bottom) - Math.max(b.y, top) > 8;
|
|
6173
|
+
};
|
|
6174
|
+
const valueBy = (i) => {
|
|
6175
|
+
const v = p.points[i]?.y ?? 0;
|
|
6176
|
+
const over = Math.max(44, y(v) - 26);
|
|
6177
|
+
if (!cmp) return over;
|
|
6178
|
+
const box2 = (by) => labelBox(x(i), by, String(v), i === 0);
|
|
6179
|
+
if (!drawnThrough(cmp.points, box2(over))) return over;
|
|
6180
|
+
const under = y(v) + 16 + RUN_ABOVE;
|
|
6181
|
+
if (under > H - PAD2.b + 4) return over;
|
|
6182
|
+
return drawnThrough(cmp.points, box2(under)) || drawnThrough(p.points, box2(under)) ? over : under;
|
|
6183
|
+
};
|
|
6130
6184
|
const shownValues = fitIndices((i) => runW(String(p.points[i]?.y ?? "")), true);
|
|
6131
6185
|
const showsValue = (i) => shownValues.has(i);
|
|
6132
6186
|
const values = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => {
|
|
6133
6187
|
const anchor = i === 0 ? ' text-anchor="start"' : "";
|
|
6134
|
-
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(
|
|
6188
|
+
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(valueBy(i))}">${esc(String(pt.y))}</text>`;
|
|
6135
6189
|
}).join("");
|
|
6136
|
-
const
|
|
6137
|
-
const w = runW(text2);
|
|
6138
|
-
return { x: anchorStart ? cx : cx - w / 2, y: cy - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
6139
|
-
};
|
|
6140
|
-
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), Math.max(44, y(pt.y) - 26), String(pt.y), i === 0));
|
|
6190
|
+
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), valueBy(i), String(pt.y), i === 0));
|
|
6141
6191
|
const overlaps = (a, b) => Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 8 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 8;
|
|
6142
6192
|
const deltaBoxes = (p.deltas ?? []).slice(0, p.points.length - 1).map((d, i) => {
|
|
6143
6193
|
const a = p.points[i];
|
|
@@ -6165,40 +6215,22 @@ var lineChart = (beat, ctx) => {
|
|
|
6165
6215
|
lineTag("line", basePath, ' opacity="0"'),
|
|
6166
6216
|
`<path id="${sid}-target" d="${path2}" fill="none" stroke="none" />`
|
|
6167
6217
|
].join("\n ") : lineTag("line", path2);
|
|
6168
|
-
const curveBand = (pts, x0, x1) => {
|
|
6169
|
-
const ys = [];
|
|
6170
|
-
const py = (i) => y(pts[i]?.y ?? 0);
|
|
6171
|
-
for (let i = 0; i < pts.length; i++) {
|
|
6172
|
-
if (x(i) >= x0 && x(i) <= x1) ys.push(py(i));
|
|
6173
|
-
}
|
|
6174
|
-
for (let i = 0; i + 1 < pts.length; i++) {
|
|
6175
|
-
const [ax, bx] = [x(i), x(i + 1)];
|
|
6176
|
-
for (const edge of [x0, x1]) {
|
|
6177
|
-
if (edge > Math.min(ax, bx) && edge < Math.max(ax, bx)) {
|
|
6178
|
-
ys.push(py(i) + (py(i + 1) - py(i)) * (edge - ax) / (bx - ax));
|
|
6179
|
-
}
|
|
6180
|
-
}
|
|
6181
|
-
}
|
|
6182
|
-
return ys.length > 0 ? [Math.min(...ys), Math.max(...ys)] : [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
6183
|
-
};
|
|
6184
6218
|
const fixedBoxes = [
|
|
6185
6219
|
// `.axname` is the one run here set at weight 500 rather than 400, and
|
|
6186
6220
|
// under-charging a width is the unrecoverable direction (see `padR`).
|
|
6187
6221
|
// No `text-anchor` on the y half, so it runs rightwards from x=0.
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
x: PAD2.l + plotW / 2 - nameW(p.xLabel) / 2,
|
|
6191
|
-
y: H - 16 - LABEL_SIZE3,
|
|
6192
|
-
w: nameW(p.xLabel),
|
|
6193
|
-
h: LABEL_SIZE3
|
|
6194
|
-
},
|
|
6222
|
+
runBox(0, 42, nameW(p.yLabel)),
|
|
6223
|
+
runBox(PAD2.l + plotW / 2 - nameW(p.xLabel) / 2, H - 16, nameW(p.xLabel)),
|
|
6195
6224
|
...ticks.map((v) => {
|
|
6196
6225
|
const w = runW(v.toFixed(scale.decimals));
|
|
6197
|
-
return
|
|
6226
|
+
return runBox(PAD2.l - 22 - w, y(v) + 13, w);
|
|
6198
6227
|
}),
|
|
6199
6228
|
...p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => shownX.has(i)).map(({ pt, i }) => labelBox(x(i), H - PAD2.b + 56, pt.x))
|
|
6200
6229
|
];
|
|
6201
6230
|
const lastI = p.points.length - 1;
|
|
6231
|
+
const LINE = RUN_ABOVE + RUN_BELOW;
|
|
6232
|
+
const SWATCH = 48;
|
|
6233
|
+
const SWATCH_GAP = 16;
|
|
6202
6234
|
const ghost = (() => {
|
|
6203
6235
|
if (!cmp) return void 0;
|
|
6204
6236
|
const w = ghostW(cmp.label);
|
|
@@ -6207,6 +6239,18 @@ var lineChart = (beat, ctx) => {
|
|
|
6207
6239
|
`line-chart ${beat.id}: the compare label "${cmp.label}" is ${Math.ceil(w)}px against the ${Math.floor(plotW)}px of plot it is set in. Shorten it.`
|
|
6208
6240
|
);
|
|
6209
6241
|
}
|
|
6242
|
+
const drawnDeltas = deltasFit ? deltaBoxes : [];
|
|
6243
|
+
const gapTo = (series, b) => {
|
|
6244
|
+
const [top, bottom] = curveBand(series, b.x, b.x + b.w);
|
|
6245
|
+
return Math.max(0, top - (b.y + b.h), b.y - bottom);
|
|
6246
|
+
};
|
|
6247
|
+
const clearOfText = (b) => !fixedBoxes.some((f) => overlaps(b, f)) && !valueBoxes.some((v) => overlaps(b, v)) && !drawnDeltas.some((d) => d !== null && overlaps(b, d));
|
|
6248
|
+
const stacked = (b) => valueBoxes.some(
|
|
6249
|
+
(v) => Math.min(b.x + b.w, v.x + v.w) - Math.max(b.x, v.x) > 0 && Math.max(v.y - (b.y + b.h), b.y - (v.y + v.h)) < LINE
|
|
6250
|
+
);
|
|
6251
|
+
const crossed = (b) => [p.points, cmp.points].some((series) => drawnThrough(series, b));
|
|
6252
|
+
const owned = (b) => gapTo(p.points, b) - gapTo(cmp.points, b) >= LINE;
|
|
6253
|
+
const honest = (b) => clearOfText(b) && !stacked(b) && !crossed(b) && owned(b);
|
|
6210
6254
|
const atEnd = (i, start) => {
|
|
6211
6255
|
const cy = y(cmp.points[i]?.y ?? 0);
|
|
6212
6256
|
const away = Math.max(44, cy - 26);
|
|
@@ -6214,20 +6258,42 @@ var lineChart = (beat, ctx) => {
|
|
|
6214
6258
|
const sides = cy < y(p.points[i]?.y ?? 0) ? [away, toward] : [toward, away];
|
|
6215
6259
|
return sides.map((by) => ({ i, start, by }));
|
|
6216
6260
|
};
|
|
6217
|
-
const
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6261
|
+
const end2 = [...atEnd(lastI, false), ...atEnd(0, true)].find(
|
|
6262
|
+
(c) => honest(runBox(c.start ? x(c.i) : x(c.i) - w, c.by, w))
|
|
6263
|
+
);
|
|
6264
|
+
if (end2) {
|
|
6265
|
+
return { kind: "label", anchor: end2.start ? "start" : "end", x: x(end2.i), by: end2.by };
|
|
6266
|
+
}
|
|
6267
|
+
const yAt = (series, at2) => curveBand(series, at2, at2)[0];
|
|
6268
|
+
const sep5 = (at2) => yAt(p.points, at2) - yAt(cmp.points, at2);
|
|
6269
|
+
const centres = p.points.flatMap(
|
|
6270
|
+
(_, i) => i < lastI ? [x(i), (x(i) + x(i + 1)) / 2] : [x(i)]
|
|
6271
|
+
);
|
|
6272
|
+
const along = centres.map((cx) => {
|
|
6273
|
+
const left = Math.min(Math.max(cx - w / 2, PAD2.l), width - padR - w);
|
|
6274
|
+
const side = Math.sign(sep5(left + w / 2));
|
|
6275
|
+
const cols = [
|
|
6276
|
+
left,
|
|
6277
|
+
left + w,
|
|
6278
|
+
...p.points.map((_, i) => x(i)).filter((v) => v > left && v < left + w)
|
|
6279
|
+
];
|
|
6280
|
+
const narrowest = Math.min(...cols.map((c) => side * sep5(c)));
|
|
6281
|
+
const [top, bottom] = curveBand(cmp.points, left, left + w);
|
|
6282
|
+
const by = side > 0 ? top - 16 - RUN_BELOW : bottom + 16 + RUN_ABOVE;
|
|
6283
|
+
return { left, by, narrowest, side };
|
|
6284
|
+
}).filter((c) => c.side !== 0 && c.by >= 44 && c.by <= H - PAD2.b + 4).sort((a, b) => b.narrowest - a.narrowest).find((c) => honest(runBox(c.left, c.by, w)));
|
|
6285
|
+
if (along) return { kind: "label", anchor: "middle", x: along.left + w / 2, by: along.by };
|
|
6286
|
+
const kw = SWATCH + SWATCH_GAP + w;
|
|
6287
|
+
const right = width - padR;
|
|
6288
|
+
const key = [42, H - 16].find((by) => {
|
|
6289
|
+
const b = runBox(right - kw, by, kw);
|
|
6290
|
+
return clearOfText(b) && !stacked(b) && !crossed(b);
|
|
6227
6291
|
});
|
|
6292
|
+
return key === void 0 ? void 0 : { kind: "key", left: right - kw, right, by: key };
|
|
6228
6293
|
})();
|
|
6229
|
-
const ghostLabel = cmp
|
|
6230
|
-
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.
|
|
6294
|
+
const ghostLabel = !cmp || !ghost ? "" : ghost.kind === "label" ? `
|
|
6295
|
+
<text class="ghostlab" id="${sid}-ghostlab" text-anchor="${ghost.anchor}" x="${n2(ghost.x)}" y="${n2(ghost.by)}">${esc(cmp.label)}</text>` : `
|
|
6296
|
+
<g class="ghostkey" id="${sid}-ghostlab"><line x1="${n2(ghost.left)}" y1="${n2(ghost.by - 14)}" x2="${n2(ghost.left + SWATCH)}" y2="${n2(ghost.by - 14)}" stroke="${theme.dim}" stroke-width="5" stroke-linecap="round" /><text class="ghostlab" text-anchor="end" x="${n2(ghost.right)}" y="${n2(ghost.by)}">${esc(cmp.label)}</text></g>`;
|
|
6231
6297
|
const readout = p.readout ? `
|
|
6232
6298
|
<div class="readout" id="${sid}-read">${esc(p.readout)}</div>` : "";
|
|
6233
6299
|
const html = `${chrome(sid, p.eyebrow, p.headline, box, face)}
|
|
@@ -6289,7 +6355,12 @@ var lineChart = (beat, ctx) => {
|
|
|
6289
6355
|
// The copy lifts off — same geometry, so the half-second reads as one line
|
|
6290
6356
|
// separating from itself rather than as a second line arriving.
|
|
6291
6357
|
tween(`#${sid}-line`, { opacity: 0 }, { opacity: 1, duration: SEPARATE }, lift2),
|
|
6292
|
-
tween(
|
|
6358
|
+
tween(
|
|
6359
|
+
`#${sid}-base`,
|
|
6360
|
+
{ stroke: theme.accent },
|
|
6361
|
+
{ stroke: theme.dim, duration: SEPARATE },
|
|
6362
|
+
lift2
|
|
6363
|
+
),
|
|
6293
6364
|
reshape(`#${sid}-line`, `#${sid}-target`, lift2 + SEPARATE, RESHAPE_SECONDS, true)
|
|
6294
6365
|
);
|
|
6295
6366
|
}
|
|
@@ -10767,8 +10838,11 @@ function closeSafe(code) {
|
|
|
10767
10838
|
}
|
|
10768
10839
|
function sceneHtml(sid, start, clip3, label, scene, transit) {
|
|
10769
10840
|
const setup = [...mathSetup(sid, scene.html), ...scene.setup ?? []].map(statement).join("\n ");
|
|
10841
|
+
const measure4 = (scene.measure ?? []).map(statement);
|
|
10842
|
+
const laps = lapSetup(sid, setup + measure4.join(""));
|
|
10770
10843
|
const build = (pad) => [
|
|
10771
|
-
...
|
|
10844
|
+
...measure4,
|
|
10845
|
+
...laps,
|
|
10772
10846
|
`window.__timelines = window.__timelines || {};`,
|
|
10773
10847
|
`// Each scene owns a paused timeline under its own composition id, with`,
|
|
10774
10848
|
`// times relative to its start \u2014 one absolute root timeline yields a`,
|
|
@@ -10813,6 +10887,15 @@ function mathSetup(sid, html) {
|
|
|
10813
10887
|
})`
|
|
10814
10888
|
];
|
|
10815
10889
|
}
|
|
10890
|
+
function lapSetup(sid, script) {
|
|
10891
|
+
if (!script.includes("katex.render")) return [];
|
|
10892
|
+
const laps = [".rlap", ".llap", ".clap"].map((c) => `#${sid} .katex ${c} *`).join(", ");
|
|
10893
|
+
return [
|
|
10894
|
+
statement(`Array.prototype.forEach.call(document.querySelectorAll('${laps}'), function (el) {
|
|
10895
|
+
el.setAttribute("data-layout-allow-overlap", "");
|
|
10896
|
+
})`)
|
|
10897
|
+
];
|
|
10898
|
+
}
|
|
10816
10899
|
function statement(s) {
|
|
10817
10900
|
const line2 = s.trim();
|
|
10818
10901
|
return line2.endsWith(";") ? line2 : `${line2};`;
|
|
@@ -23,3 +23,25 @@ export declare function familyFor(lang: string): string | null;
|
|
|
23
23
|
export declare function bundleFont(lang: string, glyphs: string, dir: string, opts?: {
|
|
24
24
|
serif?: boolean;
|
|
25
25
|
}): Promise<FontBundle | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Inter's vertical metrics as `@font-face` descriptors: ascent 0.969em,
|
|
28
|
+
* descent 0.241em, no line gap.
|
|
29
|
+
*
|
|
30
|
+
* Every vertical number in the emitters (line boxes, the SVG label `dy`, the
|
|
31
|
+
* centring of a tspan block) and every box the gates measure was tuned on Inter.
|
|
32
|
+
* `src/build/files.ts` declares a v2 pack's Latin faces with these for that
|
|
33
|
+
* reason, and the CJK bundle needs them for the same one. Noto Sans SC as Google
|
|
34
|
+
* serves it is 1.160 / 0.288 (hhea, read from the woff2), so its line box is
|
|
35
|
+
* 1.448em where the emitters budget 1.21. MEASURED in Chrome on a zh-Hans deck
|
|
36
|
+
* (HypePaper, v0.9.2, 2026-10-11): a 56px split-compare heading spanned 65px
|
|
37
|
+
* above its baseline and overran the top of its svg by 11px. Every verify
|
|
38
|
+
* reports that as `container_overflow` on `#sN-sideI`; HypePaper's build also
|
|
39
|
+
* reported it as `text_box_overflow` on `#sN-labI`, failed on it, and a later
|
|
40
|
+
* verify of the same deck did not. The ink was never cut, because a Han glyph's
|
|
41
|
+
* ink sits inside 0.88em. Only the box was wrong.
|
|
42
|
+
*
|
|
43
|
+
* Advances are untouched, so the width tables in `src/emit/svg.ts` still apply.
|
|
44
|
+
*/
|
|
45
|
+
export declare const INTER_METRICS = "ascent-override: 96.875%;\n descent-override: 24.1211%;\n line-gap-override: 0%";
|
|
46
|
+
/** Declare every `@font-face` in `css` with `INTER_METRICS`. Idempotent. */
|
|
47
|
+
export declare function withInterMetrics(css: string): string;
|
|
@@ -340,10 +340,16 @@ export declare function scanPaperArc(storyboard: Storyboard, prefs: Prefs): Find
|
|
|
340
340
|
* NARROW ON PURPOSE. It compares only beats that have BOTH a narration text and
|
|
341
341
|
* segments, so a deck narrated at a lower density — where some beats are
|
|
342
342
|
* deliberately silent — says nothing, and the existing missing-audio error keeps
|
|
343
|
-
* its own case.
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
343
|
+
* its own case.
|
|
344
|
+
*
|
|
345
|
+
* COMPARED SENTENCE BY SENTENCE, through the same `splitSentences` that cut the
|
|
346
|
+
* segments. The splitter trims each sentence, so whitespace at a sentence
|
|
347
|
+
* boundary is gone from the record and the segments' rejoin cannot know what it
|
|
348
|
+
* was. Rejoining with a space and comparing strings was right for English and
|
|
349
|
+
* wrong for every Chinese and Japanese deck: they write no space after 。, so
|
|
350
|
+
* every beat narrated over two or more stops "drifted" — 15 of 18 beats on a
|
|
351
|
+
* freshly narrated zh-Hans HypePaper run, exactly the multi-segment ones.
|
|
352
|
+
* Whitespace inside a sentence is still normalised, not dropped.
|
|
347
353
|
*/
|
|
348
354
|
export declare function scanNarrationDrift(storyboard: Storyboard, narration: {
|
|
349
355
|
beats: Record<string, {
|