@idle-screens/schema 3.1.1 → 3.2.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 CHANGED
@@ -7,7 +7,8 @@ deterministic, flash-safe `SaverPlugin`. There is no code in a spec: no
7
7
  scripting, no network access, no DOM access.
8
8
 
9
9
  - **Machine-readable schema:** [`saver-spec.schema.json`](./saver-spec.schema.json)
10
- (JSON Schema draft-07), importable as
10
+ (JSON Schema draft-07), covering SaverSpec scenes and `idle-sequence`
11
+ envelopes (`oneOf`, discriminated by `format: "idle-sequence"`). Importable as
11
12
  `@idle-screens/schema/saver-spec.schema.json`.
12
13
  - **Runtime validator:** `validateSpec(spec)` / `assertValidSpec(spec)` — used by
13
14
  `compileSaver()`, which refuses to run an invalid spec.
@@ -29,7 +30,8 @@ Additions to version 1 so far, all optional and backward compatible:
29
30
  `ghosting`, `wander` / `warp` / `path` motions, `ring` / `streak` / `rect`
30
31
  sprites, `colorWeights`, `pulse.wave`, `layout` (grid), `life`,
31
32
  `links.mode/falloff/closed`, `blend: screen|multiply`, orbit layer-parents
32
- (2026-07-21 — "the v1 ceiling").
33
+ (2026-07-21 — "the v1 ceiling");
34
+ `textBlock` sprite — deterministic multi-line text with viewport-unit sizing.
33
35
 
34
36
  ## Safety invariants
35
37
 
@@ -154,6 +156,18 @@ with distance, removing pop-in at the cutoff.
154
156
  (rain that reads as rain, shooting stars, warp stars)
155
157
  - `{ "kind": "rect", "width": [0.012, 0.02], "aspect": [1.3, 1.7], "color": "#ffb347" }` —
156
158
  rectangle; `aspect` is the height/width ratio range (rotates with `spin`)
159
+ - `{ "kind": "textBlock", "text": "Multi-line text with wrapping.", "maxWidth": 0.8, "fontSize": 0.04, "lineHeight": 1.4, "align": "left", "color": "#e6e8ef" }` —
160
+ multi-line text block with deterministic line-breaking. All dimensions are
161
+ viewport fractions (of `min(w,h)`), not px — `units: "px"` specs reject
162
+ textBlock sprites at validation time. `position` is always the block's
163
+ **top-left corner** regardless of `align` (align moves text within the box,
164
+ not the box itself — different from the `text` sprite where `align`/`baseline`
165
+ shift the meaning of `position`). Line breaks are computed from a fixed
166
+ character-class metrics table so wrapping is identical across platforms; note
167
+ that the table is approximate — a painted line may slightly exceed `maxWidth`
168
+ when the real font is wider than the table estimates, so `maxWidth` is a
169
+ layout target, not a hard clip.
170
+ Use with `count: 1`, `motion: { type: "static" }`, and `position`.
157
171
 
