@jokerized/decksmith 0.2.0 → 0.3.1

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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The type floor, measured on the rendered frame instead of read off the source.
3
+ *
4
+ * `typefloor.ts` scans declared sizes and says so plainly in its own header:
5
+ * "text shrunk by a `scale` below 1 at a hold reads as its unscaled size". That
6
+ * hole is not hypothetical and it is not only about `scale`. Measured in Chrome
7
+ * at `rotateX(30deg)` under `perspective: 1400px`, a run declaring 46px renders
8
+ * 31.4 apparent px near the top of the plane — invariant 5 broken by 8.6px, with
9
+ * `lint`, `check`, `drift` and the declared-size scan all green, because not one
10
+ * of them looks at how big the glyph actually came out.
11
+ *
12
+ * That matters now because Gap 2 wants depth, and every way of tilting a plane
13
+ * shrinks the type on the far half of it. A depth archetype built against a floor
14
+ * that cannot see projection would ship unreadable slides that pass. So the gate
15
+ * comes first and the archetype second.
16
+ *
17
+ * HOW APPARENT SIZE IS MEASURED. For an SVG text node, `getBBox()` is the
18
+ * untransformed box in user units and `getClientRects()` is the painted box in
19
+ * device px, so their ratio is the TOTAL scale between the two — the element's
20
+ * own transform, every ancestor transform, the 3D projection's per-point
21
+ * perspective divide, and the stage zoom, all of it, without having to know which
22
+ * of them applied. Measured against the three cases that matter:
23
+ *
24
+ * flat, declared 46 ratio 1.000 -> 46.0 apparent px
25
+ * tilted 30deg, near plane top 0.682 -> 31.4
26
+ * tilted 30deg, near plane bottom 1.126 -> 51.8 (magnified, not a fault)
27
+ * `scale(0.6)` at a hold 0.600 -> 27.6
28
+ *
29
+ * `getScreenCTM()` was tried first and rejected: it returns 0.673 for BOTH the
30
+ * top and bottom runs above, because it carries the affine part and drops the
31
+ * perspective divide that makes those two differ by 23 apparent px.
32
+ *
33
+ * WHY IT DIVIDES BY THE STAGE. A deck legitimately scales its whole stage —
34
+ * `zoomOf`/`REF_PULL` — and in portrait 40 reference px is SUPPOSED to land at 30
35
+ * canvas px, which `typefloor.ts` calls "the whole argument of REF_PULL and not a
36
+ * violation". Dividing the run's own ratio by the scene's gives apparent size
37
+ * back in REFERENCE px, so this gate agrees with the declared one on every deck
38
+ * that does nothing clever, and only diverges where something shrank one element
39
+ * relative to its own scene.
40
+ */
41
+ import type { Finding } from "../types.js";
42
+ /** One text run, as the frame actually drew it. */
43
+ export interface ApparentRun {
44
+ /** First 40 characters, for the message. */
45
+ text: string;
46
+ /** `font-size` as declared, in px. */
47
+ declared: number;
48
+ /** Painted height over untransformed height, including the stage's own zoom. */
49
+ ratio: number;
50
+ /** Its own computed opacity, multiplied down the ancestors that carry one. */
51
+ opacity: number;
52
+ }
53
+ /** What `measureApparent` hands back for one instant. */
54
+ export interface ApparentStop {
55
+ sid: string;
56
+ t: number;
57
+ runs: ApparentRun[];
58
+ /** The scene's own painted-over-declared scale, the baseline runs divide by. */
59
+ stage: number;
60
+ /**
61
+ * True at a declared stop, where the frame has arrived and everything drawn is
62
+ * being read. False at a sampled midpoint, where a run may be part-way through
63
+ * its own entrance — see `SETTLED_OPACITY`.
64
+ */
65
+ settled: boolean;
66
+ }
67
+ /**
68
+ * Serialised into the page: every visible SVG text run's apparent scale.
69
+ *
70
+ * Deliberately the same walk as `collectSvgTextRuns` in overprint.ts — same
71
+ * visibility rules, same "only inside an `<svg>`" restriction — because two gates
72
+ * that disagree about which text exists are two gates that cannot be reconciled
73
+ * when they disagree about a deck.
74
+ */
75
+ export declare function collectApparent(sid: string): {
76
+ runs: ApparentRun[];
77
+ stage: number;
78
+ };
79
+ /**
80
+ * Apparent size in reference px: what the run declared, times how much the frame
81
+ * shrank or grew it relative to its own scene.
82
+ */
83
+ export declare function apparentPx(run: ApparentRun, stage: number): number;
84
+ /**
85
+ * How opaque a run must be, at a sampled midpoint, to count as text being read.
86
+ *
87
+ * At a declared stop the frame has arrived and everything drawn counts. Between
88
+ * stops it has not: `annotated-figure` enters its labels from `scale: 0.97`, so
89
+ * a run caught half way through its own entrance is BOTH under the floor and
90
+ * fading in, and failing a build for it would be crying wolf about a frame no
91
+ * one reads. A run at 0.95 opacity is not entering any more; it has arrived.
92
+ */
93
+ export declare const SETTLED_OPACITY = 0.95;
94
+ /**
95
+ * The times to sample BETWEEN the declared stops.
96
+ *
97
+ * `fidelity` measures at stops because that is where a frame has settled, and
98
+ * the apparent floor inherited that — which left exactly the hole it was built
99
+ * to close, one interval over: text scaled DOWN between two stops is invisible
100
+ * to a gate that only looks at the stops. A camera pulling back, or an exit that
101
+ * shrinks a label, would ship.
102
+ *
103
+ * Midway between consecutive stops of the SAME scene. Not across a scene
104
+ * boundary, where the midpoint lands in a cross-fade between two compositions
105
+ * and belongs to neither. Costs a seek and a DOM read each — no screenshot,
106
+ * which is what makes doubling the sample count affordable.
107
+ */
108
+ export declare function midpoints(stops: readonly {
109
+ sid: string;
110
+ t: number;
111
+ }[]): {
112
+ sid: string;
113
+ t: number;
114
+ }[];
115
+ /**
116
+ * Runs the frame drew below the floor, whatever shrank them.
117
+ *
118
+ * An ERROR, not a warning, and for the same reason `typefloor` is: text the
119
+ * audience cannot read is not a matter of degree. The tolerance is a tenth of a
120
+ * pixel, which is rasteriser noise rather than a grace margin — a deck that wants
121
+ * 40px declares 40px and measures 40.0, as the flat control does exactly.
122
+ */
123
+ export declare function gradeApparent(stops: readonly ApparentStop[], floor?: number): Finding[];
@@ -242,6 +242,70 @@ export declare function scanUnusedFigures(storyboard: Storyboard, source: Source
242
242
  * decision cannot be made by accident.
243
243
  */
244
244
  export declare function scanBeatCount(storyboard: Storyboard, prefs: Prefs): Finding[];
245
+ /**
246
+ * Where a paper-shaped deck missed the shape it was asked for.
247
+ *
248
+ * SILENT UNLESS THE ARC WAS DECLARED. `prefs.genre` defaults to `general`, so
249
+ * every deck built before this existed, and every deck whose author did not say
250
+ * `--genre paper`, gets exactly nothing from this scan. That is not timidity: a
251
+ * ten-role heading lexicon over all 351 markdown files in this repository found
252
+ * 345 with zero hits, so there is no honest way to infer the genre, and a scan
253
+ * that guessed would mislabel the Korean fixture and every hypepaper analysis
254
+ * the tool actually ingests.
255
+ *
256
+ * REPORTED, NEVER REPAIRED, and a WARNING. It joins the four scans that already
257
+ * take that shape, and `scanBeatCount`'s header settles the class: `build`
258
+ * prints every content loss and fails on none of them, because the alternative
259
+ * is re-asking, and the only possible second instruction — "return more" — is a
260
+ * padding request with extra steps. Promoting this one is a decision about all
261
+ * of them.
262
+ *
263
+ * WHAT IT CANNOT DO. It cannot tell whether a beat labelled `limitations` really
264
+ * discusses one; `role` is the plan's own declaration, and a declaration can be
265
+ * false. What it checks instead is the part that is mechanical and that no
266
+ * committed plan has ever got right on its own: that the roles exist, that they
267
+ * are unique, that the ending is in the right ORDER, and that the closing pair
268
+ * does not collapse into two of the same picture. A rule about wording would be
269
+ * met cosmetically, exactly as `scanHeadlines` records a sharpened rule being
270
+ * answered by swapping one verb.
271
+ */
272
+ export declare function scanPaperArc(storyboard: Storyboard, prefs: Prefs): Finding[];
273
+ /**
274
+ * Narration that belongs to a different version of this storyboard.
275
+ *
276
+ * `narration.json` is keyed by BEAT ID and carries nothing that ties it to the
277
+ * plan it was made for, so an id that still exists is an id `build` will happily
278
+ * speak — even when the beat behind it has been replaced. Renumbering is the way
279
+ * in: inserting one beat and shifting the ids after it leaves every later id
280
+ * pointing at its neighbour's voice.
281
+ *
282
+ * FOUND IN THE SHIPPED DEMO, and it is the seventh green-gate case this project
283
+ * has recorded. A thirteenth beat was inserted at position six and the rest
284
+ * renumbered; the audio on disk predated it, so beats six through twelve each
285
+ * spoke the slide after them and the thirteenth spoke nothing. `build` reported
286
+ * `PASS — 0 error(s), 0 warning(s)`, because every id it looked for was present
287
+ * and nothing compared what was SAID against what the beat says it says.
288
+ *
289
+ * AN ERROR, NOT A WARNING, and it is the one member of this family that earns
290
+ * it. The other storyboard scans are editorial judgements only an author can
291
+ * settle; this is a string comparison between two authored fields that must
292
+ * agree, and the artifact it lets through is a video whose voice describes the
293
+ * wrong pictures. Missing narration audio is already an error here, and audio
294
+ * that is present and wrong is not the better failure.
295
+ *
296
+ * NARROW ON PURPOSE. It compares only beats that have BOTH a narration text and
297
+ * segments, so a deck narrated at a lower density — where some beats are
298
+ * deliberately silent — says nothing, and the existing missing-audio error keeps
299
+ * its own case. Whitespace is normalised because the splitter re-joins sentences
300
+ * at stop boundaries; on the demo's thirteen beats the concatenation reproduces
301
+ * the authored text exactly, which is what makes the comparison safe to make
302
+ * fatal.
303
+ */
304
+ export declare function scanNarrationDrift(storyboard: Storyboard, narration: {
305
+ beats: Record<string, {
306
+ text: string;
307
+ }[]>;
308
+ }): Finding[];
245
309
  /**
246
310
  * How far a name may precede the thing it names before a viewer notices.
247
311
  *
@@ -28,7 +28,9 @@
28
28
  *
29
29
  * WHAT IT DOES NOT SEE, stated because a gate that overclaims is the thing this
30
30
  * file was written to stop. It reads declared sizes, so text shrunk by a
31
- * `scale` below 1 at a hold reads as its unscaled size; and it says nothing about
31
+ * `scale` below 1 at a hold reads as its unscaled size `apparent.ts` is the
32
+ * gate that closes exactly that, by measuring the painted glyph instead, and it
33
+ * catches a 3D projection the same way; and it says nothing about
32
34
  * whether the text FITS — `container_overflow` and `text_occluded` in
33
35
  * `hyperframes check` are the gates for that, and `check.ts` already grades them
34
36
  * up to errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",