@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 +65 -2
- package/dist/index.d.ts +86 -1
- package/dist/index.js +434 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +933 -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,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 };
|