158
172
  `circle`, `ring`, `streak`, and `rect` all accept `colors: [...]` (seeded
159
173
  per-entity palette pick) and `colorWeights: [...]` (relative weights, same
@@ -271,6 +285,55 @@ is estimated from font size × string length. Good enough to perceive
271
285
  composition, focus, balance, and motion — not a substitute for a human (or VLM)
272
286
  judgement of beauty.
273
287
 
288
+ ## Sequence envelope (`idle-sequence`)
289
+
290
+ A second top-level format that composes multiple SaverSpecs into a sequenced
291
+ timeline. Discriminated from SaverSpec by `format: 'idle-sequence'`.
292
+
293
+ ```jsonc
294
+ {
295
+ "format": "idle-sequence",
296
+ "schemaVersion": 1,
297
+ "id": "my-sequence",
298
+ "label": "My Sequence",
299
+ "seed": 42, // optional; forwarded to children without their own seed
300
+ "loop": false,
301
+ "segments": [
302
+ { "key": "intro", "scene": { /* SaverSpec */ }, "duration": 5000 },
303
+ { "key": "main", "scene": { /* SaverSpec */ }, "duration": 10000, "advance": "auto" },
304
+ { "key": "outro", "scene": { /* SaverSpec */ } } // durationless: holds forever
305
+ ]
306
+ }
307
+ ```
308
+
309
+ **Segments:** 1–24 segments, each carrying an unmodified SaverSpec. Keys must
310
+ be unique. Duration is in milliseconds (minimum 1000 ms for flash safety). Only
311
+ the final segment may omit duration (holds indefinitely).
312
+
313
+ **Advance mode:** `auto` (default), `input`, or `either`. Validated but not
314
+ wired to runtime behavior — timer/input drivers are planned for a follow-up.
315
+
316
+ **Transitions:** `{ type: 'cut' }` is the only supported transition. `fade` is
317
+ defined as a type but rejected at validation.
318
+
319
+ **Time mapping:** global clock `T` maps to `(segmentIndex, localT)` via prefix
320
+ sums of durations. Half-open segments: `[start, start+duration)`. With
321
+ `loop: true`, `T` wraps at the sum of all durations (loop is incompatible with
322
+ a durationless final segment).
323
+
324
+ **Compilation:** `compileSequence()` returns an ordinary `SaverPlugin` — the
325
+ viewer needs zero changes. All children share a single canvas; only the active
326
+ segment's `SpecInstance` is alive at any time. `workerReady` is `false` (the
327
+ worker compile-hook does not dispatch sequences).
328
+
329
+ **Steering:** segment switching uses the `sequence.segment` delta path via
330
+ `applyTrack`. The `SequenceInstance` intercepts this path before delegation.
331
+ Remaining deltas are forwarded to the active child's `applyTrack`.
332
+
333
+ **Seed:** `seq.seed` is forwarded to children that lack a scene-level seed
334
+ (offset by segment index for independence). Children with their own seed are
335
+ unaffected.
336
+
274
337
  ## Examples
275
338
 
