@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/mcp.js
CHANGED
|
@@ -127,6 +127,13 @@ var equationWalkParamsSchema = z.object({
|
|
|
127
127
|
/** Walked in order, one hold-point each. */
|
|
128
128
|
terms: z.array(termSchema).min(1).max(4)
|
|
129
129
|
});
|
|
130
|
+
var equationMorphParamsSchema = z.object({
|
|
131
|
+
eyebrow: z.string().optional(),
|
|
132
|
+
headline: z.string(),
|
|
133
|
+
fromId: z.string(),
|
|
134
|
+
toId: z.string(),
|
|
135
|
+
terms: z.array(termSchema).min(1).max(4)
|
|
136
|
+
});
|
|
130
137
|
var dataTableParamsSchema = z.object({
|
|
131
138
|
eyebrow: z.string().optional(),
|
|
132
139
|
headline: z.string(),
|
|
@@ -255,7 +262,22 @@ var stackParamsSchema = z.object({
|
|
|
255
262
|
headline: z.string(),
|
|
256
263
|
/** Drawn bottom-up as offset planes, revealed in order. */
|
|
257
264
|
layers: z.array(z.object({ label: z.string(), note: z.string().optional() })).min(2).max(7),
|
|
258
|
-
note: z.string().optional()
|
|
265
|
+
note: z.string().optional(),
|
|
266
|
+
/**
|
|
267
|
+
* Tilt the slab stack away from the viewer, so the planes read as stacked in
|
|
268
|
+
* depth rather than merely offset up the page.
|
|
269
|
+
*
|
|
270
|
+
* OPTIONAL, and absent means flat — every storyboard written before this
|
|
271
|
+
* existed still parses, and every deck that does not ask for it emits exactly
|
|
272
|
+
* the bytes it did before.
|
|
273
|
+
*
|
|
274
|
+
* Degrees, and bounded at 18 rather than by taste: the tilt is paid for in
|
|
275
|
+
* declared type, because perspective shrinks the far half of the plane and
|
|
276
|
+
* invariant 5 is about what the audience SEES. At 18 degrees a 40px floor
|
|
277
|
+
* already needs 53.8px declared (`src/emit/depth.ts`), and past that a
|
|
278
|
+
* headline cannot spend enough and still fit its own line.
|
|
279
|
+
*/
|
|
280
|
+
tilt: z.number().min(0).max(18).optional()
|
|
259
281
|
});
|
|
260
282
|
var splitSideSchema = z.object({
|
|
261
283
|
label: z.string(),
|
|
@@ -320,6 +342,12 @@ var beatSchema = z.discriminatedUnion("archetype", [
|
|
|
320
342
|
params: equationWalkParamsSchema,
|
|
321
343
|
...beatTail
|
|
322
344
|
}),
|
|
345
|
+
z.object({
|
|
346
|
+
...beatCore,
|
|
347
|
+
archetype: z.literal("equation-morph"),
|
|
348
|
+
params: equationMorphParamsSchema,
|
|
349
|
+
...beatTail
|
|
350
|
+
}),
|
|
323
351
|
z.object({
|
|
324
352
|
...beatCore,
|
|
325
353
|
archetype: z.literal("data-table"),
|
|
@@ -377,7 +405,8 @@ var ARCHETYPE_FAMILY = {
|
|
|
377
405
|
"bar-compare": "quantity",
|
|
378
406
|
"line-chart": "quantity",
|
|
379
407
|
"data-table": "quantity",
|
|
380
|
-
"equation-walk": "formal"
|
|
408
|
+
"equation-walk": "formal",
|
|
409
|
+
"equation-morph": "formal"
|
|
381
410
|
};
|
|
382
411
|
var storyboardSchema = z.object({
|
|
383
412
|
sourceId: z.string(),
|
|
@@ -3082,7 +3111,7 @@ function locate(tex, term) {
|
|
|
3082
3111
|
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
3083
3112
|
return { start, end: Math.min(end, tex.length) };
|
|
3084
3113
|
}
|
|
3085
|
-
function wrapTerms(tex, terms, beatId) {
|
|
3114
|
+
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
3086
3115
|
let parts = [{ text: tex, raw: true }];
|
|
3087
3116
|
const used = [];
|
|
3088
3117
|
const missing = [];
|
|
@@ -3098,7 +3127,7 @@ function wrapTerms(tex, terms, beatId) {
|
|
|
3098
3127
|
1,
|
|
3099
3128
|
{ text: part.text.slice(0, at.start), raw: true },
|
|
3100
3129
|
{
|
|
3101
|
-
text: `\\htmlClass{
|
|
3130
|
+
text: `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`,
|
|
3102
3131
|
raw: false
|
|
3103
3132
|
},
|
|
3104
3133
|
{ text: part.text.slice(at.end), raw: true }
|
|
@@ -3131,6 +3160,24 @@ function statements(tex, stacked) {
|
|
|
3131
3160
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
3132
3161
|
return parts.length > 0 ? parts : [tex];
|
|
3133
3162
|
}
|
|
3163
|
+
function legendRows(sid, terms, theme) {
|
|
3164
|
+
return terms.map(
|
|
3165
|
+
(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>`
|
|
3166
|
+
).join("\n ");
|
|
3167
|
+
}
|
|
3168
|
+
function legendCss(theme) {
|
|
3169
|
+
return [
|
|
3170
|
+
// `width:fit-content` + auto margins, not `align-items:center`: centring
|
|
3171
|
+
// each row individually gave the legend a ragged left edge, because a short
|
|
3172
|
+
// label indented its own chip further than a long one did. The column is
|
|
3173
|
+
// centred as one block and the rows start on a shared spine.
|
|
3174
|
+
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3175
|
+
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3176
|
+
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3177
|
+
// are one symbol each and their natural widths differ by a few pixels.
|
|
3178
|
+
`.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}`
|
|
3179
|
+
].join("\n");
|
|
3180
|
+
}
|
|
3134
3181
|
var equationWalk = (beat, ctx) => {
|
|
3135
3182
|
const { sid, theme } = ctx;
|
|
3136
3183
|
const p = beat.params;
|
|
@@ -3142,9 +3189,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3142
3189
|
}
|
|
3143
3190
|
const walk = wrapTerms(eq.tex, p.terms, beat.id);
|
|
3144
3191
|
const terms = walk.used;
|
|
3145
|
-
const legend = terms
|
|
3146
|
-
(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>`
|
|
3147
|
-
).join("\n ");
|
|
3192
|
+
const legend = legendRows(sid, terms, theme);
|
|
3148
3193
|
const stacked = isPortrait(ctx.format);
|
|
3149
3194
|
const raw2 = statements(eq.tex, stacked);
|
|
3150
3195
|
const shown = statements(walk.tex, stacked);
|
|
@@ -3243,15 +3288,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3243
3288
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
3244
3289
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
3245
3290
|
".term{display:inline-block}",
|
|
3246
|
-
|
|
3247
|
-
// each row individually gave the legend a ragged left edge, because a short
|
|
3248
|
-
// label indented its own chip further than a long one did. The column is
|
|
3249
|
-
// centred as one block and the rows start on a shared spine.
|
|
3250
|
-
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3251
|
-
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3252
|
-
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3253
|
-
// are one symbol each and their natural widths differ by a few pixels.
|
|
3254
|
-
`.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}`,
|
|
3291
|
+
legendCss(theme),
|
|
3255
3292
|
// The block, not the term under discussion: which term that is, is a fact
|
|
3256
3293
|
// about the paused timeline, and CSS cannot see it. The terms are also the
|
|
3257
3294
|
// one thing here GSAP tints and swells, so a rule on them would win the
|
|
@@ -3261,6 +3298,117 @@ var equationWalk = (beat, ctx) => {
|
|
|
3261
3298
|
};
|
|
3262
3299
|
};
|
|
3263
3300
|
|
|
3301
|
+
// src/emit/archetypes/equation-morph.ts
|
|
3302
|
+
var MORPH_SECONDS = 1.6;
|
|
3303
|
+
var equationMorph = (beat, ctx) => {
|
|
3304
|
+
const { sid, theme } = ctx;
|
|
3305
|
+
const p = beat.params;
|
|
3306
|
+
const find2 = (id2) => {
|
|
3307
|
+
const eq = ctx.source.equations.find((e) => e.id === id2);
|
|
3308
|
+
if (!eq)
|
|
3309
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${ctx.source.id}`);
|
|
3310
|
+
return eq;
|
|
3311
|
+
};
|
|
3312
|
+
const a = find2(p.fromId);
|
|
3313
|
+
const b = find2(p.toId);
|
|
3314
|
+
const both = p.terms.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
3315
|
+
if (both.length === 0) {
|
|
3316
|
+
throw new Error(
|
|
3317
|
+
`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)}`
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
3321
|
+
const wa = wrapTerms(a.tex, both, beat.id, cls).tex;
|
|
3322
|
+
const wb = wrapTerms(b.tex, both, beat.id, cls).tex;
|
|
3323
|
+
const size = Math.max(
|
|
3324
|
+
MIN_FONT,
|
|
3325
|
+
Math.min(
|
|
3326
|
+
equationSize(a.tex.length > b.tex.length ? a.tex : b.tex),
|
|
3327
|
+
Math.floor(contentW(ctx.format) / Math.max(texUnits(a.tex), texUnits(b.tex)))
|
|
3328
|
+
)
|
|
3329
|
+
);
|
|
3330
|
+
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
3331
|
+
<div class="eqslide">
|
|
3332
|
+
<div class="morph" id="${sid}-morph" style="font-size:${size}px">
|
|
3333
|
+
<div class="side" data-morph="a" id="${sid}-eqa"></div>
|
|
3334
|
+
<div class="side" data-morph="b" id="${sid}-eqb"></div>
|
|
3335
|
+
</div>
|
|
3336
|
+
<div class="legend">
|
|
3337
|
+
${legendRows(sid, both, theme)}
|
|
3338
|
+
</div>
|
|
3339
|
+
</div>`;
|
|
3340
|
+
const setup = [
|
|
3341
|
+
`var OPTS = ${OPTS};`,
|
|
3342
|
+
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
3343
|
+
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
3344
|
+
...both.map(
|
|
3345
|
+
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
3346
|
+
)
|
|
3347
|
+
];
|
|
3348
|
+
const first = 1.8;
|
|
3349
|
+
const at = Math.round(
|
|
3350
|
+
Math.max(2.6, Math.min(beat.seconds - MORPH_SECONDS - 0.9, beat.seconds * 0.45)) * 100
|
|
3351
|
+
) / 100;
|
|
3352
|
+
const tl = [
|
|
3353
|
+
...chromeIn(sid, p.eyebrow !== void 0),
|
|
3354
|
+
tween(`#${sid}-morph`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8),
|
|
3355
|
+
...both.map(
|
|
3356
|
+
(t2, i) => tween(
|
|
3357
|
+
`#${sid}-leg-${t2.tone}`,
|
|
3358
|
+
{ opacity: 0, x: -18 },
|
|
3359
|
+
{ opacity: 1, x: 0, duration: 0.5 },
|
|
3360
|
+
1 + i * 0.15
|
|
3361
|
+
)
|
|
3362
|
+
),
|
|
3363
|
+
// ONE tween, on the host, driving the plugin. Its ease is "none" because the
|
|
3364
|
+
// plan carries its own eases per segment; `pace` scales this duration and
|
|
3365
|
+
// the plan, being in fractions of it, scales with it.
|
|
3366
|
+
tween(
|
|
3367
|
+
`#${sid}-morph`,
|
|
3368
|
+
{ dsMorph: 0 },
|
|
3369
|
+
{ dsMorph: 1, duration: MORPH_SECONDS, ease: "none" },
|
|
3370
|
+
at
|
|
3371
|
+
)
|
|
3372
|
+
];
|
|
3373
|
+
return {
|
|
3374
|
+
html,
|
|
3375
|
+
tl,
|
|
3376
|
+
setup,
|
|
3377
|
+
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
3378
|
+
// the ready gate, and the plugin tween above finds it on the host.
|
|
3379
|
+
measure: [`DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
3380
|
+
plugins: ["dsMorph"],
|
|
3381
|
+
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
3382
|
+
css: [
|
|
3383
|
+
chromeCss(theme),
|
|
3384
|
+
".eqslide{display:flex;flex-direction:column;justify-content:space-evenly;gap:64px;flex:1;min-height:0}",
|
|
3385
|
+
".katex-display{margin:0 !important}",
|
|
3386
|
+
// Both sides in one grid cell, so the host is as tall as the taller line
|
|
3387
|
+
// and neither needs a guessed height; the overlay is absolute over it.
|
|
3388
|
+
// Padded by the room an arc needs, so a bowing glyph stays inside its
|
|
3389
|
+
// offset parent and the layout gate's `escaped_container` stays quiet; the
|
|
3390
|
+
// bow is capped to the same 0.8em in `plan`.
|
|
3391
|
+
`.morph{position:relative;display:grid;place-items:center;text-align:center;padding:0.8em 0.5em;color:${theme.fg}}`,
|
|
3392
|
+
".side{grid-area:1/1}",
|
|
3393
|
+
// B is measured, never seen: the runtime lifts its glyphs into the overlay
|
|
3394
|
+
// and drives them from there. Hidden by the sheet so nothing is captured
|
|
3395
|
+
// before the gate has built the plan.
|
|
3396
|
+
'.side[data-morph="b"]{visibility:hidden}',
|
|
3397
|
+
".ds-morph-layer{position:absolute;inset:0}",
|
|
3398
|
+
".term{display:inline-block}",
|
|
3399
|
+
// Keys are tinted from the start, on BOTH lines — the colour is what lets
|
|
3400
|
+
// a viewer follow a body across the move. Scoped under `.morph` because
|
|
3401
|
+
// `equation-walk` tweens `.t-<tone>` from the foreground colour, and a
|
|
3402
|
+
// bare rule on the class would win that cascade and cancel its walk.
|
|
3403
|
+
...["a", "b", "c", "d"].map(
|
|
3404
|
+
(tone2) => `.morph .t-${tone2}{color:${theme.tones[tone2]}}`
|
|
3405
|
+
),
|
|
3406
|
+
legendCss(theme),
|
|
3407
|
+
ambient(sid, "-morph", BREATHE)
|
|
3408
|
+
].join("\n")
|
|
3409
|
+
};
|
|
3410
|
+
};
|
|
3411
|
+
|
|
3264
3412
|
// src/emit/archetypes/grid.ts
|
|
3265
3413
|
var LABEL = 42;
|
|
3266
3414
|
var LH = 1.25;
|
|
@@ -4544,6 +4692,30 @@ var splitCompare = (beat, ctx) => {
|
|
|
4544
4692
|
};
|
|
4545
4693
|
};
|
|
4546
4694
|
|
|
4695
|
+
// src/emit/depth.ts
|
|
4696
|
+
var DEFAULT_POSE = { rotateX: 12, perspective: 1400 };
|
|
4697
|
+
function scaleAt(pose, dy) {
|
|
4698
|
+
const t2 = pose.rotateX * Math.PI / 180;
|
|
4699
|
+
const denom = pose.perspective - dy * Math.sin(t2);
|
|
4700
|
+
if (denom <= 0) return 0;
|
|
4701
|
+
return Math.cos(t2) * (pose.perspective / denom) ** 2;
|
|
4702
|
+
}
|
|
4703
|
+
var MODEL_SLACK = 0.98;
|
|
4704
|
+
function worstScale(pose, height) {
|
|
4705
|
+
if (scaleAt(pose, height / 2) <= 0) return 0;
|
|
4706
|
+
return scaleAt(pose, -height / 2) * MODEL_SLACK;
|
|
4707
|
+
}
|
|
4708
|
+
function tiltedFloor(pose, height, floor) {
|
|
4709
|
+
const s = worstScale(pose, height);
|
|
4710
|
+
return s > 0 ? floor / s : Number.POSITIVE_INFINITY;
|
|
4711
|
+
}
|
|
4712
|
+
function depthCss(sid, pose, part) {
|
|
4713
|
+
return [
|
|
4714
|
+
`#${sid} { perspective: ${pose.perspective}px; }`,
|
|
4715
|
+
`#${sid} ${part} { transform: rotateX(${pose.rotateX}deg); transform-origin: 50% 50%; }`
|
|
4716
|
+
].join("\n");
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4547
4719
|
// src/emit/archetypes/stack.ts
|
|
4548
4720
|
var GAP3 = 44;
|
|
4549
4721
|
var NUM_X = 48;
|
|
@@ -4567,15 +4739,20 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4567
4739
|
function labelWeight(i, count) {
|
|
4568
4740
|
return i === count - 1 ? 700 : 600;
|
|
4569
4741
|
}
|
|
4742
|
+
function floorFor(p, format) {
|
|
4743
|
+
if (!p.tilt) return MIN_FONT;
|
|
4744
|
+
return tiltedFloor({ ...DEFAULT_POSE, rotateX: p.tilt }, contentH(format), MIN_FONT);
|
|
4745
|
+
}
|
|
4570
4746
|
function solve2(p, format, inline, face) {
|
|
4571
4747
|
const width = contentW(format);
|
|
4572
4748
|
const boxH = contentH(format);
|
|
4749
|
+
const floor = floorFor(p, format);
|
|
4573
4750
|
const count = p.layers.length;
|
|
4574
4751
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4575
4752
|
const riseMax = RISE_MAX[k];
|
|
4576
4753
|
const syMax = SY_MAX[k];
|
|
4577
4754
|
const tMax = T_MAX[k];
|
|
4578
|
-
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note,
|
|
4755
|
+
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4579
4756
|
const want = Math.max(
|
|
4580
4757
|
...p.layers.map(
|
|
4581
4758
|
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count), 0, false, face) + noteW(l)
|
|
@@ -4590,7 +4767,7 @@ function solve2(p, format, inline, face) {
|
|
|
4590
4767
|
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count), 0, false, face))
|
|
4591
4768
|
)
|
|
4592
4769
|
);
|
|
4593
|
-
const labelSize = Math.max(
|
|
4770
|
+
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
4594
4771
|
const lines = p.layers.map((l, i) => {
|
|
4595
4772
|
const nw = noteW(l);
|
|
4596
4773
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
@@ -4598,14 +4775,14 @@ function solve2(p, format, inline, face) {
|
|
|
4598
4775
|
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count), 0, face),
|
|
4599
4776
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4600
4777
|
// short line" — and wrapping one would put its second line under the label.
|
|
4601
|
-
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note,
|
|
4778
|
+
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
4602
4779
|
noteW: nw,
|
|
4603
4780
|
labelMaxW
|
|
4604
4781
|
};
|
|
4605
4782
|
});
|
|
4606
4783
|
const blockH = Math.max(
|
|
4607
4784
|
...lines.map(
|
|
4608
|
-
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length *
|
|
4785
|
+
(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)
|
|
4609
4786
|
)
|
|
4610
4787
|
);
|
|
4611
4788
|
const pad = Math.max(EDGE2, blockH / 2);
|
|
@@ -4625,6 +4802,7 @@ function solve2(p, format, inline, face) {
|
|
|
4625
4802
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
4626
4803
|
// layout that fits; it is one whose overflow is in the axis nothing measured.
|
|
4627
4804
|
fits: room >= blockH + 10 && height <= free && wide,
|
|
4805
|
+
floor,
|
|
4628
4806
|
wide,
|
|
4629
4807
|
inline,
|
|
4630
4808
|
width,
|
|
@@ -4664,7 +4842,7 @@ var stack = (beat, ctx) => {
|
|
|
4664
4842
|
const last = count - 1;
|
|
4665
4843
|
if (!L.fits) {
|
|
4666
4844
|
throw new Error(
|
|
4667
|
-
`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 ${
|
|
4845
|
+
`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.`
|
|
4668
4846
|
);
|
|
4669
4847
|
}
|
|
4670
4848
|
const parts = {};
|
|
@@ -4686,7 +4864,7 @@ var stack = (beat, ctx) => {
|
|
|
4686
4864
|
const dot = circle({ x: L.x0 + L.w + L.sx / 2 + 8, y: mid }, 6, { fill: tint });
|
|
4687
4865
|
const block = L.lines[i] ?? { label: [], note: [], noteW: 0, labelMaxW: L.colW };
|
|
4688
4866
|
const labelH = block.label.length * L.labelSize * 1.16;
|
|
4689
|
-
const noteH = block.note.length > 0 ? 6 + block.note.length *
|
|
4867
|
+
const noteH = block.note.length > 0 ? 6 + block.note.length * L.floor * 1.16 : 0;
|
|
4690
4868
|
const label = text(
|
|
4691
4869
|
layer.label,
|
|
4692
4870
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
@@ -4712,7 +4890,7 @@ var stack = (beat, ctx) => {
|
|
|
4712
4890
|
{ x: L.labelX, y: mid + labelH / 2 + 3 }
|
|
4713
4891
|
),
|
|
4714
4892
|
{
|
|
4715
|
-
size:
|
|
4893
|
+
size: L.floor,
|
|
4716
4894
|
fill: theme.muted,
|
|
4717
4895
|
anchor: L.inline ? "end" : "start",
|
|
4718
4896
|
maxWidth: L.inline ? void 0 : L.colW,
|
|
@@ -4724,7 +4902,7 @@ var stack = (beat, ctx) => {
|
|
|
4724
4902
|
const num2 = text(
|
|
4725
4903
|
String(i + 1),
|
|
4726
4904
|
{ x: NUM_X, y: mid },
|
|
4727
|
-
{ size:
|
|
4905
|
+
{ size: L.floor, weight: 600, fill: theme.dim, anchor: "end", vAlign: "middle" }
|
|
4728
4906
|
);
|
|
4729
4907
|
return group(slab(L.x0, y0, L, tint, lift2, stroke), { id: id(sid, "lay", i), class: "lay" }) + group(num2 + leader + dot + label + note, { id: id(sid, "cap", i), class: "cap" });
|
|
4730
4908
|
}).join("");
|
|
@@ -4746,6 +4924,8 @@ var stack = (beat, ctx) => {
|
|
|
4746
4924
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4747
4925
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4748
4926
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe3)}</div>${noteHtml}`;
|
|
4927
|
+
const centre2 = (count - 1) / 2;
|
|
4928
|
+
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4749
4929
|
const first = 0.9;
|
|
4750
4930
|
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count));
|
|
4751
4931
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
@@ -4756,7 +4936,7 @@ var stack = (beat, ctx) => {
|
|
|
4756
4936
|
tl.push(
|
|
4757
4937
|
tween(
|
|
4758
4938
|
`#${sid}-lay${i}`,
|
|
4759
|
-
{ opacity: 0, y:
|
|
4939
|
+
{ opacity: 0, y: enterFrom(i) },
|
|
4760
4940
|
{ opacity: 1, y: 0, duration: 0.55, ease: "power2.out" },
|
|
4761
4941
|
at
|
|
4762
4942
|
)
|
|
@@ -4799,8 +4979,12 @@ var stack = (beat, ctx) => {
|
|
|
4799
4979
|
// The top plane is the focal point — last built, differently toned, and the
|
|
4800
4980
|
// one the final hold sits on. Its entrance owns `opacity` and `transform`,
|
|
4801
4981
|
// so the breath takes `filter`, the property nothing else writes.
|
|
4802
|
-
ambient(sid, `-lay${last}`, BREATHE)
|
|
4803
|
-
|
|
4982
|
+
ambient(sid, `-lay${last}`, BREATHE),
|
|
4983
|
+
// Absent unless the beat asked for it, so a flat stack emits the bytes it
|
|
4984
|
+
// always did.
|
|
4985
|
+
// `.stackwrap` and not the scene: the slabs lean, the headline does not.
|
|
4986
|
+
p.tilt ? depthCss(sid, { ...DEFAULT_POSE, rotateX: p.tilt }, ".stackwrap") : ""
|
|
4987
|
+
].filter(Boolean).join("\n")
|
|
4804
4988
|
};
|
|
4805
4989
|
};
|
|
4806
4990
|
|
|
@@ -4815,6 +4999,7 @@ var emitters = {
|
|
|
4815
4999
|
stack,
|
|
4816
5000
|
"split-compare": splitCompare,
|
|
4817
5001
|
"equation-walk": equationWalk,
|
|
5002
|
+
"equation-morph": equationMorph,
|
|
4818
5003
|
"line-chart": lineChart,
|
|
4819
5004
|
// The ones that describe.
|
|
4820
5005
|
title,
|
|
@@ -5007,6 +5192,7 @@ function round4(n3) {
|
|
|
5007
5192
|
// src/emit/composition.ts
|
|
5008
5193
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5009
5194
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5195
|
+
var MORPH_SRC = "./vendor/ds-morph.js";
|
|
5010
5196
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5011
5197
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5012
5198
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5097,6 +5283,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5097
5283
|
});
|
|
5098
5284
|
let start = 0;
|
|
5099
5285
|
let builds = false;
|
|
5286
|
+
const plugins = /* @__PURE__ */ new Set();
|
|
5100
5287
|
cuts.forEach((cut2, i) => {
|
|
5101
5288
|
const { beat, sid, dive, inside, duration } = cut2;
|
|
5102
5289
|
if (cut2.segments?.length) spoken[sid] = cut2.segments;
|
|
@@ -5111,6 +5298,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5111
5298
|
}
|
|
5112
5299
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5113
5300
|
if (scene.measure?.length) builds = true;
|
|
5301
|
+
for (const p of scene.plugins ?? []) plugins.add(p);
|
|
5114
5302
|
scenes.push(
|
|
5115
5303
|
sceneHtml(
|
|
5116
5304
|
sid,
|
|
@@ -5142,7 +5330,8 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5142
5330
|
spoken,
|
|
5143
5331
|
total: start,
|
|
5144
5332
|
cut,
|
|
5145
|
-
builds
|
|
5333
|
+
builds,
|
|
5334
|
+
plugins
|
|
5146
5335
|
};
|
|
5147
5336
|
}
|
|
5148
5337
|
function enteredParts(beats) {
|
|
@@ -5223,6 +5412,9 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5223
5412
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5224
5413
|
const island = format.navigable ? `
|
|
5225
5414
|
${emitIsland(slides)}` : "";
|
|
5415
|
+
const morph = laid.plugins.has("dsMorph") ? `
|
|
5416
|
+
<script src="${MORPH_SRC}"></script>
|
|
5417
|
+
<script>gsap.registerPlugin(DSMorphPlugin);</script>` : "";
|
|
5226
5418
|
return `<!doctype html>
|
|
5227
5419
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5228
5420
|
<head>
|
|
@@ -5231,7 +5423,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5231
5423
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5232
5424
|
<script src="${GSAP_SRC}"></script>
|
|
5233
5425
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5234
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script
|
|
5426
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${morph}
|
|
5235
5427
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5236
5428
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5237
5429
|
<style>
|
|
@@ -6138,6 +6330,7 @@ var REVEALS = {
|
|
|
6138
6330
|
title: "1",
|
|
6139
6331
|
"claim-figure": "2",
|
|
6140
6332
|
"equation-walk": "one per term",
|
|
6333
|
+
"equation-morph": "2",
|
|
6141
6334
|
"data-table": "one per highlighted row, plus 1",
|
|
6142
6335
|
"line-chart": "1",
|
|
6143
6336
|
callout: "one per panel",
|
|
@@ -6169,9 +6362,9 @@ A beat is one idea, one visual, one hold. It carries:
|
|
|
6169
6362
|
beat immediately before it. See RULE 11. Leave it off unless the
|
|
6170
6363
|
source itself puts one inside the other.
|
|
6171
6364
|
|
|
6172
|
-
THE
|
|
6365
|
+
THE THIRTEEN ARCHETYPES
|
|
6173
6366
|
|
|
6174
|
-
|
|
6367
|
+
Nine of them DRAW: they build a vector graphic out of the source's own content
|
|
6175
6368
|
and reveal it stage by stage, so the viewer watches the idea assemble. Four only
|
|
6176
6369
|
describe. The drawing ones are the default. The describing ones are what you
|
|
6177
6370
|
fall back to when a point genuinely has no shape.
|
|
@@ -6249,6 +6442,14 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
6249
6442
|
An equation quoted to back a claim someone else is making is
|
|
6250
6443
|
evidence under another archetype, not a beat of its own.
|
|
6251
6444
|
|
|
6445
|
+
equation-morph One equation becoming the next, the shared terms carried
|
|
6446
|
+
across. The tell: THE SOURCE DERIVES ONE LINE FROM ANOTHER \u2014 a
|
|
6447
|
+
substitution, a rearrangement, a special case \u2014 and the point
|
|
6448
|
+
is what moved. \`fromId\` and \`toId\` name two equations from
|
|
6449
|
+
the inventory. Each terms[].tex must appear verbatim in BOTH,
|
|
6450
|
+
and travels as one piece; a term in only one of them is
|
|
6451
|
+
dropped. Four terms maximum.
|
|
6452
|
+
|
|
6252
6453
|
line-chart A trend the source states numerically but does not plot. The
|
|
6253
6454
|
tell: A QUANTITY MOVING ALONG AN ORDERED AXIS \u2014 over length, over
|
|
6254
6455
|
scale, over training. Points come from the source's numbers;
|
|
@@ -6647,7 +6848,8 @@ function renderSource(source) {
|
|
|
6647
6848
|
out.push("", "== EQUATIONS ==");
|
|
6648
6849
|
for (const e of source.equations)
|
|
6649
6850
|
out.push(`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${e.tex}`);
|
|
6650
|
-
if (!source.equations.length)
|
|
6851
|
+
if (!source.equations.length)
|
|
6852
|
+
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
6651
6853
|
out.push("", "== TABLES ==");
|
|
6652
6854
|
for (const t2 of source.tables) {
|
|
6653
6855
|
out.push(`[table ${t2.id}] ${t2.caption ?? "(no caption)"}`);
|
|
@@ -6719,6 +6921,10 @@ function assertRefsResolve(storyboard, source, opts = {}) {
|
|
|
6719
6921
|
case "equation-walk":
|
|
6720
6922
|
check3(beat, "equation", beat.params.equationId, "params.equationId");
|
|
6721
6923
|
break;
|
|
6924
|
+
case "equation-morph":
|
|
6925
|
+
check3(beat, "equation", beat.params.fromId, "params.fromId");
|
|
6926
|
+
check3(beat, "equation", beat.params.toId, "params.toId");
|
|
6927
|
+
break;
|
|
6722
6928
|
case "data-table": {
|
|
6723
6929
|
check3(beat, "table", beat.params.tableId, "params.tableId");
|
|
6724
6930
|
const table = source.tables.find((t2) => t2.id === beat.params.tableId);
|
|
@@ -6813,7 +7019,31 @@ function stripNulls(node) {
|
|
|
6813
7019
|
}
|
|
6814
7020
|
return out;
|
|
6815
7021
|
}
|
|
6816
|
-
var
|
|
7022
|
+
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
7023
|
+
function hideFromPlanner(node) {
|
|
7024
|
+
if (Array.isArray(node)) return node.map(hideFromPlanner);
|
|
7025
|
+
if (node === null || typeof node !== "object") return node;
|
|
7026
|
+
const src = node;
|
|
7027
|
+
const out = {};
|
|
7028
|
+
for (const [key, value] of Object.entries(src)) {
|
|
7029
|
+
if (key === "properties" && value && typeof value === "object") {
|
|
7030
|
+
const kept = {};
|
|
7031
|
+
for (const [prop, sub] of Object.entries(value))
|
|
7032
|
+
if (!PLANNER_INVISIBLE.has(prop)) kept[prop] = hideFromPlanner(sub);
|
|
7033
|
+
out.properties = kept;
|
|
7034
|
+
continue;
|
|
7035
|
+
}
|
|
7036
|
+
if (key === "required" && Array.isArray(value)) {
|
|
7037
|
+
out.required = value.filter((r) => typeof r !== "string" || !PLANNER_INVISIBLE.has(r));
|
|
7038
|
+
continue;
|
|
7039
|
+
}
|
|
7040
|
+
out[key] = hideFromPlanner(value);
|
|
7041
|
+
}
|
|
7042
|
+
return out;
|
|
7043
|
+
}
|
|
7044
|
+
var SCHEMA = hideFromPlanner(
|
|
7045
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" }))
|
|
7046
|
+
);
|
|
6817
7047
|
async function codexPlanner(source, opts = {}) {
|
|
6818
7048
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
6819
7049
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
@@ -1,2 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* An equation, walked one symbol at a time.
|
|
3
|
+
*
|
|
4
|
+
* This is the most valuable thing the vocabulary does: the reader is never shown
|
|
5
|
+
* a wall of TeX and left to find the symbol being discussed. Each term is wrapped
|
|
6
|
+
* in `\htmlClass{term t-<tone>}{...}` so KaTeX emits a real element for it, which
|
|
7
|
+
* GSAP then tints and swells in step with its legend row.
|
|
8
|
+
*/
|
|
9
|
+
import type { Term } from "../../types.js";
|
|
10
|
+
import type { Emitter, Theme } from "../kit.js";
|
|
11
|
+
export declare const OPTS = "{ displayMode: true, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
|
|
12
|
+
export declare const INLINE_OPTS = "{ displayMode: false, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
|
|
13
|
+
/** Where `term` sits in `tex`, comparing normal forms. Null when it is absent. */
|
|
14
|
+
export declare function locate(tex: string, term: string): {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Wrap each term where it first occurs, and report which ones were wrapped.
|
|
20
|
+
*
|
|
21
|
+
* Segments are tracked as raw/wrapped so a later term cannot match inside an
|
|
22
|
+
* earlier term's `\htmlClass{...}` and produce nested markup that highlights the
|
|
23
|
+
* wrong span.
|
|
24
|
+
*
|
|
25
|
+
* A term that cannot be found is DROPPED rather than thrown on, and the caller
|
|
26
|
+
* drops its legend row with it — the two go together, which is what keeps this
|
|
27
|
+
* honest. Silently highlighting nothing is the failure this archetype exists to
|
|
28
|
+
* avoid, and a legend line pointing at an unhighlighted symbol is that failure;
|
|
29
|
+
* a shorter legend is not. Losing an entire deck to one mis-spelled term is a
|
|
30
|
+
* worse answer than either. If NOTHING matches, the beat has no work to do and
|
|
31
|
+
* that is still an error.
|
|
32
|
+
*/
|
|
33
|
+
export declare function wrapTerms(tex: string, terms: Term[], beatId: string,
|
|
34
|
+
/** The class the wrapper carries; the morph adds its key to the walk's tint. */
|
|
35
|
+
cls?: (t: Term) => string): {
|
|
36
|
+
tex: string;
|
|
37
|
+
used: Term[];
|
|
38
|
+
missing: Term[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Display equations want to live between 68px and 108px.
|
|
42
|
+
*
|
|
43
|
+
* The equation is the whole argument of this archetype and it was the smallest
|
|
44
|
+
* thing on the slide — 72px of TeX centred in a 1700px box, measured at 46% fill
|
|
45
|
+
* with a 1920x316 band under it. It is sized off the source length rather than
|
|
46
|
+
* off `textWidth` because TeX is not the string that gets set: `\mathcal{W}(F)`
|
|
47
|
+
* is fourteen characters and three glyphs.
|
|
48
|
+
*
|
|
49
|
+
* This is the *wanted* size, not the final one. The old comment here claimed
|
|
50
|
+
* "KaTeX's own `\displaystyle` box will shrink to the container if the estimate
|
|
51
|
+
* runs wide". It does not — it overflows, silently, because the composition is
|
|
52
|
+
* the size it says it is and no gate reads past the canvas edge. At 9:16 that
|
|
53
|
+
* truncated `X = \mathcal{W}(F)` to "X =" and then left the legend explaining a
|
|
54
|
+
* symbol the viewer could not see, which is worse than a clip: the slide
|
|
55
|
+
* asserted something false. `statements` and the `size` cap below are what make
|
|
56
|
+
* the claim true.
|
|
57
|
+
*/
|
|
58
|
+
export declare function equationSize(tex: string): number;
|
|
59
|
+
/** Estimated rendered width of a display, in ems. */
|
|
60
|
+
export declare function texUnits(tex: string): number;
|
|
61
|
+
/** One row per term: a chip KaTeX fills in `setup`, and the label. Shared with the morph. */
|
|
62
|
+
export declare function legendRows(sid: string, terms: Term[], theme: Theme): string;
|
|
63
|
+
export declare function legendCss(theme: Theme): string;
|
|
2
64
|
export declare const equationWalk: Emitter<"equation-walk">;
|
|
@@ -12,7 +12,7 @@ export declare const emitters: {
|
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
14
|
* Dispatch a beat to its emitter. The cast is the one place the pairing is taken
|
|
15
|
-
* on trust: `emitters[beat.archetype]` is a union of
|
|
15
|
+
* on trust: `emitters[beat.archetype]` is a union of thirteen emitters and TypeScript
|
|
16
16
|
* will not narrow the key and the beat together. The table above already proves
|
|
17
17
|
* every archetype has exactly one emitter of the right shape.
|
|
18
18
|
*/
|
|
@@ -26,6 +26,13 @@ import type { Emitter } from "../kit.js";
|
|
|
26
26
|
import { type Face } from "../svg.js";
|
|
27
27
|
type Params = BeatOf<"stack">["params"];
|
|
28
28
|
export interface StackLayout {
|
|
29
|
+
/**
|
|
30
|
+
* The type floor this layout solved against, which is `MIN_FONT` for a flat
|
|
31
|
+
* beat and larger for a tilted one. The emitter reads it back so the notes it
|
|
32
|
+
* draws are set at the same floor the solver reserved room for — they were
|
|
33
|
+
* pinned to `MIN_FONT` directly, which under a tilt drew them below it.
|
|
34
|
+
*/
|
|
35
|
+
floor: number;
|
|
29
36
|
/**
|
|
30
37
|
* False when the rise the canvas allows is shorter than the tallest label
|
|
31
38
|
* block — i.e. adjacent labels would overlap. `stackLayout` re-composes rather
|