@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/cli.js
CHANGED
|
@@ -110,6 +110,13 @@ var equationWalkParamsSchema = z.object({
|
|
|
110
110
|
/** Walked in order, one hold-point each. */
|
|
111
111
|
terms: z.array(termSchema).min(1).max(4)
|
|
112
112
|
});
|
|
113
|
+
var equationMorphParamsSchema = z.object({
|
|
114
|
+
eyebrow: z.string().optional(),
|
|
115
|
+
headline: z.string(),
|
|
116
|
+
fromId: z.string(),
|
|
117
|
+
toId: z.string(),
|
|
118
|
+
terms: z.array(termSchema).min(1).max(4)
|
|
119
|
+
});
|
|
113
120
|
var dataTableParamsSchema = z.object({
|
|
114
121
|
eyebrow: z.string().optional(),
|
|
115
122
|
headline: z.string(),
|
|
@@ -238,7 +245,22 @@ var stackParamsSchema = z.object({
|
|
|
238
245
|
headline: z.string(),
|
|
239
246
|
/** Drawn bottom-up as offset planes, revealed in order. */
|
|
240
247
|
layers: z.array(z.object({ label: z.string(), note: z.string().optional() })).min(2).max(7),
|
|
241
|
-
note: z.string().optional()
|
|
248
|
+
note: z.string().optional(),
|
|
249
|
+
/**
|
|
250
|
+
* Tilt the slab stack away from the viewer, so the planes read as stacked in
|
|
251
|
+
* depth rather than merely offset up the page.
|
|
252
|
+
*
|
|
253
|
+
* OPTIONAL, and absent means flat — every storyboard written before this
|
|
254
|
+
* existed still parses, and every deck that does not ask for it emits exactly
|
|
255
|
+
* the bytes it did before.
|
|
256
|
+
*
|
|
257
|
+
* Degrees, and bounded at 18 rather than by taste: the tilt is paid for in
|
|
258
|
+
* declared type, because perspective shrinks the far half of the plane and
|
|
259
|
+
* invariant 5 is about what the audience SEES. At 18 degrees a 40px floor
|
|
260
|
+
* already needs 53.8px declared (`src/emit/depth.ts`), and past that a
|
|
261
|
+
* headline cannot spend enough and still fit its own line.
|
|
262
|
+
*/
|
|
263
|
+
tilt: z.number().min(0).max(18).optional()
|
|
242
264
|
});
|
|
243
265
|
var splitSideSchema = z.object({
|
|
244
266
|
label: z.string(),
|
|
@@ -303,6 +325,12 @@ var beatSchema = z.discriminatedUnion("archetype", [
|
|
|
303
325
|
params: equationWalkParamsSchema,
|
|
304
326
|
...beatTail
|
|
305
327
|
}),
|
|
328
|
+
z.object({
|
|
329
|
+
...beatCore,
|
|
330
|
+
archetype: z.literal("equation-morph"),
|
|
331
|
+
params: equationMorphParamsSchema,
|
|
332
|
+
...beatTail
|
|
333
|
+
}),
|
|
306
334
|
z.object({
|
|
307
335
|
...beatCore,
|
|
308
336
|
archetype: z.literal("data-table"),
|
|
@@ -357,6 +385,7 @@ var DIAGRAMMATIC = /* @__PURE__ */ new Set([
|
|
|
357
385
|
"stack",
|
|
358
386
|
"split-compare",
|
|
359
387
|
"equation-walk",
|
|
388
|
+
"equation-morph",
|
|
360
389
|
"line-chart"
|
|
361
390
|
]);
|
|
362
391
|
var ARCHETYPE_FAMILY = {
|
|
@@ -371,7 +400,8 @@ var ARCHETYPE_FAMILY = {
|
|
|
371
400
|
"bar-compare": "quantity",
|
|
372
401
|
"line-chart": "quantity",
|
|
373
402
|
"data-table": "quantity",
|
|
374
|
-
"equation-walk": "formal"
|
|
403
|
+
"equation-walk": "formal",
|
|
404
|
+
"equation-morph": "formal"
|
|
375
405
|
};
|
|
376
406
|
var storyboardSchema = z.object({
|
|
377
407
|
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 size3 = 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:${size3}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 step2 = 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) {
|
|
@@ -5220,6 +5410,9 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5220
5410
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5221
5411
|
const island = format.navigable ? `
|
|
5222
5412
|
${emitIsland(slides)}` : "";
|
|
5413
|
+
const morph = laid.plugins.has("dsMorph") ? `
|
|
5414
|
+
<script src="${MORPH_SRC}"></script>
|
|
5415
|
+
<script>gsap.registerPlugin(DSMorphPlugin);</script>` : "";
|
|
5223
5416
|
return `<!doctype html>
|
|
5224
5417
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5225
5418
|
<head>
|
|
@@ -5228,7 +5421,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5228
5421
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5229
5422
|
<script src="${GSAP_SRC}"></script>
|
|
5230
5423
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5231
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script
|
|
5424
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${morph}
|
|
5232
5425
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5233
5426
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5234
5427
|
<style>
|
|
@@ -5672,6 +5865,7 @@ var REVEALS = {
|
|
|
5672
5865
|
title: "1",
|
|
5673
5866
|
"claim-figure": "2",
|
|
5674
5867
|
"equation-walk": "one per term",
|
|
5868
|
+
"equation-morph": "2",
|
|
5675
5869
|
"data-table": "one per highlighted row, plus 1",
|
|
5676
5870
|
"line-chart": "1",
|
|
5677
5871
|
callout: "one per panel",
|
|
@@ -5703,9 +5897,9 @@ A beat is one idea, one visual, one hold. It carries:
|
|
|
5703
5897
|
beat immediately before it. See RULE 11. Leave it off unless the
|
|
5704
5898
|
source itself puts one inside the other.
|
|
5705
5899
|
|
|
5706
|
-
THE
|
|
5900
|
+
THE THIRTEEN ARCHETYPES
|
|
5707
5901
|
|
|
5708
|
-
|
|
5902
|
+
Nine of them DRAW: they build a vector graphic out of the source's own content
|
|
5709
5903
|
and reveal it stage by stage, so the viewer watches the idea assemble. Four only
|
|
5710
5904
|
describe. The drawing ones are the default. The describing ones are what you
|
|
5711
5905
|
fall back to when a point genuinely has no shape.
|
|
@@ -5783,6 +5977,14 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
5783
5977
|
An equation quoted to back a claim someone else is making is
|
|
5784
5978
|
evidence under another archetype, not a beat of its own.
|
|
5785
5979
|
|
|
5980
|
+
equation-morph One equation becoming the next, the shared terms carried
|
|
5981
|
+
across. The tell: THE SOURCE DERIVES ONE LINE FROM ANOTHER \u2014 a
|
|
5982
|
+
substitution, a rearrangement, a special case \u2014 and the point
|
|
5983
|
+
is what moved. \`fromId\` and \`toId\` name two equations from
|
|
5984
|
+
the inventory. Each terms[].tex must appear verbatim in BOTH,
|
|
5985
|
+
and travels as one piece; a term in only one of them is
|
|
5986
|
+
dropped. Four terms maximum.
|
|
5987
|
+
|
|
5786
5988
|
line-chart A trend the source states numerically but does not plot. The
|
|
5787
5989
|
tell: A QUANTITY MOVING ALONG AN ORDERED AXIS \u2014 over length, over
|
|
5788
5990
|
scale, over training. Points come from the source's numbers;
|
|
@@ -6181,7 +6383,8 @@ function renderSource(source) {
|
|
|
6181
6383
|
out.push("", "== EQUATIONS ==");
|
|
6182
6384
|
for (const e of source.equations)
|
|
6183
6385
|
out.push(`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${e.tex}`);
|
|
6184
|
-
if (!source.equations.length)
|
|
6386
|
+
if (!source.equations.length)
|
|
6387
|
+
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
6185
6388
|
out.push("", "== TABLES ==");
|
|
6186
6389
|
for (const t2 of source.tables) {
|
|
6187
6390
|
out.push(`[table ${t2.id}] ${t2.caption ?? "(no caption)"}`);
|
|
@@ -6253,6 +6456,10 @@ function assertRefsResolve(storyboard, source, opts = {}) {
|
|
|
6253
6456
|
case "equation-walk":
|
|
6254
6457
|
check3(beat, "equation", beat.params.equationId, "params.equationId");
|
|
6255
6458
|
break;
|
|
6459
|
+
case "equation-morph":
|
|
6460
|
+
check3(beat, "equation", beat.params.fromId, "params.fromId");
|
|
6461
|
+
check3(beat, "equation", beat.params.toId, "params.toId");
|
|
6462
|
+
break;
|
|
6256
6463
|
case "data-table": {
|
|
6257
6464
|
check3(beat, "table", beat.params.tableId, "params.tableId");
|
|
6258
6465
|
const table = source.tables.find((t2) => t2.id === beat.params.tableId);
|
|
@@ -6389,7 +6596,31 @@ function stripNulls(node) {
|
|
|
6389
6596
|
}
|
|
6390
6597
|
return out;
|
|
6391
6598
|
}
|
|
6392
|
-
var
|
|
6599
|
+
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
6600
|
+
function hideFromPlanner(node) {
|
|
6601
|
+
if (Array.isArray(node)) return node.map(hideFromPlanner);
|
|
6602
|
+
if (node === null || typeof node !== "object") return node;
|
|
6603
|
+
const src = node;
|
|
6604
|
+
const out = {};
|
|
6605
|
+
for (const [key, value] of Object.entries(src)) {
|
|
6606
|
+
if (key === "properties" && value && typeof value === "object") {
|
|
6607
|
+
const kept = {};
|
|
6608
|
+
for (const [prop, sub] of Object.entries(value))
|
|
6609
|
+
if (!PLANNER_INVISIBLE.has(prop)) kept[prop] = hideFromPlanner(sub);
|
|
6610
|
+
out.properties = kept;
|
|
6611
|
+
continue;
|
|
6612
|
+
}
|
|
6613
|
+
if (key === "required" && Array.isArray(value)) {
|
|
6614
|
+
out.required = value.filter((r) => typeof r !== "string" || !PLANNER_INVISIBLE.has(r));
|
|
6615
|
+
continue;
|
|
6616
|
+
}
|
|
6617
|
+
out[key] = hideFromPlanner(value);
|
|
6618
|
+
}
|
|
6619
|
+
return out;
|
|
6620
|
+
}
|
|
6621
|
+
var SCHEMA = hideFromPlanner(
|
|
6622
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" }))
|
|
6623
|
+
);
|
|
6393
6624
|
async function codexPlanner(source, opts = {}) {
|
|
6394
6625
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
6395
6626
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
@@ -8932,6 +9163,178 @@ function tail(s) {
|
|
|
8932
9163
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
8933
9164
|
import { join as join12 } from "node:path";
|
|
8934
9165
|
|
|
9166
|
+
// src/verify/typefloor.ts
|
|
9167
|
+
var TYPE_FLOOR_PX = 40;
|
|
9168
|
+
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
9169
|
+
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
9170
|
+
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
9171
|
+
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
9172
|
+
const findings = [];
|
|
9173
|
+
const zones = svgZones(html);
|
|
9174
|
+
const small = [];
|
|
9175
|
+
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
9176
|
+
pattern.lastIndex = 0;
|
|
9177
|
+
for (const m of html.matchAll(pattern)) {
|
|
9178
|
+
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
9179
|
+
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
if (small.length > 0) {
|
|
9183
|
+
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
9184
|
+
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
9185
|
+
findings.push({
|
|
9186
|
+
severity: "error",
|
|
9187
|
+
gate: "typography",
|
|
9188
|
+
rule: "type_below_floor",
|
|
9189
|
+
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.`
|
|
9190
|
+
});
|
|
9191
|
+
}
|
|
9192
|
+
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
9193
|
+
if (units.length > 0) {
|
|
9194
|
+
findings.push({
|
|
9195
|
+
severity: "warning",
|
|
9196
|
+
gate: "typography",
|
|
9197
|
+
rule: "type_unmeasurable",
|
|
9198
|
+
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.`
|
|
9199
|
+
});
|
|
9200
|
+
}
|
|
9201
|
+
return findings;
|
|
9202
|
+
}
|
|
9203
|
+
function svgZones(html) {
|
|
9204
|
+
const zones = [];
|
|
9205
|
+
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
9206
|
+
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
9207
|
+
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
9208
|
+
const close = html.indexOf("</svg>", m.index);
|
|
9209
|
+
zones.push({
|
|
9210
|
+
start: m.index,
|
|
9211
|
+
end: close < 0 ? html.length : close,
|
|
9212
|
+
unit: width > 0 && box > 0 ? width / box : 1
|
|
9213
|
+
});
|
|
9214
|
+
}
|
|
9215
|
+
return zones;
|
|
9216
|
+
}
|
|
9217
|
+
function userUnit(zones, at) {
|
|
9218
|
+
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
9219
|
+
}
|
|
9220
|
+
function where(html, at) {
|
|
9221
|
+
const open = html.lastIndexOf("<", at);
|
|
9222
|
+
const closed = html.lastIndexOf(">", at);
|
|
9223
|
+
if (open > closed) {
|
|
9224
|
+
const end = html.indexOf(">", at);
|
|
9225
|
+
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
9226
|
+
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
9227
|
+
if (id2) return `#${id2}`;
|
|
9228
|
+
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
9229
|
+
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
9230
|
+
return cls ? `${name}.${cls}` : name;
|
|
9231
|
+
}
|
|
9232
|
+
const brace = html.lastIndexOf("{", at);
|
|
9233
|
+
if (brace < 0) return "the stylesheet";
|
|
9234
|
+
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
9235
|
+
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
9236
|
+
return selector ? selector : "the stylesheet";
|
|
9237
|
+
}
|
|
9238
|
+
function round6(px) {
|
|
9239
|
+
return Math.round(px * 100) / 100;
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9242
|
+
// src/verify/apparent.ts
|
|
9243
|
+
function collectApparent(sid) {
|
|
9244
|
+
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
9245
|
+
if (!scene) return { runs: [], stage: 1 };
|
|
9246
|
+
const declaredW = Number(scene.dataset.width ?? 0);
|
|
9247
|
+
const sceneRect = scene.getBoundingClientRect();
|
|
9248
|
+
const stage = declaredW > 0 && sceneRect.width > 0 ? sceneRect.width / declaredW : 1;
|
|
9249
|
+
const runs = [];
|
|
9250
|
+
const walk = (el, inSvg) => {
|
|
9251
|
+
const style = getComputedStyle(el);
|
|
9252
|
+
if (style.display === "none" || style.visibility === "hidden") return;
|
|
9253
|
+
if (Number(style.opacity) === 0) return;
|
|
9254
|
+
const within = inSvg || el.tagName.toLowerCase() === "svg";
|
|
9255
|
+
if (within && typeof el.getBBox === "function") {
|
|
9256
|
+
let hasText = false;
|
|
9257
|
+
for (const node of Array.from(el.childNodes))
|
|
9258
|
+
if (node.nodeType === 3 && node.textContent?.trim()) hasText = true;
|
|
9259
|
+
if (hasText) {
|
|
9260
|
+
let box = null;
|
|
9261
|
+
try {
|
|
9262
|
+
box = el.getBBox();
|
|
9263
|
+
} catch {
|
|
9264
|
+
box = null;
|
|
9265
|
+
}
|
|
9266
|
+
const painted = el.getBoundingClientRect().height;
|
|
9267
|
+
if (box && box.height > 0 && painted > 0) {
|
|
9268
|
+
let opacity = 1;
|
|
9269
|
+
for (let node = el; node; node = node.parentElement) {
|
|
9270
|
+
const own = Number(getComputedStyle(node).opacity);
|
|
9271
|
+
if (Number.isFinite(own)) opacity *= own;
|
|
9272
|
+
if (node === scene) break;
|
|
9273
|
+
}
|
|
9274
|
+
runs.push({
|
|
9275
|
+
text: (el.textContent ?? "").trim().slice(0, 40),
|
|
9276
|
+
declared: Number.parseFloat(getComputedStyle(el).fontSize) || 0,
|
|
9277
|
+
ratio: painted / box.height,
|
|
9278
|
+
opacity
|
|
9279
|
+
});
|
|
9280
|
+
}
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
9283
|
+
for (const child of Array.from(el.children)) walk(child, within);
|
|
9284
|
+
};
|
|
9285
|
+
walk(scene, false);
|
|
9286
|
+
return { runs, stage };
|
|
9287
|
+
}
|
|
9288
|
+
function apparentPx(run4, stage) {
|
|
9289
|
+
if (stage <= 0) return run4.declared;
|
|
9290
|
+
return run4.declared * run4.ratio / stage;
|
|
9291
|
+
}
|
|
9292
|
+
var SETTLED_OPACITY = 0.95;
|
|
9293
|
+
function midpoints(stops) {
|
|
9294
|
+
const out = [];
|
|
9295
|
+
for (let i = 1; i < stops.length; i++) {
|
|
9296
|
+
const a = stops[i - 1];
|
|
9297
|
+
const b = stops[i];
|
|
9298
|
+
if (a.sid !== b.sid || b.t <= a.t) continue;
|
|
9299
|
+
out.push({ sid: b.sid, t: Math.round((a.t + b.t) / 2 * 1e3) / 1e3 });
|
|
9300
|
+
}
|
|
9301
|
+
return out;
|
|
9302
|
+
}
|
|
9303
|
+
function gradeApparent(stops, floor = TYPE_FLOOR_PX) {
|
|
9304
|
+
const worst = /* @__PURE__ */ new Map();
|
|
9305
|
+
for (const stop of stops) {
|
|
9306
|
+
for (const run4 of stop.runs) {
|
|
9307
|
+
if (!stop.settled && run4.opacity < SETTLED_OPACITY) continue;
|
|
9308
|
+
const px = apparentPx(run4, stop.stage);
|
|
9309
|
+
if (px >= floor - 0.1) continue;
|
|
9310
|
+
const seen = worst.get(run4.text);
|
|
9311
|
+
if (!seen || px < seen.px)
|
|
9312
|
+
worst.set(run4.text, { sid: stop.sid, t: stop.t, text: run4.text, px });
|
|
9313
|
+
}
|
|
9314
|
+
}
|
|
9315
|
+
if (worst.size === 0) return [];
|
|
9316
|
+
const byScene = /* @__PURE__ */ new Map();
|
|
9317
|
+
for (const row of worst.values()) {
|
|
9318
|
+
const list = byScene.get(row.sid) ?? [];
|
|
9319
|
+
list.push(row);
|
|
9320
|
+
byScene.set(row.sid, list);
|
|
9321
|
+
}
|
|
9322
|
+
return [...byScene].map(([sid, rows]) => {
|
|
9323
|
+
rows.sort((a, b) => a.px - b.px);
|
|
9324
|
+
const smallest = rows[0];
|
|
9325
|
+
const named = rows.slice(0, 3).map((r) => `"${r.text}" at ${Math.round(r.px * 10) / 10}px`).join(", ");
|
|
9326
|
+
return {
|
|
9327
|
+
severity: "error",
|
|
9328
|
+
gate: "apparent",
|
|
9329
|
+
rule: "apparent_type_floor",
|
|
9330
|
+
// `#${sid}` as a selector, like every other finding here: `scripts/sweep.mjs`
|
|
9331
|
+
// reads the selector back to decide which beat a finding belongs to, and a
|
|
9332
|
+
// bare id was once filed as a deck-level orphan while the beat reported clean.
|
|
9333
|
+
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.`
|
|
9334
|
+
};
|
|
9335
|
+
});
|
|
9336
|
+
}
|
|
9337
|
+
|
|
8935
9338
|
// src/verify/overprint.ts
|
|
8936
9339
|
var MIN_OVERLAP = 8;
|
|
8937
9340
|
function collectSvgTextRuns(sid) {
|
|
@@ -9215,6 +9618,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
9215
9618
|
const { page, height } = deck;
|
|
9216
9619
|
const measured = [];
|
|
9217
9620
|
const collided = [];
|
|
9621
|
+
const apparent = [];
|
|
9218
9622
|
for (const stop of stops) {
|
|
9219
9623
|
await deck.seek(stop.t);
|
|
9220
9624
|
const bandTopPx = await page.evaluate(
|
|
@@ -9227,6 +9631,11 @@ async function fidelity(dir, opts = {}) {
|
|
|
9227
9631
|
...stop,
|
|
9228
9632
|
pairs: overprints(await page.evaluate(collectSvgTextRuns, stop.sid))
|
|
9229
9633
|
});
|
|
9634
|
+
apparent.push({
|
|
9635
|
+
...stop,
|
|
9636
|
+
settled: true,
|
|
9637
|
+
...await page.evaluate(collectApparent, stop.sid)
|
|
9638
|
+
});
|
|
9230
9639
|
const frame = await decodePng(await deck.shoot());
|
|
9231
9640
|
measured.push({
|
|
9232
9641
|
...stop,
|
|
@@ -9234,9 +9643,17 @@ async function fidelity(dir, opts = {}) {
|
|
|
9234
9643
|
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
9235
9644
|
});
|
|
9236
9645
|
}
|
|
9646
|
+
for (const mid of midpoints(stops)) {
|
|
9647
|
+
await deck.seek(mid.t);
|
|
9648
|
+
apparent.push({ ...mid, settled: false, ...await page.evaluate(collectApparent, mid.sid) });
|
|
9649
|
+
}
|
|
9237
9650
|
return {
|
|
9238
9651
|
stops: measured,
|
|
9239
|
-
findings: [
|
|
9652
|
+
findings: [
|
|
9653
|
+
...gradeFidelity(measured, floor),
|
|
9654
|
+
...gradeOverprint(collided),
|
|
9655
|
+
...gradeApparent(apparent)
|
|
9656
|
+
],
|
|
9240
9657
|
elapsedMs: Date.now() - started
|
|
9241
9658
|
};
|
|
9242
9659
|
} catch (err) {
|
|
@@ -9247,82 +9664,6 @@ async function fidelity(dir, opts = {}) {
|
|
|
9247
9664
|
}
|
|
9248
9665
|
}
|
|
9249
9666
|
|
|
9250
|
-
// src/verify/typefloor.ts
|
|
9251
|
-
var TYPE_FLOOR_PX = 40;
|
|
9252
|
-
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
9253
|
-
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
9254
|
-
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
9255
|
-
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
9256
|
-
const findings = [];
|
|
9257
|
-
const zones = svgZones(html);
|
|
9258
|
-
const small = [];
|
|
9259
|
-
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
9260
|
-
pattern.lastIndex = 0;
|
|
9261
|
-
for (const m of html.matchAll(pattern)) {
|
|
9262
|
-
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
9263
|
-
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
9264
|
-
}
|
|
9265
|
-
}
|
|
9266
|
-
if (small.length > 0) {
|
|
9267
|
-
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
9268
|
-
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
9269
|
-
findings.push({
|
|
9270
|
-
severity: "error",
|
|
9271
|
-
gate: "typography",
|
|
9272
|
-
rule: "type_below_floor",
|
|
9273
|
-
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.`
|
|
9274
|
-
});
|
|
9275
|
-
}
|
|
9276
|
-
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
9277
|
-
if (units.length > 0) {
|
|
9278
|
-
findings.push({
|
|
9279
|
-
severity: "warning",
|
|
9280
|
-
gate: "typography",
|
|
9281
|
-
rule: "type_unmeasurable",
|
|
9282
|
-
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.`
|
|
9283
|
-
});
|
|
9284
|
-
}
|
|
9285
|
-
return findings;
|
|
9286
|
-
}
|
|
9287
|
-
function svgZones(html) {
|
|
9288
|
-
const zones = [];
|
|
9289
|
-
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
9290
|
-
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
9291
|
-
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
9292
|
-
const close = html.indexOf("</svg>", m.index);
|
|
9293
|
-
zones.push({
|
|
9294
|
-
start: m.index,
|
|
9295
|
-
end: close < 0 ? html.length : close,
|
|
9296
|
-
unit: width > 0 && box > 0 ? width / box : 1
|
|
9297
|
-
});
|
|
9298
|
-
}
|
|
9299
|
-
return zones;
|
|
9300
|
-
}
|
|
9301
|
-
function userUnit(zones, at) {
|
|
9302
|
-
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
9303
|
-
}
|
|
9304
|
-
function where(html, at) {
|
|
9305
|
-
const open = html.lastIndexOf("<", at);
|
|
9306
|
-
const closed = html.lastIndexOf(">", at);
|
|
9307
|
-
if (open > closed) {
|
|
9308
|
-
const end = html.indexOf(">", at);
|
|
9309
|
-
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
9310
|
-
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
9311
|
-
if (id2) return `#${id2}`;
|
|
9312
|
-
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
9313
|
-
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
9314
|
-
return cls ? `${name}.${cls}` : name;
|
|
9315
|
-
}
|
|
9316
|
-
const brace = html.lastIndexOf("{", at);
|
|
9317
|
-
if (brace < 0) return "the stylesheet";
|
|
9318
|
-
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
9319
|
-
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
9320
|
-
return selector ? selector : "the stylesheet";
|
|
9321
|
-
}
|
|
9322
|
-
function round6(px) {
|
|
9323
|
-
return Math.round(px * 100) / 100;
|
|
9324
|
-
}
|
|
9325
|
-
|
|
9326
9667
|
// src/verify/drift.ts
|
|
9327
9668
|
import { execFile as execFile3 } from "node:child_process";
|
|
9328
9669
|
import { createHash as createHash8 } from "node:crypto";
|
|
@@ -10023,6 +10364,10 @@ async function vendorScripts(out) {
|
|
|
10023
10364
|
const from = join15(dirname4(require2.resolve(pkg)), rel);
|
|
10024
10365
|
await cp(from, join15(out, "vendor", name));
|
|
10025
10366
|
}
|
|
10367
|
+
await cp(
|
|
10368
|
+
fileURLToPath(new URL("./ds-morph.js", import.meta.url)),
|
|
10369
|
+
join15(out, "vendor", "ds-morph.js")
|
|
10370
|
+
);
|
|
10026
10371
|
}
|
|
10027
10372
|
var HYPERFRAMES_JSON = `${JSON.stringify(
|
|
10028
10373
|
{
|