276
339
  Shipped working specs (also exposed as `EXAMPLE_SPECS` /
package/dist/index.d.ts CHANGED
@@ -257,6 +257,23 @@ type SpriteSpec = {
257
257
  color: string;
258
258
  colors?: string[];
259
259
  colorWeights?: number[];
260
+ }
261
+ /**
262
+ * Multi-line text block with deterministic line-breaking. Unlike `text` (one
263
+ * string drawn as a single fillText call), `textBlock` wraps `text` within
264
+ * `maxWidth` using a fixed metrics table so line breaks are identical across
265
+ * platforms (same seed → same frames). All dimensions are viewport-fraction
266
+ * (of `min(w,h)`), not px — addresses the absolute-font-size inconsistency.
267
+ * Use with `count: 1`, `motion: { type: 'static' }`, and `position`.
268
+ */
269
+ | {
270
+ kind: 'textBlock';
271
+ text: string;
272
+ maxWidth: number;
273
+ fontSize: number;
274
+ lineHeight?: number;
275
+ align?: 'left' | 'center' | 'right';
276
+ color?: string;
260
277
  };
261
278
  /** Rotate through sprite variants over time. Each entity offsets by its seeded phase. */
262
279
  interface CycleSpec {
@@ -405,7 +422,36 @@ declare const LIMITS: {
405
422
  readonly maxPathPoints: 24;
406
423
  readonly minPathDuration: 2000;
407
424
  readonly maxGridColumns: 100;
425
+ readonly maxTextBlockLength: 2000;
426
+ readonly minTextBlockFontSize: 0.01;
427
+ readonly maxTextBlockFontSize: 0.2;
428
+ readonly maxTextBlockMaxWidth: 1;
429
+ readonly maxSegments: 24;
430
+ readonly minSegmentDuration: 1000;
431
+ readonly maxTransitionDur: 5000;
432
+ readonly minTransitionDur: 200;
408
433
  };
434
+ interface SequenceTransition {
435
+ type: 'cut';
436
+ }
437
+ interface SequenceSegment {
438
+ key: string;
439
+ scene: SaverSpec;
440
+ /** Segment duration in ms. Only the final segment may omit this (holds until input). */
441
+ duration?: number;
442
+ /** How the segment advances: 'auto' (timer), 'input' (external event), 'either'. */
443
+ advance?: 'auto' | 'input' | 'either';
444
+ transition?: SequenceTransition;
445
+ }
446
+ interface IdleSequence {
447
+ format: 'idle-sequence';
448
+ schemaVersion: 1;
449
+ id: string;
450
+ label: string;
451
+ seed?: number;
452
+ loop: boolean;
453
+ segments: SequenceSegment[];
454
+ }
409
455
 
410
456
  /**
411
457
  * Validate an untrusted (agent- or JSON-authored) spec structurally + semantically.
@@ -417,6 +463,13 @@ declare const LIMITS: {
417
463
  declare function validateSpec(spec: unknown): ValidationResult;
418
464
  /** Narrowing helper: validate + cast. Throws with a joined message when invalid. */
419
465
  declare function assertValidSpec(spec: unknown): SaverSpec;
466
+ /**
467
+ * Validate a sequence envelope. Each segment's `scene` goes through `validateSpec`;
468
+ * sequence-level rules (segment count, duration floors, key uniqueness, transition
469
+ * bounds) are checked here.
470
+ */
471
+ declare function validateSequence(seq: unknown): ValidationResult;
472
+ declare function assertValidSequence(seq: unknown): IdleSequence;
420
473
 
421
474
  /** Near plane for warp motion. z lives in [WARP_NEAR, 1]; screen scale is 1/z. */
422
475
  declare const WARP_NEAR = 0.08;
@@ -551,6 +604,16 @@ declare function linkPairs(positions: Array<{
551
604
  x: number;
552
605
  y: number;
553
606
  }>, k: number, maxDist: number, doWrap: boolean, w: number, h: number): Array<[number, number]>;
607
+ interface TextBlockLine {
608
+ text: string;
609
+ widthEm: number;
610
+ }
611
+ /**
612
+ * Break `text` into lines that fit within `maxWidthEm` em-widths. Splits on
613
+ * whitespace; a single word wider than the limit gets its own line unbroken.
614
+ * Explicit `\n` always forces a break. Pure, deterministic, no canvas needed.
615
+ */
616
+ declare function breakTextBlock(text: string, maxWidthEm: number): TextBlockLine[];
554
617
 
555
618
  /** Derive a manifest so a compiled spec composes with @idle-screens/capabilities. */
556
619
  declare function manifestFor(spec: SaverSpec): SaverManifest;
@@ -560,6 +623,7 @@ declare function manifestFor(spec: SaverSpec): SaverManifest;
560
623
  * deterministic, `renderFrame(t,seed)`-addressable, and flash-safe by construction.
561
624
  */
562
625
  declare function compileSaver(spec: unknown): SaverPlugin;
626
+ declare function compileSequence(spec: unknown): SaverPlugin;
563
627
 
564
628
  /**
565
629
  * Pure helpers for LIVE steering of compiled specs (setParam / applyTrack).
@@ -851,6 +915,24 @@ interface ScenePerception {
851
915
  */
852
916
  declare function perceiveScene(spec: SaverSpec, opts?: LuminanceGridOptions): ScenePerception;
853
917
 
918
+ interface ResolvedSegment {
919
+ index: number;
920
+ localT: number;
921
+ /** Cumulative start time of this segment in the global timeline. */
922
+ startT: number;
923
+ }
924
+ /**
925
+ * Map global time T to (segmentIndex, localT) via prefix sums of durations.
926
+ * Segments are half-open: [start, start+duration). The final segment may omit
927
+ * duration, in which case it holds indefinitely (T past the last timed segment
928
+ * always resolves to the final one).
929
+ *
930
+ * With `loop: true`, T wraps modulo the total duration of all segments. If the
931
+ * final segment has no duration, looping is over the timed prefix only — the
932
+ * durationless tail is unreachable in loop mode (validated elsewhere).
933
+ */
934
+ declare function resolveSegment(seq: IdleSequence, T: number): ResolvedSegment;
935
+
854
936
  /**
855
937
  * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
856
938
  * that the schema is actually expressive: two layers (fish drifting both ways at varied
@@ -919,6 +1001,9 @@ declare const SAKURA_SPEC: SaverSpec;
919
1001
  */
920
1002
  declare const DASHBOARD_SPEC: SaverSpec;
921
1003
 
1004
+ /** Haiku: a centered text block with a slowly drifting haiku over deep indigo. */
1005
+ declare const HAIKU_SPEC: SaverSpec;
1006
+
922
1007
  /**
923
1008
  * Orrery: a mechanical solar system. Exercises orbit motion (concentric rings
924
1009
  * at different speeds), spin (planets rotate on their axes), and grow (the sun
@@ -990,4 +1075,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
990
1075
  /** Lookup by saver id. */
991
1076
  declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
992
1077
 
993
- 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, 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, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSpec, buildEntities, compileSaver, describeScene, diffScenes, dominanceRanking, easeSmooth, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSpecPath, rotationAt, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, validateSpec };
1078
+ 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, 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 TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, breakTextBlock, buildEntities, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, rotationAt, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, validateSequence, validateSpec };