@idle-screens/schema 3.4.4 → 3.5.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/FORMAT.md +39 -7
- package/dist/index.d.ts +53 -8
- package/dist/index.js +192 -35
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/FORMAT.md
CHANGED
|
@@ -127,7 +127,7 @@ trails, long-exposure light). 0.85–0.95 is the useful range; 0 (default) is of
|
|
|
127
127
|
| `pulse` | `{amp ≤ 0.5, period ≥ 500, wave?}` | none | opacity breathing; `wave: {wavelength, angle?}` turns it into a traveling wave across the field |
|
|
128
128
|
| `spin` | number \| `[min,max]` ±360 deg/sec | none | per-entity rotation (seeded start angle); a range gives each entity a seeded speed (confetti, tumbling debris) |
|
|
129
129
|
| `grow` | `{amp ≤ 0.8, period ≥ 500}` | none | size breathing (seeded phase) |
|
|
130
|
-
| `trail` | `{length ≤ 5000, fade?}` | none | analytic afterglow trail
|
|
130
|
+
| `trail` | `{length ≤ 5000, fade?}` | none | analytic afterglow trail; `length` is **milliseconds** of history (max 5000), `fade` a number 0..1 (not a boolean — `links.falloff` in the next row is the boolean) |
|
|
131
131
|
| `links` | see below | none | inter-entity lines |
|
|
132
132
|
| `layout` | `{type: "grid", columns?, jitter?}` | scatter | grid placement; `jitter` scalar or `{x?, y?}` 0..1 per axis |
|
|
133
133
|
| `life` | `{enter?, exit?, fade?}` ms | always on | act structure: fade the layer in at `enter`, out at `exit` |
|
|
@@ -180,7 +180,14 @@ with distance, removing pop-in at the cutoff.
|
|
|
180
180
|
frontier per grapheme (`typewriter`, default), per `word`, or per `line`.
|
|
181
181
|
`speed` (graphemes/sec, max 120) makes the block type itself from scene
|
|
182
182
|
start: effective progress is `min(progress, speed·t/total)`, so a steered
|
|
183
|
-
`progress` can still hold or delete a self-typing block.
|
|
183
|
+
`progress` can still hold or delete a self-typing block. A fourth mode,
|
|
184
|
+
`glyphFade`, fades each glyph in over a staggered alpha ramp instead of
|
|
185
|
+
popping it whole (the caption look); `fade` (0–1 exclusive of 0, default
|
|
186
|
+
0.15, `glyphFade` only) is each glyph's fade window as a fraction of
|
|
187
|
+
progress — small reads as a soft typewriter, large as a wave of
|
|
188
|
+
overlapping fades. Still a pure function of `progress`, so it glides and
|
|
189
|
+
deletes exactly like the other modes. Native clients render `glyphFade`
|
|
190
|
+
as `typewriter` until they gain per-glyph alpha. `caret` draws a
|
|
184
191
|
blinking caret at the frontier (`true`, or `{ "blink": 1.2, "color": "#fff" }`;
|
|
185
192
|
blink is in full cycles/sec, capped at 3 for flash safety, and is a square
|
|
186
193
|
wave of `t` like every other animation). With `align: "center"`/`"right"`
|
|
@@ -290,6 +297,14 @@ trade-offs for a zero-dependency, renderer-free analysis tool.
|
|
|
290
297
|
text layer. Glyphs are invisible in the luminance maps, so this is the only
|
|
291
298
|
way to confirm *what words* are on screen and how big. (Also on
|
|
292
299
|
`perceiveScene().text`.)
|
|
300
|
+
- `adviseSpec(spec)` — non-blocking advisories (also on
|
|
301
|
+
`perceiveScene().advisories`). Two of them are **spatial**, for static text:
|
|
302
|
+
`text-off-screen` (a text/textBlock box crosses the viewport edge by more
|
|
303
|
+
than 1%) and `text-overlap` (two static text layers share more than 10% of
|
|
304
|
+
the smaller box). Boxes come from the same character-class width table the
|
|
305
|
+
textBlock line-breaker uses, so they are estimates of the renderer's own
|
|
306
|
+
layout — a caption that fails these will look wrong on the wall; one that
|
|
307
|
+
passes may still sit a few px off.
|
|
293
308
|
- `diffScenes(a, b, opts)` — **relative sight**: coverage/luminance deltas,
|
|
294
309
|
visual-balance shift, 3×3 region deltas, dominance-rank movement, and
|
|
295
310
|
advisory codes added/removed. Agents judge "is B better than A" far more
|
|
@@ -330,8 +345,12 @@ timeline. Discriminated from SaverSpec by `format: 'idle-sequence'`.
|
|
|
330
345
|
be unique. Duration is in milliseconds (minimum 1000 ms for flash safety). Only
|
|
331
346
|
the final segment may omit duration (holds indefinitely).
|
|
332
347
|
|
|
333
|
-
**Advance mode:** `auto` (default)
|
|
334
|
-
|
|
348
|
+
**Advance mode:** `auto` (default) and `either` advance on the timer. `input`
|
|
349
|
+
makes a timed segment **hold** at the end of its `duration` until a
|
|
350
|
+
`sequence.segment` steer releases it — the clicker (see Steering below). The
|
|
351
|
+
held scene keeps animating (its `localT` keeps growing), so a slide waiting for
|
|
352
|
+
the presenter never freezes. A durationless final segment holds regardless of
|
|
353
|
+
`advance`.
|
|
335
354
|
|
|
336
355
|
**Transitions:** `{ type: 'cut' }` (default) performs a hard switch.
|
|
337
356
|
`{ type: 'morph', dur: number }` smoothly interpolates paint properties
|
|
@@ -345,7 +364,8 @@ seed is unused. `dur` must be between 200 and 5000 ms.
|
|
|
345
364
|
**Time mapping:** global clock `T` maps to `(segmentIndex, localT)` via prefix
|
|
346
365
|
sums of durations. Half-open segments: `[start, start+duration)`. With
|
|
347
366
|
`loop: true`, `T` wraps at the sum of all durations (loop is incompatible with
|
|
348
|
-
a durationless final segment).
|
|
367
|
+
a durationless final segment). An unreleased `advance: 'input'` hold blocks the
|
|
368
|
+
wrap; after a wrap every hold is armed again.
|
|
349
369
|
|
|
350
370
|
**Compilation:** `compileSequence()` returns an ordinary `SaverPlugin` — the
|
|
351
371
|
viewer needs zero changes. All children share a single canvas; only the active
|
|
@@ -353,8 +373,20 @@ segment's `SpecInstance` is alive at any time. `workerReady` is `false` (the
|
|
|
353
373
|
worker compile-hook does not dispatch sequences).
|
|
354
374
|
|
|
355
375
|
**Steering:** segment switching uses the `sequence.segment` delta path via
|
|
356
|
-
`applyTrack
|
|
357
|
-
|
|
376
|
+
`applyTrack` (`setParam("sequence.segment", n)` over MCP). The
|
|
377
|
+
`SequenceInstance` intercepts this path before delegation; remaining deltas are
|
|
378
|
+
forwarded to the active child's `applyTrack`.
|
|
379
|
+
|
|
380
|
+
The steer **moves the clock, not the frame**: it displaces the timeline so the
|
|
381
|
+
target segment starts at its own `localT` 0 (its `life.enter` build replays)
|
|
382
|
+
and then runs on the timer from there — the next animation frame resolves to
|
|
383
|
+
the same segment instead of snapping back to the wall clock. A steer to
|
|
384
|
+
segment `n` also counts as the presenter clicking past every `advance: 'input'`
|
|
385
|
+
hold before `n`; holds at and after `n` stay armed, so steering backwards
|
|
386
|
+
re-arms the ones in between. This is the whole clicker: a deck is a sequence
|
|
387
|
+
whose slides carry `advance: 'input'`, and "next" is one `setParam`. Pinned by
|
|
388
|
+
`sequence.test.ts` → "sequence.segment steering is sticky" and
|
|
389
|
+
"advance: 'input' holds until released".
|
|
358
390
|
|
|
359
391
|
**Seed:** `seq.seed` is forwarded to children that lack a scene-level seed
|
|
360
392
|
(offset by segment index for independence). Children with their own seed are
|
package/dist/index.d.ts
CHANGED
|
@@ -292,8 +292,12 @@ interface TextRevealSpec {
|
|
|
292
292
|
* by `mode`. Steerable paint — glide it to animate typing or deleting.
|
|
293
293
|
*/
|
|
294
294
|
progress?: number;
|
|
295
|
-
/**
|
|
296
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Reveal granularity: per-grapheme (default), per-word, per-line, or
|
|
297
|
+
* `glyphFade` — each glyph fades in over a staggered alpha ramp instead of
|
|
298
|
+
* appearing whole (the caption look). Still a pure function of `progress`.
|
|
299
|
+
*/
|
|
300
|
+
mode?: 'typewriter' | 'word' | 'line' | 'glyphFade';
|
|
297
301
|
/**
|
|
298
302
|
* Self-typing rate in graphemes/second from scene start (media time). The
|
|
299
303
|
* effective progress is `min(progress, speed·t/total)` — so a steered
|
|
@@ -309,6 +313,12 @@ interface TextRevealSpec {
|
|
|
309
313
|
blink?: number;
|
|
310
314
|
color?: string;
|
|
311
315
|
};
|
|
316
|
+
/**
|
|
317
|
+
* `glyphFade` only: each glyph's fade window as a fraction of progress
|
|
318
|
+
* (0 exclusive to 1, default 0.15). Small = soft typewriter; large = a
|
|
319
|
+
* wave of overlapping fades. Ignored by the other modes.
|
|
320
|
+
*/
|
|
321
|
+
fade?: number;
|
|
312
322
|
}
|
|
313
323
|
/** Rotate through sprite variants over time. Each entity offsets by its seeded phase. */
|
|
314
324
|
interface CycleSpec {
|
|
@@ -637,6 +647,8 @@ declare function linkPairs(positions: Array<{
|
|
|
637
647
|
x: number;
|
|
638
648
|
y: number;
|
|
639
649
|
}>, k: number, maxDist: number, doWrap: boolean, w: number, h: number): Array<[number, number]>;
|
|
650
|
+
/** Em-width of a string under the same character-class table `breakTextBlock` uses. */
|
|
651
|
+
declare function textWidthEm(str: string): number;
|
|
640
652
|
interface TextBlockLine {
|
|
641
653
|
text: string;
|
|
642
654
|
widthEm: number;
|
|
@@ -656,8 +668,9 @@ declare function breakTextBlock(text: string, maxWidthEm: number): TextBlockLine
|
|
|
656
668
|
declare function graphemeClusters(text: string): string[];
|
|
657
669
|
interface TextRevealInput {
|
|
658
670
|
progress?: number;
|
|
659
|
-
mode?: 'typewriter' | 'word' | 'line';
|
|
671
|
+
mode?: 'typewriter' | 'word' | 'line' | 'glyphFade';
|
|
660
672
|
speed?: number;
|
|
673
|
+
fade?: number;
|
|
661
674
|
}
|
|
662
675
|
interface RevealState {
|
|
663
676
|
/** Effective 0..1 progress after combining authored progress and speed at t. */
|
|
@@ -669,6 +682,12 @@ interface RevealState {
|
|
|
669
682
|
/** Caret anchor: line index and the visible prefix on that line. */
|
|
670
683
|
caretLine: number;
|
|
671
684
|
caretPrefix: string;
|
|
685
|
+
/**
|
|
686
|
+
* `glyphFade` mode only: per-line per-grapheme alpha (0..1), indexed
|
|
687
|
+
* parallel to `graphemeClusters(lines[i].text)`. Alphas only decrease in
|
|
688
|
+
* reading order, so a renderer may stop at the first zero.
|
|
689
|
+
*/
|
|
690
|
+
glyphAlphas?: number[][];
|
|
672
691
|
}
|
|
673
692
|
/**
|
|
674
693
|
* Resolve which glyphs of a broken textBlock are visible at time `tMs`.
|
|
@@ -993,18 +1012,44 @@ interface ResolvedSegment {
|
|
|
993
1012
|
localT: number;
|
|
994
1013
|
/** Cumulative start time of this segment in the global timeline. */
|
|
995
1014
|
startT: number;
|
|
1015
|
+
/**
|
|
1016
|
+
* Present (true) when the timeline has run past this segment's duration but
|
|
1017
|
+
* the segment has `advance: 'input'` and no input has released it yet — the
|
|
1018
|
+
* scene keeps animating (`localT` keeps growing) while the show waits for
|
|
1019
|
+
* the presenter.
|
|
1020
|
+
*/
|
|
1021
|
+
held?: true;
|
|
996
1022
|
}
|
|
1023
|
+
interface ResolveOptions {
|
|
1024
|
+
/**
|
|
1025
|
+
* Segments below this index have been released by input: a `sequence.segment`
|
|
1026
|
+
* steer to index `n` counts as the presenter clicking past every hold before
|
|
1027
|
+
* `n`. `SequenceInstance` maintains this; callers resolving a bare timeline
|
|
1028
|
+
* leave it at 0, so every `advance: 'input'` hold is honoured.
|
|
1029
|
+
*/
|
|
1030
|
+
releasedBelow?: number;
|
|
1031
|
+
}
|
|
1032
|
+
/** Global start time of segment `index` — the prefix sum of the durations before it. */
|
|
1033
|
+
declare function segmentStart(seq: IdleSequence, index: number): number;
|
|
997
1034
|
/**
|
|
998
1035
|
* Map global time T to (segmentIndex, localT) via prefix sums of durations.
|
|
999
1036
|
* Segments are half-open: [start, start+duration). The final segment may omit
|
|
1000
1037
|
* duration, in which case it holds indefinitely (T past the last timed segment
|
|
1001
1038
|
* always resolves to the final one).
|
|
1002
1039
|
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1040
|
+
* `advance: 'input'` makes a timed segment hold at its end instead of yielding
|
|
1041
|
+
* to the next one — the clicker. `auto` (default) and `either` advance on the
|
|
1042
|
+
* timer. A hold is released by a `sequence.segment` steer (see
|
|
1043
|
+
* `ResolveOptions.releasedBelow`); the held scene keeps its clock running, so
|
|
1044
|
+
* `localT` grows past `duration` rather than freezing.
|
|
1045
|
+
*
|
|
1046
|
+
* With `loop: true`, T wraps modulo the total duration of all segments — but
|
|
1047
|
+
* only once every hold on the current lap has been released; after a wrap
|
|
1048
|
+
* every hold is armed again. If the final segment has no duration, looping is
|
|
1049
|
+
* over the timed prefix only — the durationless tail is unreachable in loop
|
|
1050
|
+
* mode (validated elsewhere).
|
|
1006
1051
|
*/
|
|
1007
|
-
declare function resolveSegment(seq: IdleSequence, T: number): ResolvedSegment;
|
|
1052
|
+
declare function resolveSegment(seq: IdleSequence, T: number, opts?: ResolveOptions): ResolvedSegment;
|
|
1008
1053
|
|
|
1009
1054
|
/**
|
|
1010
1055
|
* Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
|
|
@@ -1148,4 +1193,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
|
|
|
1148
1193
|
/** Lookup by saver id. */
|
|
1149
1194
|
declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
|
|
1150
1195
|
|
|
1151
|
-
export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COMETS_SPEC, CONSTELLATION_SPEC, type CycleSpec, DASHBOARD_SPEC, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type Placed, RAIN_SPEC, type ResolvedSegment, type RevealState, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SequenceSegment, type SequenceTransition, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type TextBlockLine, type TextRevealInput, type TextRevealSpec, type TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, breakTextBlock, buildEntities, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, graphemeClusters, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rotationAt, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, validateSequence, validateSpec };
|
|
1196
|
+
export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COMETS_SPEC, CONSTELLATION_SPEC, type CycleSpec, DASHBOARD_SPEC, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type Placed, RAIN_SPEC, type ResolveOptions, type ResolvedSegment, type RevealState, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SequenceSegment, type SequenceTransition, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type TextBlockLine, type TextRevealInput, type TextRevealSpec, type TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, breakTextBlock, buildEntities, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, graphemeClusters, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rotationAt, segmentStart, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, textWidthEm, validateSequence, validateSpec };
|
package/dist/index.js
CHANGED
|
@@ -221,7 +221,7 @@ var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "c
|
|
|
221
221
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
222
222
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
223
223
|
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal"]);
|
|
224
|
-
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret"]);
|
|
224
|
+
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade"]);
|
|
225
225
|
var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
|
|
226
226
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
227
227
|
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
@@ -604,8 +604,11 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
604
604
|
if (rv.progress !== void 0 && (!isNum(rv.progress) || rv.progress < 0 || rv.progress > 1)) {
|
|
605
605
|
err(`${path}.reveal.progress`, "must be a number between 0 and 1");
|
|
606
606
|
}
|
|
607
|
-
if (rv.mode !== void 0 && !["typewriter", "word", "line"].includes(rv.mode)) {
|
|
608
|
-
err(`${path}.reveal.mode`, "must be 'typewriter' | 'word' | 'line'");
|
|
607
|
+
if (rv.mode !== void 0 && !["typewriter", "word", "line", "glyphFade"].includes(rv.mode)) {
|
|
608
|
+
err(`${path}.reveal.mode`, "must be 'typewriter' | 'word' | 'line' | 'glyphFade'");
|
|
609
|
+
}
|
|
610
|
+
if (rv.fade !== void 0 && (!isNum(rv.fade) || rv.fade <= 0 || rv.fade > 1)) {
|
|
611
|
+
err(`${path}.reveal.fade`, "must be between 0 (exclusive) and 1 (fraction of progress)");
|
|
609
612
|
}
|
|
610
613
|
if (rv.speed !== void 0 && (!isNum(rv.speed) || rv.speed < 0 || rv.speed > LIMITS.maxRevealSpeed)) {
|
|
611
614
|
err(`${path}.reveal.speed`, `must be between 0 and ${LIMITS.maxRevealSpeed} (graphemes/sec)`);
|
|
@@ -1462,6 +1465,17 @@ function revealState(lines, reveal, tMs) {
|
|
|
1462
1465
|
const mode = reveal.mode ?? "typewriter";
|
|
1463
1466
|
let fullLines = 0;
|
|
1464
1467
|
let partialText = "";
|
|
1468
|
+
let glyphAlphas;
|
|
1469
|
+
if (mode === "glyphFade") {
|
|
1470
|
+
const f = Math.min(1, Math.max(0.01, reveal.fade ?? 0.15));
|
|
1471
|
+
let g = 0;
|
|
1472
|
+
glyphAlphas = lineClusters.map(
|
|
1473
|
+
(clusters) => clusters.map(() => {
|
|
1474
|
+
const start = totalGraphemes > 0 ? g++ / totalGraphemes * (1 - f) : 0;
|
|
1475
|
+
return Math.max(0, Math.min(1, (progress - start) / f));
|
|
1476
|
+
})
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1465
1479
|
if (mode === "line") {
|
|
1466
1480
|
fullLines = Math.round(progress * lines.length);
|
|
1467
1481
|
} else if (mode === "word") {
|
|
@@ -1501,7 +1515,7 @@ function revealState(lines, reveal, tMs) {
|
|
|
1501
1515
|
caretLine = 0;
|
|
1502
1516
|
caretPrefix = "";
|
|
1503
1517
|
}
|
|
1504
|
-
return { progress, fullLines, partialText, caretLine, caretPrefix };
|
|
1518
|
+
return { progress, fullLines, partialText, caretLine, caretPrefix, glyphAlphas };
|
|
1505
1519
|
}
|
|
1506
1520
|
|
|
1507
1521
|
// src/compile.ts
|
|
@@ -1510,7 +1524,13 @@ import {
|
|
|
1510
1524
|
} from "@idle-screens/core";
|
|
1511
1525
|
|
|
1512
1526
|
// src/sequence.ts
|
|
1513
|
-
function
|
|
1527
|
+
function segmentStart(seq, index) {
|
|
1528
|
+
let t = 0;
|
|
1529
|
+
const n = Math.max(0, Math.min(index, seq.segments.length - 1));
|
|
1530
|
+
for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
|
|
1531
|
+
return t;
|
|
1532
|
+
}
|
|
1533
|
+
function resolveSegment(seq, T, opts = {}) {
|
|
1514
1534
|
const { segments, loop } = seq;
|
|
1515
1535
|
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
1516
1536
|
let totalTimed = 0;
|
|
@@ -1518,24 +1538,33 @@ function resolveSegment(seq, T) {
|
|
|
1518
1538
|
if (seg.duration != null) totalTimed += seg.duration;
|
|
1519
1539
|
else break;
|
|
1520
1540
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1541
|
+
const t0 = Math.max(0, T);
|
|
1542
|
+
const walk = (t, releasedBelow) => {
|
|
1543
|
+
let cumulative = 0;
|
|
1544
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1545
|
+
const seg = segments[i];
|
|
1546
|
+
if (seg.duration == null) {
|
|
1547
|
+
return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
|
|
1548
|
+
}
|
|
1549
|
+
if (t < cumulative + seg.duration) {
|
|
1550
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
1551
|
+
}
|
|
1552
|
+
if (seg.advance === "input" && i >= releasedBelow) {
|
|
1553
|
+
return { index: i, localT: t - cumulative, startT: cumulative, held: true };
|
|
1554
|
+
}
|
|
1555
|
+
cumulative += seg.duration;
|
|
1533
1556
|
}
|
|
1534
|
-
|
|
1557
|
+
return null;
|
|
1558
|
+
};
|
|
1559
|
+
const first = walk(t0, opts.releasedBelow ?? 0);
|
|
1560
|
+
if (first) return first;
|
|
1561
|
+
if (loop && totalTimed > 0) {
|
|
1562
|
+
const wrapped = walk(t0 % totalTimed, 0);
|
|
1563
|
+
if (wrapped) return wrapped;
|
|
1535
1564
|
}
|
|
1536
1565
|
const last = segments.length - 1;
|
|
1537
|
-
const lastStart =
|
|
1538
|
-
return { index: last, localT:
|
|
1566
|
+
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
1567
|
+
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
1539
1568
|
}
|
|
1540
1569
|
|
|
1541
1570
|
// src/compile.ts
|
|
@@ -1853,11 +1882,39 @@ var SpecInstance = class {
|
|
|
1853
1882
|
ctx.textBaseline = "top";
|
|
1854
1883
|
ctx.textAlign = align;
|
|
1855
1884
|
const xOff = align === "center" ? maxWPx / 2 : align === "right" ? maxWPx : 0;
|
|
1856
|
-
|
|
1857
|
-
ctx.
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1885
|
+
if (rs?.glyphAlphas) {
|
|
1886
|
+
ctx.textAlign = "left";
|
|
1887
|
+
const baseAlpha = ctx.globalAlpha;
|
|
1888
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1889
|
+
const alphas = rs.glyphAlphas[li] ?? [];
|
|
1890
|
+
if (alphas.length > 0 && alphas[0] <= 0) break;
|
|
1891
|
+
const clusters = graphemeClusters(lines[li].text);
|
|
1892
|
+
const lw = ctx.measureText(lines[li].text).width;
|
|
1893
|
+
const x0 = align === "center" ? (maxWPx - lw) / 2 : align === "right" ? maxWPx - lw : 0;
|
|
1894
|
+
let opaque = 0;
|
|
1895
|
+
while (opaque < clusters.length && (alphas[opaque] ?? 0) >= 1) opaque++;
|
|
1896
|
+
let prefix = "";
|
|
1897
|
+
if (opaque > 0) {
|
|
1898
|
+
prefix = clusters.slice(0, opaque).join("");
|
|
1899
|
+
ctx.globalAlpha = baseAlpha;
|
|
1900
|
+
ctx.fillText(prefix, x0, li * lh);
|
|
1901
|
+
}
|
|
1902
|
+
for (let gi = opaque; gi < clusters.length; gi++) {
|
|
1903
|
+
const a = alphas[gi] ?? 0;
|
|
1904
|
+
if (a <= 0) break;
|
|
1905
|
+
ctx.globalAlpha = baseAlpha * a;
|
|
1906
|
+
ctx.fillText(clusters[gi], x0 + ctx.measureText(prefix).width, li * lh);
|
|
1907
|
+
prefix += clusters[gi];
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
ctx.globalAlpha = baseAlpha;
|
|
1911
|
+
} else {
|
|
1912
|
+
for (let li = 0; li < fullLines; li++) {
|
|
1913
|
+
ctx.fillText(lines[li].text, xOff, li * lh);
|
|
1914
|
+
}
|
|
1915
|
+
if (rs && rs.partialText.length > 0) {
|
|
1916
|
+
ctx.fillText(rs.partialText, xOff, fullLines * lh);
|
|
1917
|
+
}
|
|
1861
1918
|
}
|
|
1862
1919
|
if (rs && reveal.caret) {
|
|
1863
1920
|
const cfg = reveal.caret === true ? {} : reveal.caret;
|
|
@@ -2091,6 +2148,18 @@ var SequenceInstance = class {
|
|
|
2091
2148
|
startT = 0;
|
|
2092
2149
|
baseT = 0;
|
|
2093
2150
|
lastT = 0;
|
|
2151
|
+
/** The T most recently passed to renderFrame — the clock a steer displaces. */
|
|
2152
|
+
renderedT = 0;
|
|
2153
|
+
/**
|
|
2154
|
+
* The clicker's two pieces of state. `clockOffset` is added to every T so
|
|
2155
|
+
* a `sequence.segment` steer lands the timeline at the target segment's
|
|
2156
|
+
* start and STAYS there as the wall clock keeps ticking — the steer moves
|
|
2157
|
+
* the clock instead of fighting it. `releasedBelow` records that the steer
|
|
2158
|
+
* counts as the presenter clicking past every `advance: 'input'` hold
|
|
2159
|
+
* before the target (see `resolveSegment`).
|
|
2160
|
+
*/
|
|
2161
|
+
clockOffset = 0;
|
|
2162
|
+
releasedBelow = 0;
|
|
2094
2163
|
constructor(seq, ctx) {
|
|
2095
2164
|
this.seq = seq;
|
|
2096
2165
|
let surface = ctx.surface ?? null;
|
|
@@ -2173,7 +2242,8 @@ var SequenceInstance = class {
|
|
|
2173
2242
|
this.renderFrame(this.lastT, this.seed);
|
|
2174
2243
|
}
|
|
2175
2244
|
renderFrame(T, seed) {
|
|
2176
|
-
|
|
2245
|
+
this.renderedT = T;
|
|
2246
|
+
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
2177
2247
|
const { index, localT } = resolved;
|
|
2178
2248
|
const prevIdx = index > 0 ? index - 1 : -1;
|
|
2179
2249
|
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
@@ -2242,14 +2312,9 @@ var SequenceInstance = class {
|
|
|
2242
2312
|
const segDelta = deltas.find((d) => d.path === "sequence.segment");
|
|
2243
2313
|
if (segDelta !== void 0 && typeof segDelta.value === "number") {
|
|
2244
2314
|
const idx = Math.max(0, Math.min(this.seq.segments.length - 1, Math.round(segDelta.value)));
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
}
|
|
2249
|
-
this.activeIndex = idx;
|
|
2250
|
-
const child = this.ensureChild(idx);
|
|
2251
|
-
child.renderFrame(0, this.seq.segments[idx].scene.seed ?? this.seq.seed ?? 0);
|
|
2252
|
-
}
|
|
2315
|
+
this.clockOffset = segmentStart(this.seq, idx) - this.renderedT;
|
|
2316
|
+
this.releasedBelow = idx;
|
|
2317
|
+
this.renderFrame(this.renderedT, this.seed);
|
|
2253
2318
|
}
|
|
2254
2319
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
2255
2320
|
if (childDeltas.length > 0 && this.activeIndex >= 0) {
|
|
@@ -2529,8 +2594,88 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2529
2594
|
});
|
|
2530
2595
|
}
|
|
2531
2596
|
}
|
|
2597
|
+
const textBoxes = [];
|
|
2598
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
2599
|
+
const layer = spec.layers[li];
|
|
2600
|
+
const s = layer.sprite;
|
|
2601
|
+
if (layer.motion.type !== "static") continue;
|
|
2602
|
+
if (s.kind !== "text" && s.kind !== "textBlock") continue;
|
|
2603
|
+
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
2604
|
+
for (const e of allEntities[li]) {
|
|
2605
|
+
const p = positionAt(e, 0, w, h);
|
|
2606
|
+
const box = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
2607
|
+
textBoxes.push({ li, label, ...box });
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
const tolX = w * 0.01;
|
|
2611
|
+
const tolY = h * 0.01;
|
|
2612
|
+
const offFlagged = /* @__PURE__ */ new Set();
|
|
2613
|
+
for (const b of textBoxes) {
|
|
2614
|
+
if (offFlagged.has(b.li)) continue;
|
|
2615
|
+
const edges = [];
|
|
2616
|
+
if (b.x0 < -tolX) edges.push(`left edge by ~${Math.round(-b.x0)}px`);
|
|
2617
|
+
if (b.x1 > w + tolX) edges.push(`right edge by ~${Math.round(b.x1 - w)}px`);
|
|
2618
|
+
if (b.y0 < -tolY) edges.push(`top edge by ~${Math.round(-b.y0)}px`);
|
|
2619
|
+
if (b.y1 > h + tolY) edges.push(`bottom edge by ~${Math.round(b.y1 - h)}px`);
|
|
2620
|
+
if (edges.length === 0) continue;
|
|
2621
|
+
offFlagged.add(b.li);
|
|
2622
|
+
warnings.push({
|
|
2623
|
+
path: `layers[${b.li}]`,
|
|
2624
|
+
code: "text-off-screen",
|
|
2625
|
+
message: `${b.label} text runs off the ${edges.join(" and the ")} at ${w}\xD7${h} \u2014 move position, or shrink fontSize / maxWidth`
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
const pairFlagged = /* @__PURE__ */ new Set();
|
|
2629
|
+
for (let i = 0; i < textBoxes.length; i++) {
|
|
2630
|
+
for (let j = i + 1; j < textBoxes.length; j++) {
|
|
2631
|
+
const a = textBoxes[i];
|
|
2632
|
+
const b = textBoxes[j];
|
|
2633
|
+
if (a.li === b.li) continue;
|
|
2634
|
+
const pair = `${Math.min(a.li, b.li)}:${Math.max(a.li, b.li)}`;
|
|
2635
|
+
if (pairFlagged.has(pair)) continue;
|
|
2636
|
+
const ix = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
|
2637
|
+
const iy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
|
2638
|
+
if (ix <= 0 || iy <= 0) continue;
|
|
2639
|
+
const smaller = Math.min((a.x1 - a.x0) * (a.y1 - a.y0), (b.x1 - b.x0) * (b.y1 - b.y0));
|
|
2640
|
+
if (smaller <= 0) continue;
|
|
2641
|
+
const share = ix * iy / smaller;
|
|
2642
|
+
if (share <= 0.1) continue;
|
|
2643
|
+
pairFlagged.add(pair);
|
|
2644
|
+
const [first, second] = a.li < b.li ? [a, b] : [b, a];
|
|
2645
|
+
warnings.push({
|
|
2646
|
+
path: `layers[${first.li}]`,
|
|
2647
|
+
code: "text-overlap",
|
|
2648
|
+
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it`
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2532
2652
|
return warnings;
|
|
2533
2653
|
}
|
|
2654
|
+
function textBoxAt(s, e, p, spec, w, h) {
|
|
2655
|
+
const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
|
|
2656
|
+
const fontScale = spec.units === "px" ? 1 : Math.min(w, h) / (spec.referenceViewport ?? LIMITS.referenceViewport);
|
|
2657
|
+
const fh = m ? Number(m[1]) * fontScale : e.size;
|
|
2658
|
+
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
2659
|
+
let fw = textWidthEm(str) * fh;
|
|
2660
|
+
if (s.maxWidth) fw = Math.min(fw, s.maxWidth * (spec.units === "px" ? 1 : Math.min(w, h)));
|
|
2661
|
+
const align = s.align ?? "center";
|
|
2662
|
+
const baseline = s.baseline ?? "middle";
|
|
2663
|
+
const x0 = align === "left" ? p.x : align === "right" ? p.x - fw : p.x - fw / 2;
|
|
2664
|
+
const y0 = baseline === "top" ? p.y : baseline === "bottom" ? p.y - fh : p.y - fh / 2;
|
|
2665
|
+
return { x0, y0, x1: x0 + fw, y1: y0 + fh };
|
|
2666
|
+
}
|
|
2667
|
+
function textBlockBoxAt(s, p, w, h) {
|
|
2668
|
+
const unit = Math.min(w, h);
|
|
2669
|
+
const fsPx = s.fontSize * unit;
|
|
2670
|
+
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
2671
|
+
const maxWPx = s.maxWidth * unit;
|
|
2672
|
+
const lines = breakTextBlock(s.text, maxWPx / fsPx);
|
|
2673
|
+
const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
|
|
2674
|
+
const totalH = lines.length * lh;
|
|
2675
|
+
const align = s.align ?? "left";
|
|
2676
|
+
const x0 = align === "center" ? p.x + (maxWPx - maxLineW) / 2 : align === "right" ? p.x + maxWPx - maxLineW : p.x;
|
|
2677
|
+
return { x0, y0: p.y, x1: x0 + maxLineW, y1: p.y + totalH };
|
|
2678
|
+
}
|
|
2534
2679
|
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
2535
2680
|
const warnings = [];
|
|
2536
2681
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
@@ -2729,7 +2874,17 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
2729
2874
|
const unitScale = Math.min(w, h);
|
|
2730
2875
|
const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
|
|
2731
2876
|
const lines = breakTextBlock(s.text, maxWEm);
|
|
2732
|
-
|
|
2877
|
+
const rs = revealState(lines, s.reveal, t);
|
|
2878
|
+
if (rs.glyphAlphas) {
|
|
2879
|
+
let sum = 0;
|
|
2880
|
+
let n = 0;
|
|
2881
|
+
for (const line of rs.glyphAlphas) for (const a of line) {
|
|
2882
|
+
sum += a;
|
|
2883
|
+
n++;
|
|
2884
|
+
}
|
|
2885
|
+
return n > 0 ? sum / n : 1;
|
|
2886
|
+
}
|
|
2887
|
+
return rs.progress;
|
|
2733
2888
|
}
|
|
2734
2889
|
function luminanceGrid(spec, opts = {}) {
|
|
2735
2890
|
const scene = buildScene(spec, opts);
|
|
@@ -4725,12 +4880,14 @@ export {
|
|
|
4725
4880
|
resolveSpecPath,
|
|
4726
4881
|
revealState,
|
|
4727
4882
|
rotationAt,
|
|
4883
|
+
segmentStart,
|
|
4728
4884
|
sizeAt,
|
|
4729
4885
|
spriteIndexAt,
|
|
4730
4886
|
spriteVariants,
|
|
4731
4887
|
steerablePaths,
|
|
4732
4888
|
structuralSignature,
|
|
4733
4889
|
textSprites,
|
|
4890
|
+
textWidthEm,
|
|
4734
4891
|
validateSequence,
|
|
4735
4892
|
validateSpec
|
|
4736
4893
|
};
|