@idle-screens/schema 3.1.1 → 3.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/FORMAT.md +71 -2
- package/dist/index.d.ts +97 -1
- package/dist/index.js +697 -136
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +947 -189
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),
|
|
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,61 @@ 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' }` (default) performs a hard switch.
|
|
317
|
+
`{ type: 'morph', dur: number }` smoothly interpolates paint properties
|
|
318
|
+
(colors, opacity values) over `dur` ms when crossing into the next segment.
|
|
319
|
+
Morph requires structurally identical adjacent segments (same
|
|
320
|
+
`structuralSignature`); if they differ, the engine falls back to cut and the
|
|
321
|
+
validator emits a `morph-structural-mismatch` warning. During a morph, entity
|
|
322
|
+
placement inherits the outgoing segment's seed — the incoming segment's own
|
|
323
|
+
seed is unused. `dur` must be between 200 and 5000 ms.
|
|
324
|
+
|
|
325
|
+
**Time mapping:** global clock `T` maps to `(segmentIndex, localT)` via prefix
|
|
326
|
+
sums of durations. Half-open segments: `[start, start+duration)`. With
|
|
327
|
+
`loop: true`, `T` wraps at the sum of all durations (loop is incompatible with
|
|
328
|
+
a durationless final segment).
|
|
329
|
+
|
|
330
|
+
**Compilation:** `compileSequence()` returns an ordinary `SaverPlugin` — the
|
|
331
|
+
viewer needs zero changes. All children share a single canvas; only the active
|
|
332
|
+
segment's `SpecInstance` is alive at any time. `workerReady` is `false` (the
|
|
333
|
+
worker compile-hook does not dispatch sequences).
|
|
334
|
+
|
|
335
|
+
**Steering:** segment switching uses the `sequence.segment` delta path via
|
|
336
|
+
`applyTrack`. The `SequenceInstance` intercepts this path before delegation.
|
|
337
|
+
Remaining deltas are forwarded to the active child's `applyTrack`.
|
|
338
|
+
|
|
339
|
+
**Seed:** `seq.seed` is forwarded to children that lack a scene-level seed
|
|
340
|
+
(offset by segment index for independence). Children with their own seed are
|
|
341
|
+
unaffected.
|
|
342
|
+
|
|
274
343
|
## Examples
|
|
275
344
|
|
|
276
345
|
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,39 @@ 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;
|
|
433
|
+
};
|
|
434
|
+
type SequenceTransition = {
|
|
435
|
+
type: 'cut';
|
|
436
|
+
} | {
|
|
437
|
+
type: 'morph';
|
|
438
|
+
dur: number;
|
|
408
439
|
};
|
|
440
|
+
interface SequenceSegment {
|
|
441
|
+
key: string;
|
|
442
|
+
scene: SaverSpec;
|
|
443
|
+
/** Segment duration in ms. Only the final segment may omit this (holds until input). */
|
|
444
|
+
duration?: number;
|
|
445
|
+
/** How the segment advances: 'auto' (timer), 'input' (external event), 'either'. */
|
|
446
|
+
advance?: 'auto' | 'input' | 'either';
|
|
447
|
+
transition?: SequenceTransition;
|
|
448
|
+
}
|
|
449
|
+
interface IdleSequence {
|
|
450
|
+
format: 'idle-sequence';
|
|
451
|
+
schemaVersion: 1;
|
|
452
|
+
id: string;
|
|
453
|
+
label: string;
|
|
454
|
+
seed?: number;
|
|
455
|
+
loop: boolean;
|
|
456
|
+
segments: SequenceSegment[];
|
|
457
|
+
}
|
|
409
458
|
|
|
410
459
|
/**
|
|
411
460
|
* Validate an untrusted (agent- or JSON-authored) spec structurally + semantically.
|
|
@@ -417,6 +466,13 @@ declare const LIMITS: {
|
|
|
417
466
|
declare function validateSpec(spec: unknown): ValidationResult;
|
|
418
467
|
/** Narrowing helper: validate + cast. Throws with a joined message when invalid. */
|
|
419
468
|
declare function assertValidSpec(spec: unknown): SaverSpec;
|
|
469
|
+
/**
|
|
470
|
+
* Validate a sequence envelope. Each segment's `scene` goes through `validateSpec`;
|
|
471
|
+
* sequence-level rules (segment count, duration floors, key uniqueness, transition
|
|
472
|
+
* bounds) are checked here.
|
|
473
|
+
*/
|
|
474
|
+
declare function validateSequence(seq: unknown): ValidationResult;
|
|
475
|
+
declare function assertValidSequence(seq: unknown): IdleSequence;
|
|
420
476
|
|
|
421
477
|
/** Near plane for warp motion. z lives in [WARP_NEAR, 1]; screen scale is 1/z. */
|
|
422
478
|
declare const WARP_NEAR = 0.08;
|
|
@@ -551,6 +607,16 @@ declare function linkPairs(positions: Array<{
|
|
|
551
607
|
x: number;
|
|
552
608
|
y: number;
|
|
553
609
|
}>, k: number, maxDist: number, doWrap: boolean, w: number, h: number): Array<[number, number]>;
|
|
610
|
+
interface TextBlockLine {
|
|
611
|
+
text: string;
|
|
612
|
+
widthEm: number;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Break `text` into lines that fit within `maxWidthEm` em-widths. Splits on
|
|
616
|
+
* whitespace; a single word wider than the limit gets its own line unbroken.
|
|
617
|
+
* Explicit `\n` always forces a break. Pure, deterministic, no canvas needed.
|
|
618
|
+
*/
|
|
619
|
+
declare function breakTextBlock(text: string, maxWidthEm: number): TextBlockLine[];
|
|
554
620
|
|
|
555
621
|
/** Derive a manifest so a compiled spec composes with @idle-screens/capabilities. */
|
|
556
622
|
declare function manifestFor(spec: SaverSpec): SaverManifest;
|
|
@@ -560,6 +626,7 @@ declare function manifestFor(spec: SaverSpec): SaverManifest;
|
|
|
560
626
|
* deterministic, `renderFrame(t,seed)`-addressable, and flash-safe by construction.
|
|
561
627
|
*/
|
|
562
628
|
declare function compileSaver(spec: unknown): SaverPlugin;
|
|
629
|
+
declare function compileSequence(spec: unknown): SaverPlugin;
|
|
563
630
|
|
|
564
631
|
/**
|
|
565
632
|
* Pure helpers for LIVE steering of compiled specs (setParam / applyTrack).
|
|
@@ -617,6 +684,14 @@ declare function adviseSpec(spec: SaverSpec, viewport?: {
|
|
|
617
684
|
width: number;
|
|
618
685
|
height: number;
|
|
619
686
|
}): SpecWarning[];
|
|
687
|
+
/**
|
|
688
|
+
* Non-blocking advisory warnings for a validated sequence envelope.
|
|
689
|
+
* Checks cross-segment luminance jumps at boundaries and per-segment advisories.
|
|
690
|
+
*/
|
|
691
|
+
declare function adviseSequence(seq: IdleSequence, viewport?: {
|
|
692
|
+
width: number;
|
|
693
|
+
height: number;
|
|
694
|
+
}): SpecWarning[];
|
|
620
695
|
|
|
621
696
|
interface LayerSnapshot {
|
|
622
697
|
key: string | undefined;
|
|
@@ -851,6 +926,24 @@ interface ScenePerception {
|
|
|
851
926
|
*/
|
|
852
927
|
declare function perceiveScene(spec: SaverSpec, opts?: LuminanceGridOptions): ScenePerception;
|
|
853
928
|
|
|
929
|
+
interface ResolvedSegment {
|
|
930
|
+
index: number;
|
|
931
|
+
localT: number;
|
|
932
|
+
/** Cumulative start time of this segment in the global timeline. */
|
|
933
|
+
startT: number;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Map global time T to (segmentIndex, localT) via prefix sums of durations.
|
|
937
|
+
* Segments are half-open: [start, start+duration). The final segment may omit
|
|
938
|
+
* duration, in which case it holds indefinitely (T past the last timed segment
|
|
939
|
+
* always resolves to the final one).
|
|
940
|
+
*
|
|
941
|
+
* With `loop: true`, T wraps modulo the total duration of all segments. If the
|
|
942
|
+
* final segment has no duration, looping is over the timed prefix only — the
|
|
943
|
+
* durationless tail is unreachable in loop mode (validated elsewhere).
|
|
944
|
+
*/
|
|
945
|
+
declare function resolveSegment(seq: IdleSequence, T: number): ResolvedSegment;
|
|
946
|
+
|
|
854
947
|
/**
|
|
855
948
|
* Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
|
|
856
949
|
* that the schema is actually expressive: two layers (fish drifting both ways at varied
|
|
@@ -919,6 +1012,9 @@ declare const SAKURA_SPEC: SaverSpec;
|
|
|
919
1012
|
*/
|
|
920
1013
|
declare const DASHBOARD_SPEC: SaverSpec;
|
|
921
1014
|
|
|
1015
|
+
/** Haiku: a centered text block with a slowly drifting haiku over deep indigo. */
|
|
1016
|
+
declare const HAIKU_SPEC: SaverSpec;
|
|
1017
|
+
|
|
922
1018
|
/**
|
|
923
1019
|
* Orrery: a mechanical solar system. Exercises orbit motion (concentric rings
|
|
924
1020
|
* at different speeds), spin (planets rotate on their axes), and grow (the sun
|
|
@@ -990,4 +1086,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
|
|
|
990
1086
|
/** Lookup by saver id. */
|
|
991
1087
|
declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
|
|
992
1088
|
|
|
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 };
|
|
1089
|
+
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, adviseSequence, 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 };
|