@jokerized/decksmith 0.2.0 → 0.3.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 +2 -1
- package/dist/cli.js +455 -110
- package/dist/ds-morph.js +1 -0
- package/dist/index.js +452 -110
- package/dist/mcp.js +263 -33
- package/dist/types/emit/archetypes/equation-morph.d.ts +2 -0
- package/dist/types/emit/archetypes/equation-walk.d.ts +63 -1
- package/dist/types/emit/archetypes/index.d.ts +1 -1
- package/dist/types/emit/archetypes/stack.d.ts +7 -0
- package/dist/types/emit/depth.d.ts +95 -0
- package/dist/types/emit/kit.d.ts +7 -0
- package/dist/types/emit/morph-runtime.d.ts +178 -0
- package/dist/types/types.d.ts +142 -0
- package/dist/types/verify/apparent.d.ts +123 -0
- package/dist/types/verify/typefloor.d.ts +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -107,6 +107,13 @@ var equationWalkParamsSchema = z.object({
|
|
|
107
107
|
/** Walked in order, one hold-point each. */
|
|
108
108
|
terms: z.array(termSchema).min(1).max(4)
|
|
109
109
|
});
|
|
110
|
+
var equationMorphParamsSchema = z.object({
|
|
111
|
+
eyebrow: z.string().optional(),
|
|
112
|
+
headline: z.string(),
|
|
113
|
+
fromId: z.string(),
|
|
114
|
+
toId: z.string(),
|
|
115
|
+
terms: z.array(termSchema).min(1).max(4)
|
|
116
|
+
});
|
|
110
117
|
var dataTableParamsSchema = z.object({
|
|
111
118
|
eyebrow: z.string().optional(),
|
|
112
119
|
headline: z.string(),
|
|
@@ -235,7 +242,22 @@ var stackParamsSchema = z.object({
|
|
|
235
242
|
headline: z.string(),
|
|
236
243
|
/** Drawn bottom-up as offset planes, revealed in order. */
|
|
237
244
|
layers: z.array(z.object({ label: z.string(), note: z.string().optional() })).min(2).max(7),
|
|
238
|
-
note: z.string().optional()
|
|
245
|
+
note: z.string().optional(),
|
|
246
|
+
/**
|
|
247
|
+
* Tilt the slab stack away from the viewer, so the planes read as stacked in
|
|
248
|
+
* depth rather than merely offset up the page.
|
|
249
|
+
*
|
|
250
|
+
* OPTIONAL, and absent means flat — every storyboard written before this
|
|
251
|
+
* existed still parses, and every deck that does not ask for it emits exactly
|
|
252
|
+
* the bytes it did before.
|
|
253
|
+
*
|
|
254
|
+
* Degrees, and bounded at 18 rather than by taste: the tilt is paid for in
|
|
255
|
+
* declared type, because perspective shrinks the far half of the plane and
|
|
256
|
+
* invariant 5 is about what the audience SEES. At 18 degrees a 40px floor
|
|
257
|
+
* already needs 53.8px declared (`src/emit/depth.ts`), and past that a
|
|
258
|
+
* headline cannot spend enough and still fit its own line.
|
|
259
|
+
*/
|
|
260
|
+
tilt: z.number().min(0).max(18).optional()
|
|
239
261
|
});
|
|
240
262
|
var splitSideSchema = z.object({
|
|
241
263
|
label: z.string(),
|
|
@@ -300,6 +322,12 @@ var beatSchema = z.discriminatedUnion("archetype", [
|
|
|
300
322
|
params: equationWalkParamsSchema,
|
|
301
323
|
...beatTail
|
|
302
324
|
}),
|
|
325
|
+
z.object({
|
|
326
|
+
...beatCore,
|
|
327
|
+
archetype: z.literal("equation-morph"),
|
|
328
|
+
params: equationMorphParamsSchema,
|
|
329
|
+
...beatTail
|
|
330
|
+
}),
|
|
303
331
|
z.object({
|
|
304
332
|
...beatCore,
|
|
305
333
|
archetype: z.literal("data-table"),
|
|
@@ -354,6 +382,7 @@ var DIAGRAMMATIC = /* @__PURE__ */ new Set([
|
|
|
354
382
|
"stack",
|
|
355
383
|
"split-compare",
|
|
356
384
|
"equation-walk",
|
|
385
|
+
"equation-morph",
|
|
357
386
|
"line-chart"
|
|
358
387
|
]);
|
|
359
388
|
var ARCHETYPE_FAMILY = {
|
|
@@ -368,7 +397,8 @@ var ARCHETYPE_FAMILY = {
|
|
|
368
397
|
"bar-compare": "quantity",
|
|
369
398
|
"line-chart": "quantity",
|
|
370
399
|
"data-table": "quantity",
|
|
371
|
-
"equation-walk": "formal"
|
|
400
|
+
"equation-walk": "formal",
|
|
401
|
+
"equation-morph": "formal"
|
|
372
402
|
};
|
|
373
403
|
var storyboardSchema = z.object({
|
|
374
404
|
sourceId: z.string(),
|
|
@@ -3079,7 +3109,7 @@ function locate(tex, term) {
|
|
|
3079
3109
|
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
3080
3110
|
return { start, end: Math.min(end, tex.length) };
|
|
3081
3111
|
}
|
|
3082
|
-
function wrapTerms(tex, terms, beatId) {
|
|
3112
|
+
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
3083
3113
|
let parts = [{ text: tex, raw: true }];
|
|
3084
3114
|
const used = [];
|
|
3085
3115
|
const missing = [];
|
|
@@ -3095,7 +3125,7 @@ function wrapTerms(tex, terms, beatId) {
|
|
|
3095
3125
|
1,
|
|
3096
3126
|
{ text: part.text.slice(0, at.start), raw: true },
|
|
3097
3127
|
{
|
|
3098
|
-
text: `\\htmlClass{
|
|
3128
|
+
text: `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`,
|
|
3099
3129
|
raw: false
|
|
3100
3130
|
},
|
|
3101
3131
|
{ text: part.text.slice(at.end), raw: true }
|
|
@@ -3128,6 +3158,24 @@ function statements(tex, stacked) {
|
|
|
3128
3158
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
3129
3159
|
return parts.length > 0 ? parts : [tex];
|
|
3130
3160
|
}
|
|
3161
|
+
function legendRows(sid, terms, theme) {
|
|
3162
|
+
return terms.map(
|
|
3163
|
+
(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>`
|
|
3164
|
+
).join("\n ");
|
|
3165
|
+
}
|
|
3166
|
+
function legendCss(theme) {
|
|
3167
|
+
return [
|
|
3168
|
+
// `width:fit-content` + auto margins, not `align-items:center`: centring
|
|
3169
|
+
// each row individually gave the legend a ragged left edge, because a short
|
|
3170
|
+
// label indented its own chip further than a long one did. The column is
|
|
3171
|
+
// centred as one block and the rows start on a shared spine.
|
|
3172
|
+
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3173
|
+
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3174
|
+
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3175
|
+
// are one symbol each and their natural widths differ by a few pixels.
|
|
3176
|
+
`.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
|
|
3177
|
+
].join("\n");
|
|
3178
|
+
}
|
|
3131
3179
|
var equationWalk = (beat, ctx) => {
|
|
3132
3180
|
const { sid, theme } = ctx;
|
|
3133
3181
|
const p = beat.params;
|
|
@@ -3139,9 +3187,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3139
3187
|
}
|
|
3140
3188
|
const walk = wrapTerms(eq.tex, p.terms, beat.id);
|
|
3141
3189
|
const terms = walk.used;
|
|
3142
|
-
const legend = terms
|
|
3143
|
-
(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>`
|
|
3144
|
-
).join("\n ");
|
|
3190
|
+
const legend = legendRows(sid, terms, theme);
|
|
3145
3191
|
const stacked = isPortrait(ctx.format);
|
|
3146
3192
|
const raw2 = statements(eq.tex, stacked);
|
|
3147
3193
|
const shown = statements(walk.tex, stacked);
|
|
@@ -3240,15 +3286,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3240
3286
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
3241
3287
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
3242
3288
|
".term{display:inline-block}",
|
|
3243
|
-
|
|
3244
|
-
// each row individually gave the legend a ragged left edge, because a short
|
|
3245
|
-
// label indented its own chip further than a long one did. The column is
|
|
3246
|
-
// centred as one block and the rows start on a shared spine.
|
|
3247
|
-
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3248
|
-
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3249
|
-
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3250
|
-
// are one symbol each and their natural widths differ by a few pixels.
|
|
3251
|
-
`.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`,
|
|
3289
|
+
legendCss(theme),
|
|
3252
3290
|
// The block, not the term under discussion: which term that is, is a fact
|
|
3253
3291
|
// about the paused timeline, and CSS cannot see it. The terms are also the
|
|
3254
3292
|
// one thing here GSAP tints and swells, so a rule on them would win the
|
|
@@ -3258,6 +3296,117 @@ var equationWalk = (beat, ctx) => {
|
|
|
3258
3296
|
};
|
|
3259
3297
|
};
|
|
3260
3298
|
|
|
3299
|
+
// src/emit/archetypes/equation-morph.ts
|
|
3300
|
+
var MORPH_SECONDS = 1.6;
|
|
3301
|
+
var equationMorph = (beat, ctx) => {
|
|
3302
|
+
const { sid, theme } = ctx;
|
|
3303
|
+
const p = beat.params;
|
|
3304
|
+
const find2 = (id2) => {
|
|
3305
|
+
const eq = ctx.source.equations.find((e) => e.id === id2);
|
|
3306
|
+
if (!eq)
|
|
3307
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${ctx.source.id}`);
|
|
3308
|
+
return eq;
|
|
3309
|
+
};
|
|
3310
|
+
const a = find2(p.fromId);
|
|
3311
|
+
const b = find2(p.toId);
|
|
3312
|
+
const both = p.terms.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
3313
|
+
if (both.length === 0) {
|
|
3314
|
+
throw new Error(
|
|
3315
|
+
`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)}`
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
3319
|
+
const wa = wrapTerms(a.tex, both, beat.id, cls).tex;
|
|
3320
|
+
const wb = wrapTerms(b.tex, both, beat.id, cls).tex;
|
|
3321
|
+
const size2 = Math.max(
|
|
3322
|
+
MIN_FONT,
|
|
3323
|
+
Math.min(
|
|
3324
|
+
equationSize(a.tex.length > b.tex.length ? a.tex : b.tex),
|
|
3325
|
+
Math.floor(contentW(ctx.format) / Math.max(texUnits(a.tex), texUnits(b.tex)))
|
|
3326
|
+
)
|
|
3327
|
+
);
|
|
3328
|
+
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
3329
|
+
<div class="eqslide">
|
|
3330
|
+
<div class="morph" id="${sid}-morph" style="font-size:${size2}px">
|
|
3331
|
+
<div class="side" data-morph="a" id="${sid}-eqa"></div>
|
|
3332
|
+
<div class="side" data-morph="b" id="${sid}-eqb"></div>
|
|
3333
|
+
</div>
|
|
3334
|
+
<div class="legend">
|
|
3335
|
+
${legendRows(sid, both, theme)}
|
|
3336
|
+
</div>
|
|
3337
|
+
</div>`;
|
|
3338
|
+
const setup = [
|
|
3339
|
+
`var OPTS = ${OPTS};`,
|
|
3340
|
+
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
3341
|
+
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
3342
|
+
...both.map(
|
|
3343
|
+
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
3344
|
+
)
|
|
3345
|
+
];
|
|
3346
|
+
const first = 1.8;
|
|
3347
|
+
const at = Math.round(
|
|
3348
|
+
Math.max(2.6, Math.min(beat.seconds - MORPH_SECONDS - 0.9, beat.seconds * 0.45)) * 100
|
|
3349
|
+
) / 100;
|
|
3350
|
+
const tl = [
|
|
3351
|
+
...chromeIn(sid, p.eyebrow !== void 0),
|
|
3352
|
+
tween(`#${sid}-morph`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8),
|
|
3353
|
+
...both.map(
|
|
3354
|
+
(t2, i) => tween(
|
|
3355
|
+
`#${sid}-leg-${t2.tone}`,
|
|
3356
|
+
{ opacity: 0, x: -18 },
|
|
3357
|
+
{ opacity: 1, x: 0, duration: 0.5 },
|
|
3358
|
+
1 + i * 0.15
|
|
3359
|
+
)
|
|
3360
|
+
),
|
|
3361
|
+
// ONE tween, on the host, driving the plugin. Its ease is "none" because the
|
|
3362
|
+
// plan carries its own eases per segment; `pace` scales this duration and
|
|
3363
|
+
// the plan, being in fractions of it, scales with it.
|
|
3364
|
+
tween(
|
|
3365
|
+
`#${sid}-morph`,
|
|
3366
|
+
{ dsMorph: 0 },
|
|
3367
|
+
{ dsMorph: 1, duration: MORPH_SECONDS, ease: "none" },
|
|
3368
|
+
at
|
|
3369
|
+
)
|
|
3370
|
+
];
|
|
3371
|
+
return {
|
|
3372
|
+
html,
|
|
3373
|
+
tl,
|
|
3374
|
+
setup,
|
|
3375
|
+
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
3376
|
+
// the ready gate, and the plugin tween above finds it on the host.
|
|
3377
|
+
measure: [`DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
3378
|
+
plugins: ["dsMorph"],
|
|
3379
|
+
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
3380
|
+
css: [
|
|
3381
|
+
chromeCss(theme),
|
|
3382
|
+
".eqslide{display:flex;flex-direction:column;justify-content:space-evenly;gap:64px;flex:1;min-height:0}",
|
|
3383
|
+
".katex-display{margin:0 !important}",
|
|
3384
|
+
// Both sides in one grid cell, so the host is as tall as the taller line
|
|
3385
|
+
// and neither needs a guessed height; the overlay is absolute over it.
|
|
3386
|
+
// Padded by the room an arc needs, so a bowing glyph stays inside its
|
|
3387
|
+
// offset parent and the layout gate's `escaped_container` stays quiet; the
|
|
3388
|
+
// bow is capped to the same 0.8em in `plan`.
|
|
3389
|
+
`.morph{position:relative;display:grid;place-items:center;text-align:center;padding:0.8em 0.5em;color:${theme.fg}}`,
|
|
3390
|
+
".side{grid-area:1/1}",
|
|
3391
|
+
// B is measured, never seen: the runtime lifts its glyphs into the overlay
|
|
3392
|
+
// and drives them from there. Hidden by the sheet so nothing is captured
|
|
3393
|
+
// before the gate has built the plan.
|
|
3394
|
+
'.side[data-morph="b"]{visibility:hidden}',
|
|
3395
|
+
".ds-morph-layer{position:absolute;inset:0}",
|
|
3396
|
+
".term{display:inline-block}",
|
|
3397
|
+
// Keys are tinted from the start, on BOTH lines — the colour is what lets
|
|
3398
|
+
// a viewer follow a body across the move. Scoped under `.morph` because
|
|
3399
|
+
// `equation-walk` tweens `.t-<tone>` from the foreground colour, and a
|
|
3400
|
+
// bare rule on the class would win that cascade and cancel its walk.
|
|
3401
|
+
...["a", "b", "c", "d"].map(
|
|
3402
|
+
(tone2) => `.morph .t-${tone2}{color:${theme.tones[tone2]}}`
|
|
3403
|
+
),
|
|
3404
|
+
legendCss(theme),
|
|
3405
|
+
ambient(sid, "-morph", BREATHE)
|
|
3406
|
+
].join("\n")
|
|
3407
|
+
};
|
|
3408
|
+
};
|
|
3409
|
+
|
|
3261
3410
|
// src/emit/archetypes/grid.ts
|
|
3262
3411
|
var LABEL = 42;
|
|
3263
3412
|
var LH = 1.25;
|
|
@@ -4541,6 +4690,30 @@ var splitCompare = (beat, ctx) => {
|
|
|
4541
4690
|
};
|
|
4542
4691
|
};
|
|
4543
4692
|
|
|
4693
|
+
// src/emit/depth.ts
|
|
4694
|
+
var DEFAULT_POSE = { rotateX: 12, perspective: 1400 };
|
|
4695
|
+
function scaleAt(pose, dy) {
|
|
4696
|
+
const t2 = pose.rotateX * Math.PI / 180;
|
|
4697
|
+
const denom = pose.perspective - dy * Math.sin(t2);
|
|
4698
|
+
if (denom <= 0) return 0;
|
|
4699
|
+
return Math.cos(t2) * (pose.perspective / denom) ** 2;
|
|
4700
|
+
}
|
|
4701
|
+
var MODEL_SLACK = 0.98;
|
|
4702
|
+
function worstScale(pose, height) {
|
|
4703
|
+
if (scaleAt(pose, height / 2) <= 0) return 0;
|
|
4704
|
+
return scaleAt(pose, -height / 2) * MODEL_SLACK;
|
|
4705
|
+
}
|
|
4706
|
+
function tiltedFloor(pose, height, floor) {
|
|
4707
|
+
const s = worstScale(pose, height);
|
|
4708
|
+
return s > 0 ? floor / s : Number.POSITIVE_INFINITY;
|
|
4709
|
+
}
|
|
4710
|
+
function depthCss(sid, pose, part) {
|
|
4711
|
+
return [
|
|
4712
|
+
`#${sid} { perspective: ${pose.perspective}px; }`,
|
|
4713
|
+
`#${sid} ${part} { transform: rotateX(${pose.rotateX}deg); transform-origin: 50% 50%; }`
|
|
4714
|
+
].join("\n");
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4544
4717
|
// src/emit/archetypes/stack.ts
|
|
4545
4718
|
var GAP3 = 44;
|
|
4546
4719
|
var NUM_X = 48;
|
|
@@ -4564,15 +4737,20 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4564
4737
|
function labelWeight(i, count) {
|
|
4565
4738
|
return i === count - 1 ? 700 : 600;
|
|
4566
4739
|
}
|
|
4740
|
+
function floorFor(p, format) {
|
|
4741
|
+
if (!p.tilt) return MIN_FONT;
|
|
4742
|
+
return tiltedFloor({ ...DEFAULT_POSE, rotateX: p.tilt }, contentH(format), MIN_FONT);
|
|
4743
|
+
}
|
|
4567
4744
|
function solve2(p, format, inline, face) {
|
|
4568
4745
|
const width = contentW(format);
|
|
4569
4746
|
const boxH = contentH(format);
|
|
4747
|
+
const floor = floorFor(p, format);
|
|
4570
4748
|
const count = p.layers.length;
|
|
4571
4749
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4572
4750
|
const riseMax = RISE_MAX[k];
|
|
4573
4751
|
const syMax = SY_MAX[k];
|
|
4574
4752
|
const tMax = T_MAX[k];
|
|
4575
|
-
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note,
|
|
4753
|
+
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4576
4754
|
const want = Math.max(
|
|
4577
4755
|
...p.layers.map(
|
|
4578
4756
|
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count), 0, false, face) + noteW(l)
|
|
@@ -4587,7 +4765,7 @@ function solve2(p, format, inline, face) {
|
|
|
4587
4765
|
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count), 0, false, face))
|
|
4588
4766
|
)
|
|
4589
4767
|
);
|
|
4590
|
-
const labelSize = Math.max(
|
|
4768
|
+
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
4591
4769
|
const lines = p.layers.map((l, i) => {
|
|
4592
4770
|
const nw = noteW(l);
|
|
4593
4771
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
@@ -4595,14 +4773,14 @@ function solve2(p, format, inline, face) {
|
|
|
4595
4773
|
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count), 0, face),
|
|
4596
4774
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4597
4775
|
// short line" — and wrapping one would put its second line under the label.
|
|
4598
|
-
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note,
|
|
4776
|
+
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
4599
4777
|
noteW: nw,
|
|
4600
4778
|
labelMaxW
|
|
4601
4779
|
};
|
|
4602
4780
|
});
|
|
4603
4781
|
const blockH = Math.max(
|
|
4604
4782
|
...lines.map(
|
|
4605
|
-
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length *
|
|
4783
|
+
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length * floor) * 1.16 : l.label.length * labelSize * 1.16 + (l.note.length > 0 ? 6 + l.note.length * floor * 1.16 : 0)
|
|
4606
4784
|
)
|
|
4607
4785
|
);
|
|
4608
4786
|
const pad = Math.max(EDGE2, blockH / 2);
|
|
@@ -4622,6 +4800,7 @@ function solve2(p, format, inline, face) {
|
|
|
4622
4800
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
4623
4801
|
// layout that fits; it is one whose overflow is in the axis nothing measured.
|
|
4624
4802
|
fits: room >= blockH + 10 && height <= free && wide,
|
|
4803
|
+
floor,
|
|
4625
4804
|
wide,
|
|
4626
4805
|
inline,
|
|
4627
4806
|
width,
|
|
@@ -4661,7 +4840,7 @@ var stack = (beat, ctx) => {
|
|
|
4661
4840
|
const last = count - 1;
|
|
4662
4841
|
if (!L.fits) {
|
|
4663
4842
|
throw new Error(
|
|
4664
|
-
`stack ${beat.id}: ${count} 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 ${
|
|
4843
|
+
`stack ${beat.id}: ${count} 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.`
|
|
4665
4844
|
);
|
|
4666
4845
|
}
|
|
4667
4846
|
const parts = {};
|
|
@@ -4683,7 +4862,7 @@ var stack = (beat, ctx) => {
|
|
|
4683
4862
|
const dot = circle({ x: L.x0 + L.w + L.sx / 2 + 8, y: mid }, 6, { fill: tint });
|
|
4684
4863
|
const block = L.lines[i] ?? { label: [], note: [], noteW: 0, labelMaxW: L.colW };
|
|
4685
4864
|
const labelH = block.label.length * L.labelSize * 1.16;
|
|
4686
|
-
const noteH = block.note.length > 0 ? 6 + block.note.length *
|
|
4865
|
+
const noteH = block.note.length > 0 ? 6 + block.note.length * L.floor * 1.16 : 0;
|
|
4687
4866
|
const label = text(
|
|
4688
4867
|
layer.label,
|
|
4689
4868
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
@@ -4709,7 +4888,7 @@ var stack = (beat, ctx) => {
|
|
|
4709
4888
|
{ x: L.labelX, y: mid + labelH / 2 + 3 }
|
|
4710
4889
|
),
|
|
4711
4890
|
{
|
|
4712
|
-
size:
|
|
4891
|
+
size: L.floor,
|
|
4713
4892
|
fill: theme.muted,
|
|
4714
4893
|
anchor: L.inline ? "end" : "start",
|
|
4715
4894
|
maxWidth: L.inline ? void 0 : L.colW,
|
|
@@ -4721,7 +4900,7 @@ var stack = (beat, ctx) => {
|
|
|
4721
4900
|
const num = text(
|
|
4722
4901
|
String(i + 1),
|
|
4723
4902
|
{ x: NUM_X, y: mid },
|
|
4724
|
-
{ size:
|
|
4903
|
+
{ size: L.floor, weight: 600, fill: theme.dim, anchor: "end", vAlign: "middle" }
|
|
4725
4904
|
);
|
|
4726
4905
|
return group(slab(L.x0, y0, L, tint, lift2, stroke), { id: id(sid, "lay", i), class: "lay" }) + group(num + leader + dot + label + note2, { id: id(sid, "cap", i), class: "cap" });
|
|
4727
4906
|
}).join("");
|
|
@@ -4743,6 +4922,8 @@ var stack = (beat, ctx) => {
|
|
|
4743
4922
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4744
4923
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4745
4924
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
|
|
4925
|
+
const centre2 = (count - 1) / 2;
|
|
4926
|
+
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4746
4927
|
const first = 0.9;
|
|
4747
4928
|
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count));
|
|
4748
4929
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
@@ -4753,7 +4934,7 @@ var stack = (beat, ctx) => {
|
|
|
4753
4934
|
tl.push(
|
|
4754
4935
|
tween(
|
|
4755
4936
|
`#${sid}-lay${i}`,
|
|
4756
|
-
{ opacity: 0, y:
|
|
4937
|
+
{ opacity: 0, y: enterFrom(i) },
|
|
4757
4938
|
{ opacity: 1, y: 0, duration: 0.55, ease: "power2.out" },
|
|
4758
4939
|
at
|
|
4759
4940
|
)
|
|
@@ -4796,8 +4977,12 @@ var stack = (beat, ctx) => {
|
|
|
4796
4977
|
// The top plane is the focal point — last built, differently toned, and the
|
|
4797
4978
|
// one the final hold sits on. Its entrance owns `opacity` and `transform`,
|
|
4798
4979
|
// so the breath takes `filter`, the property nothing else writes.
|
|
4799
|
-
ambient(sid, `-lay${last}`, BREATHE)
|
|
4800
|
-
|
|
4980
|
+
ambient(sid, `-lay${last}`, BREATHE),
|
|
4981
|
+
// Absent unless the beat asked for it, so a flat stack emits the bytes it
|
|
4982
|
+
// always did.
|
|
4983
|
+
// `.stackwrap` and not the scene: the slabs lean, the headline does not.
|
|
4984
|
+
p.tilt ? depthCss(sid, { ...DEFAULT_POSE, rotateX: p.tilt }, ".stackwrap") : ""
|
|
4985
|
+
].filter(Boolean).join("\n")
|
|
4801
4986
|
};
|
|
4802
4987
|
};
|
|
4803
4988
|
|
|
@@ -4812,6 +4997,7 @@ var emitters = {
|
|
|
4812
4997
|
stack,
|
|
4813
4998
|
"split-compare": splitCompare,
|
|
4814
4999
|
"equation-walk": equationWalk,
|
|
5000
|
+
"equation-morph": equationMorph,
|
|
4815
5001
|
"line-chart": lineChart,
|
|
4816
5002
|
// The ones that describe.
|
|
4817
5003
|
title,
|
|
@@ -5004,6 +5190,7 @@ function round4(n3) {
|
|
|
5004
5190
|
// src/emit/composition.ts
|
|
5005
5191
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5006
5192
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5193
|
+
var MORPH_SRC = "./vendor/ds-morph.js";
|
|
5007
5194
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5008
5195
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5009
5196
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5094,6 +5281,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5094
5281
|
});
|
|
5095
5282
|
let start = 0;
|
|
5096
5283
|
let builds = false;
|
|
5284
|
+
const plugins = /* @__PURE__ */ new Set();
|
|
5097
5285
|
cuts.forEach((cut2, i) => {
|
|
5098
5286
|
const { beat, sid, dive, inside, duration } = cut2;
|
|
5099
5287
|
if (cut2.segments?.length) spoken[sid] = cut2.segments;
|
|
@@ -5108,6 +5296,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5108
5296
|
}
|
|
5109
5297
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5110
5298
|
if (scene.measure?.length) builds = true;
|
|
5299
|
+
for (const p of scene.plugins ?? []) plugins.add(p);
|
|
5111
5300
|
scenes.push(
|
|
5112
5301
|
sceneHtml(
|
|
5113
5302
|
sid,
|
|
@@ -5139,7 +5328,8 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5139
5328
|
spoken,
|
|
5140
5329
|
total: start,
|
|
5141
5330
|
cut,
|
|
5142
|
-
builds
|
|
5331
|
+
builds,
|
|
5332
|
+
plugins
|
|
5143
5333
|
};
|
|
5144
5334
|
}
|
|
5145
5335
|
function enteredParts(beats) {
|
|
@@ -5223,6 +5413,9 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5223
5413
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5224
5414
|
const island = format.navigable ? `
|
|
5225
5415
|
${emitIsland(slides)}` : "";
|
|
5416
|
+
const morph = laid.plugins.has("dsMorph") ? `
|
|
5417
|
+
<script src="${MORPH_SRC}"></script>
|
|
5418
|
+
<script>gsap.registerPlugin(DSMorphPlugin);</script>` : "";
|
|
5226
5419
|
return `<!doctype html>
|
|
5227
5420
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5228
5421
|
<head>
|
|
@@ -5231,7 +5424,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5231
5424
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5232
5425
|
<script src="${GSAP_SRC}"></script>
|
|
5233
5426
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5234
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script
|
|
5427
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${morph}
|
|
5235
5428
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5236
5429
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5237
5430
|
<style>
|
|
@@ -6138,6 +6331,7 @@ var REVEALS = {
|
|
|
6138
6331
|
title: "1",
|
|
6139
6332
|
"claim-figure": "2",
|
|
6140
6333
|
"equation-walk": "one per term",
|
|
6334
|
+
"equation-morph": "2",
|
|
6141
6335
|
"data-table": "one per highlighted row, plus 1",
|
|
6142
6336
|
"line-chart": "1",
|
|
6143
6337
|
callout: "one per panel",
|
|
@@ -6169,9 +6363,9 @@ A beat is one idea, one visual, one hold. It carries:
|
|
|
6169
6363
|
beat immediately before it. See RULE 11. Leave it off unless the
|
|
6170
6364
|
source itself puts one inside the other.
|
|
6171
6365
|
|
|
6172
|
-
THE
|
|
6366
|
+
THE THIRTEEN ARCHETYPES
|
|
6173
6367
|
|
|
6174
|
-
|
|
6368
|
+
Nine of them DRAW: they build a vector graphic out of the source's own content
|
|
6175
6369
|
and reveal it stage by stage, so the viewer watches the idea assemble. Four only
|
|
6176
6370
|
describe. The drawing ones are the default. The describing ones are what you
|
|
6177
6371
|
fall back to when a point genuinely has no shape.
|
|
@@ -6249,6 +6443,14 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
6249
6443
|
An equation quoted to back a claim someone else is making is
|
|
6250
6444
|
evidence under another archetype, not a beat of its own.
|
|
6251
6445
|
|
|
6446
|
+
equation-morph One equation becoming the next, the shared terms carried
|
|
6447
|
+
across. The tell: THE SOURCE DERIVES ONE LINE FROM ANOTHER \u2014 a
|
|
6448
|
+
substitution, a rearrangement, a special case \u2014 and the point
|
|
6449
|
+
is what moved. \`fromId\` and \`toId\` name two equations from
|
|
6450
|
+
the inventory. Each terms[].tex must appear verbatim in BOTH,
|
|
6451
|
+
and travels as one piece; a term in only one of them is
|
|
6452
|
+
dropped. Four terms maximum.
|
|
6453
|
+
|
|
6252
6454
|
line-chart A trend the source states numerically but does not plot. The
|
|
6253
6455
|
tell: A QUANTITY MOVING ALONG AN ORDERED AXIS \u2014 over length, over
|
|
6254
6456
|
scale, over training. Points come from the source's numbers;
|
|
@@ -6647,7 +6849,8 @@ function renderSource(source) {
|
|
|
6647
6849
|
out.push("", "== EQUATIONS ==");
|
|
6648
6850
|
for (const e of source.equations)
|
|
6649
6851
|
out.push(`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${e.tex}`);
|
|
6650
|
-
if (!source.equations.length)
|
|
6852
|
+
if (!source.equations.length)
|
|
6853
|
+
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
6651
6854
|
out.push("", "== TABLES ==");
|
|
6652
6855
|
for (const t2 of source.tables) {
|
|
6653
6856
|
out.push(`[table ${t2.id}] ${t2.caption ?? "(no caption)"}`);
|
|
@@ -6719,6 +6922,10 @@ function assertRefsResolve(storyboard, source, opts = {}) {
|
|
|
6719
6922
|
case "equation-walk":
|
|
6720
6923
|
check3(beat, "equation", beat.params.equationId, "params.equationId");
|
|
6721
6924
|
break;
|
|
6925
|
+
case "equation-morph":
|
|
6926
|
+
check3(beat, "equation", beat.params.fromId, "params.fromId");
|
|
6927
|
+
check3(beat, "equation", beat.params.toId, "params.toId");
|
|
6928
|
+
break;
|
|
6722
6929
|
case "data-table": {
|
|
6723
6930
|
check3(beat, "table", beat.params.tableId, "params.tableId");
|
|
6724
6931
|
const table = source.tables.find((t2) => t2.id === beat.params.tableId);
|
|
@@ -6855,7 +7062,31 @@ function stripNulls(node) {
|
|
|
6855
7062
|
}
|
|
6856
7063
|
return out;
|
|
6857
7064
|
}
|
|
6858
|
-
var
|
|
7065
|
+
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
7066
|
+
function hideFromPlanner(node) {
|
|
7067
|
+
if (Array.isArray(node)) return node.map(hideFromPlanner);
|
|
7068
|
+
if (node === null || typeof node !== "object") return node;
|
|
7069
|
+
const src = node;
|
|
7070
|
+
const out = {};
|
|
7071
|
+
for (const [key, value] of Object.entries(src)) {
|
|
7072
|
+
if (key === "properties" && value && typeof value === "object") {
|
|
7073
|
+
const kept = {};
|
|
7074
|
+
for (const [prop, sub] of Object.entries(value))
|
|
7075
|
+
if (!PLANNER_INVISIBLE.has(prop)) kept[prop] = hideFromPlanner(sub);
|
|
7076
|
+
out.properties = kept;
|
|
7077
|
+
continue;
|
|
7078
|
+
}
|
|
7079
|
+
if (key === "required" && Array.isArray(value)) {
|
|
7080
|
+
out.required = value.filter((r) => typeof r !== "string" || !PLANNER_INVISIBLE.has(r));
|
|
7081
|
+
continue;
|
|
7082
|
+
}
|
|
7083
|
+
out[key] = hideFromPlanner(value);
|
|
7084
|
+
}
|
|
7085
|
+
return out;
|
|
7086
|
+
}
|
|
7087
|
+
var SCHEMA = hideFromPlanner(
|
|
7088
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" }))
|
|
7089
|
+
);
|
|
6859
7090
|
async function codexPlanner(source, opts = {}) {
|
|
6860
7091
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
6861
7092
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
@@ -8061,6 +8292,178 @@ async function openDeck(dir, opts = {}) {
|
|
|
8061
8292
|
}
|
|
8062
8293
|
}
|
|
8063
8294
|
|
|
8295
|
+
// src/verify/typefloor.ts
|
|
8296
|
+
var TYPE_FLOOR_PX = 40;
|
|
8297
|
+
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
8298
|
+
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
8299
|
+
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
8300
|
+
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
8301
|
+
const findings = [];
|
|
8302
|
+
const zones = svgZones(html);
|
|
8303
|
+
const small = [];
|
|
8304
|
+
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
8305
|
+
pattern.lastIndex = 0;
|
|
8306
|
+
for (const m of html.matchAll(pattern)) {
|
|
8307
|
+
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
8308
|
+
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
if (small.length > 0) {
|
|
8312
|
+
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
8313
|
+
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
8314
|
+
findings.push({
|
|
8315
|
+
severity: "error",
|
|
8316
|
+
gate: "typography",
|
|
8317
|
+
rule: "type_below_floor",
|
|
8318
|
+
message: `${file} declares ${small.length} text size(s) under the ${floorPx}px floor: ${named}${worst.length > 6 ? ", \u2026" : ""}. Sizes are in reference space, so this is the floor as authored and not an artefact of the format's zoom. Raise the size, or give the archetype less to say \u2014 a beat that only fits below the floor is a beat that has to be cut in two.`
|
|
8319
|
+
});
|
|
8320
|
+
}
|
|
8321
|
+
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
8322
|
+
if (units.length > 0) {
|
|
8323
|
+
findings.push({
|
|
8324
|
+
severity: "warning",
|
|
8325
|
+
gate: "typography",
|
|
8326
|
+
rule: "type_unmeasurable",
|
|
8327
|
+
message: `${file} sizes some text in ${units.join("/")}, which resolves against an inherited size this scan cannot follow, so the ${floorPx}px floor was not checked there. Declare it in px, or measure it in a browser.`
|
|
8328
|
+
});
|
|
8329
|
+
}
|
|
8330
|
+
return findings;
|
|
8331
|
+
}
|
|
8332
|
+
function svgZones(html) {
|
|
8333
|
+
const zones = [];
|
|
8334
|
+
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
8335
|
+
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
8336
|
+
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
8337
|
+
const close = html.indexOf("</svg>", m.index);
|
|
8338
|
+
zones.push({
|
|
8339
|
+
start: m.index,
|
|
8340
|
+
end: close < 0 ? html.length : close,
|
|
8341
|
+
unit: width > 0 && box > 0 ? width / box : 1
|
|
8342
|
+
});
|
|
8343
|
+
}
|
|
8344
|
+
return zones;
|
|
8345
|
+
}
|
|
8346
|
+
function userUnit(zones, at) {
|
|
8347
|
+
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
8348
|
+
}
|
|
8349
|
+
function where(html, at) {
|
|
8350
|
+
const open = html.lastIndexOf("<", at);
|
|
8351
|
+
const closed = html.lastIndexOf(">", at);
|
|
8352
|
+
if (open > closed) {
|
|
8353
|
+
const end = html.indexOf(">", at);
|
|
8354
|
+
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
8355
|
+
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
8356
|
+
if (id2) return `#${id2}`;
|
|
8357
|
+
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
8358
|
+
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
8359
|
+
return cls ? `${name}.${cls}` : name;
|
|
8360
|
+
}
|
|
8361
|
+
const brace = html.lastIndexOf("{", at);
|
|
8362
|
+
if (brace < 0) return "the stylesheet";
|
|
8363
|
+
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
8364
|
+
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
8365
|
+
return selector ? selector : "the stylesheet";
|
|
8366
|
+
}
|
|
8367
|
+
function round6(px) {
|
|
8368
|
+
return Math.round(px * 100) / 100;
|
|
8369
|
+
}
|
|
8370
|
+
|
|
8371
|
+
// src/verify/apparent.ts
|
|
8372
|
+
function collectApparent(sid) {
|
|
8373
|
+
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
8374
|
+
if (!scene) return { runs: [], stage: 1 };
|
|
8375
|
+
const declaredW = Number(scene.dataset.width ?? 0);
|
|
8376
|
+
const sceneRect = scene.getBoundingClientRect();
|
|
8377
|
+
const stage = declaredW > 0 && sceneRect.width > 0 ? sceneRect.width / declaredW : 1;
|
|
8378
|
+
const runs = [];
|
|
8379
|
+
const walk = (el, inSvg) => {
|
|
8380
|
+
const style = getComputedStyle(el);
|
|
8381
|
+
if (style.display === "none" || style.visibility === "hidden") return;
|
|
8382
|
+
if (Number(style.opacity) === 0) return;
|
|
8383
|
+
const within = inSvg || el.tagName.toLowerCase() === "svg";
|
|
8384
|
+
if (within && typeof el.getBBox === "function") {
|
|
8385
|
+
let hasText = false;
|
|
8386
|
+
for (const node of Array.from(el.childNodes))
|
|
8387
|
+
if (node.nodeType === 3 && node.textContent?.trim()) hasText = true;
|
|
8388
|
+
if (hasText) {
|
|
8389
|
+
let box = null;
|
|
8390
|
+
try {
|
|
8391
|
+
box = el.getBBox();
|
|
8392
|
+
} catch {
|
|
8393
|
+
box = null;
|
|
8394
|
+
}
|
|
8395
|
+
const painted = el.getBoundingClientRect().height;
|
|
8396
|
+
if (box && box.height > 0 && painted > 0) {
|
|
8397
|
+
let opacity = 1;
|
|
8398
|
+
for (let node = el; node; node = node.parentElement) {
|
|
8399
|
+
const own = Number(getComputedStyle(node).opacity);
|
|
8400
|
+
if (Number.isFinite(own)) opacity *= own;
|
|
8401
|
+
if (node === scene) break;
|
|
8402
|
+
}
|
|
8403
|
+
runs.push({
|
|
8404
|
+
text: (el.textContent ?? "").trim().slice(0, 40),
|
|
8405
|
+
declared: Number.parseFloat(getComputedStyle(el).fontSize) || 0,
|
|
8406
|
+
ratio: painted / box.height,
|
|
8407
|
+
opacity
|
|
8408
|
+
});
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
for (const child of Array.from(el.children)) walk(child, within);
|
|
8413
|
+
};
|
|
8414
|
+
walk(scene, false);
|
|
8415
|
+
return { runs, stage };
|
|
8416
|
+
}
|
|
8417
|
+
function apparentPx(run4, stage) {
|
|
8418
|
+
if (stage <= 0) return run4.declared;
|
|
8419
|
+
return run4.declared * run4.ratio / stage;
|
|
8420
|
+
}
|
|
8421
|
+
var SETTLED_OPACITY = 0.95;
|
|
8422
|
+
function midpoints(stops) {
|
|
8423
|
+
const out = [];
|
|
8424
|
+
for (let i = 1; i < stops.length; i++) {
|
|
8425
|
+
const a = stops[i - 1];
|
|
8426
|
+
const b = stops[i];
|
|
8427
|
+
if (a.sid !== b.sid || b.t <= a.t) continue;
|
|
8428
|
+
out.push({ sid: b.sid, t: Math.round((a.t + b.t) / 2 * 1e3) / 1e3 });
|
|
8429
|
+
}
|
|
8430
|
+
return out;
|
|
8431
|
+
}
|
|
8432
|
+
function gradeApparent(stops, floor = TYPE_FLOOR_PX) {
|
|
8433
|
+
const worst = /* @__PURE__ */ new Map();
|
|
8434
|
+
for (const stop of stops) {
|
|
8435
|
+
for (const run4 of stop.runs) {
|
|
8436
|
+
if (!stop.settled && run4.opacity < SETTLED_OPACITY) continue;
|
|
8437
|
+
const px = apparentPx(run4, stop.stage);
|
|
8438
|
+
if (px >= floor - 0.1) continue;
|
|
8439
|
+
const seen = worst.get(run4.text);
|
|
8440
|
+
if (!seen || px < seen.px)
|
|
8441
|
+
worst.set(run4.text, { sid: stop.sid, t: stop.t, text: run4.text, px });
|
|
8442
|
+
}
|
|
8443
|
+
}
|
|
8444
|
+
if (worst.size === 0) return [];
|
|
8445
|
+
const byScene = /* @__PURE__ */ new Map();
|
|
8446
|
+
for (const row of worst.values()) {
|
|
8447
|
+
const list = byScene.get(row.sid) ?? [];
|
|
8448
|
+
list.push(row);
|
|
8449
|
+
byScene.set(row.sid, list);
|
|
8450
|
+
}
|
|
8451
|
+
return [...byScene].map(([sid, rows]) => {
|
|
8452
|
+
rows.sort((a, b) => a.px - b.px);
|
|
8453
|
+
const smallest = rows[0];
|
|
8454
|
+
const named = rows.slice(0, 3).map((r) => `"${r.text}" at ${Math.round(r.px * 10) / 10}px`).join(", ");
|
|
8455
|
+
return {
|
|
8456
|
+
severity: "error",
|
|
8457
|
+
gate: "apparent",
|
|
8458
|
+
rule: "apparent_type_floor",
|
|
8459
|
+
// `#${sid}` as a selector, like every other finding here: `scripts/sweep.mjs`
|
|
8460
|
+
// reads the selector back to decide which beat a finding belongs to, and a
|
|
8461
|
+
// bare id was once filed as a deck-level orphan while the beat reported clean.
|
|
8462
|
+
message: `#${sid} draws ${rows.length} text run(s) below the ${floor}px floor once the frame is rendered: ${named}, at t=${smallest.t}s. Their DECLARED sizes pass \u2014 something between the source and the glyph scales them down, a transform at a hold or a 3D projection. Invariant 5 is about what the audience sees, and the declared-size scan cannot see this.`
|
|
8463
|
+
};
|
|
8464
|
+
});
|
|
8465
|
+
}
|
|
8466
|
+
|
|
8064
8467
|
// src/verify/overprint.ts
|
|
8065
8468
|
var MIN_OVERLAP = 8;
|
|
8066
8469
|
function collectSvgTextRuns(sid) {
|
|
@@ -8344,6 +8747,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
8344
8747
|
const { page, height } = deck;
|
|
8345
8748
|
const measured = [];
|
|
8346
8749
|
const collided = [];
|
|
8750
|
+
const apparent = [];
|
|
8347
8751
|
for (const stop of stops) {
|
|
8348
8752
|
await deck.seek(stop.t);
|
|
8349
8753
|
const bandTopPx = await page.evaluate(
|
|
@@ -8356,6 +8760,11 @@ async function fidelity(dir, opts = {}) {
|
|
|
8356
8760
|
...stop,
|
|
8357
8761
|
pairs: overprints(await page.evaluate(collectSvgTextRuns, stop.sid))
|
|
8358
8762
|
});
|
|
8763
|
+
apparent.push({
|
|
8764
|
+
...stop,
|
|
8765
|
+
settled: true,
|
|
8766
|
+
...await page.evaluate(collectApparent, stop.sid)
|
|
8767
|
+
});
|
|
8359
8768
|
const frame = await decodePng(await deck.shoot());
|
|
8360
8769
|
measured.push({
|
|
8361
8770
|
...stop,
|
|
@@ -8363,9 +8772,17 @@ async function fidelity(dir, opts = {}) {
|
|
|
8363
8772
|
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
8364
8773
|
});
|
|
8365
8774
|
}
|
|
8775
|
+
for (const mid of midpoints(stops)) {
|
|
8776
|
+
await deck.seek(mid.t);
|
|
8777
|
+
apparent.push({ ...mid, settled: false, ...await page.evaluate(collectApparent, mid.sid) });
|
|
8778
|
+
}
|
|
8366
8779
|
return {
|
|
8367
8780
|
stops: measured,
|
|
8368
|
-
findings: [
|
|
8781
|
+
findings: [
|
|
8782
|
+
...gradeFidelity(measured, floor),
|
|
8783
|
+
...gradeOverprint(collided),
|
|
8784
|
+
...gradeApparent(apparent)
|
|
8785
|
+
],
|
|
8369
8786
|
elapsedMs: Date.now() - started
|
|
8370
8787
|
};
|
|
8371
8788
|
} catch (err) {
|
|
@@ -8376,82 +8793,6 @@ async function fidelity(dir, opts = {}) {
|
|
|
8376
8793
|
}
|
|
8377
8794
|
}
|
|
8378
8795
|
|
|
8379
|
-
// src/verify/typefloor.ts
|
|
8380
|
-
var TYPE_FLOOR_PX = 40;
|
|
8381
|
-
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
8382
|
-
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
8383
|
-
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
8384
|
-
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
8385
|
-
const findings = [];
|
|
8386
|
-
const zones = svgZones(html);
|
|
8387
|
-
const small = [];
|
|
8388
|
-
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
8389
|
-
pattern.lastIndex = 0;
|
|
8390
|
-
for (const m of html.matchAll(pattern)) {
|
|
8391
|
-
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
8392
|
-
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
8393
|
-
}
|
|
8394
|
-
}
|
|
8395
|
-
if (small.length > 0) {
|
|
8396
|
-
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
8397
|
-
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
8398
|
-
findings.push({
|
|
8399
|
-
severity: "error",
|
|
8400
|
-
gate: "typography",
|
|
8401
|
-
rule: "type_below_floor",
|
|
8402
|
-
message: `${file} declares ${small.length} text size(s) under the ${floorPx}px floor: ${named}${worst.length > 6 ? ", \u2026" : ""}. Sizes are in reference space, so this is the floor as authored and not an artefact of the format's zoom. Raise the size, or give the archetype less to say \u2014 a beat that only fits below the floor is a beat that has to be cut in two.`
|
|
8403
|
-
});
|
|
8404
|
-
}
|
|
8405
|
-
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
8406
|
-
if (units.length > 0) {
|
|
8407
|
-
findings.push({
|
|
8408
|
-
severity: "warning",
|
|
8409
|
-
gate: "typography",
|
|
8410
|
-
rule: "type_unmeasurable",
|
|
8411
|
-
message: `${file} sizes some text in ${units.join("/")}, which resolves against an inherited size this scan cannot follow, so the ${floorPx}px floor was not checked there. Declare it in px, or measure it in a browser.`
|
|
8412
|
-
});
|
|
8413
|
-
}
|
|
8414
|
-
return findings;
|
|
8415
|
-
}
|
|
8416
|
-
function svgZones(html) {
|
|
8417
|
-
const zones = [];
|
|
8418
|
-
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
8419
|
-
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
8420
|
-
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
8421
|
-
const close = html.indexOf("</svg>", m.index);
|
|
8422
|
-
zones.push({
|
|
8423
|
-
start: m.index,
|
|
8424
|
-
end: close < 0 ? html.length : close,
|
|
8425
|
-
unit: width > 0 && box > 0 ? width / box : 1
|
|
8426
|
-
});
|
|
8427
|
-
}
|
|
8428
|
-
return zones;
|
|
8429
|
-
}
|
|
8430
|
-
function userUnit(zones, at) {
|
|
8431
|
-
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
8432
|
-
}
|
|
8433
|
-
function where(html, at) {
|
|
8434
|
-
const open = html.lastIndexOf("<", at);
|
|
8435
|
-
const closed = html.lastIndexOf(">", at);
|
|
8436
|
-
if (open > closed) {
|
|
8437
|
-
const end = html.indexOf(">", at);
|
|
8438
|
-
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
8439
|
-
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
8440
|
-
if (id2) return `#${id2}`;
|
|
8441
|
-
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
8442
|
-
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
8443
|
-
return cls ? `${name}.${cls}` : name;
|
|
8444
|
-
}
|
|
8445
|
-
const brace = html.lastIndexOf("{", at);
|
|
8446
|
-
if (brace < 0) return "the stylesheet";
|
|
8447
|
-
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
8448
|
-
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
8449
|
-
return selector ? selector : "the stylesheet";
|
|
8450
|
-
}
|
|
8451
|
-
function round6(px) {
|
|
8452
|
-
return Math.round(px * 100) / 100;
|
|
8453
|
-
}
|
|
8454
|
-
|
|
8455
8796
|
// src/verify/drift.ts
|
|
8456
8797
|
import { execFile as execFile2 } from "node:child_process";
|
|
8457
8798
|
import { createHash as createHash7 } from "node:crypto";
|
|
@@ -10127,6 +10468,7 @@ export {
|
|
|
10127
10468
|
edgeProvider,
|
|
10128
10469
|
emitComposition,
|
|
10129
10470
|
emitDeck,
|
|
10471
|
+
equationMorphParamsSchema,
|
|
10130
10472
|
equationSchema,
|
|
10131
10473
|
equationWalkParamsSchema,
|
|
10132
10474
|
fetchFigures,
|