@idle-screens/schema 2.2.0 → 2.4.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 +172 -36
- package/dist/index.d.ts +493 -26
- package/dist/index.js +1842 -351
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +192 -8
package/FORMAT.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SaverSpec — Format Specification (version 1)
|
|
2
2
|
|
|
3
3
|
The declarative, agent-authorable screensaver format compiled by
|
|
4
|
-
`@idle-screens/schema`. A spec describes a saver as **data** — a
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
`@idle-screens/schema`. A spec describes a saver as **data** — a background plus
|
|
5
|
+
layers of moving sprites — which `compileSaver()` turns into a seeded,
|
|
6
|
+
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)
|
|
@@ -23,23 +23,38 @@ older runtimes ignore unknown fields (see Validation semantics).
|
|
|
23
23
|
Additions to version 1 so far, all optional and backward compatible:
|
|
24
24
|
`alpha`, `blend`, `pulse`, `region` (layer appearance/placement);
|
|
25
25
|
`static` motion, `position`, `key` (dashboard/HUD support);
|
|
26
|
-
`align`, `baseline`, `maxWidth` (text sprites)
|
|
26
|
+
`align`, `baseline`, `maxWidth` (text sprites);
|
|
27
|
+
`spin`, `grow`, `cycle`, `orbit`, `links`, `units`, `referenceViewport`;
|
|
28
|
+
`trail`, `background.drift` (2026-07-19);
|
|
29
|
+
`ghosting`, `wander` / `warp` / `path` motions, `ring` / `streak` / `rect`
|
|
30
|
+
sprites, `colorWeights`, `pulse.wave`, `layout` (grid), `life`,
|
|
31
|
+
`links.mode/falloff/closed`, `blend: screen|multiply`, orbit layer-parents
|
|
32
|
+
(2026-07-21 — "the v1 ceiling").
|
|
27
33
|
|
|
28
34
|
## Safety invariants
|
|
29
35
|
|
|
30
36
|
These hold **by construction** — no spec can violate them:
|
|
31
37
|
|
|
32
|
-
1. **No flash primitive.** The background is static
|
|
33
|
-
sprites, so a compiled spec cannot strobe the full
|
|
34
|
-
sampling any compiled spec through
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
1. **No flash primitive.** The background is static (drift is floored at 10 s)
|
|
39
|
+
and entities are bounded sprites, so a compiled spec cannot strobe the full
|
|
40
|
+
field. Provable by sampling any compiled spec through
|
|
41
|
+
`@idle-screens/validator`.
|
|
42
|
+
2. **Pulse/grow/cycle are bounded.** Breathing amplitude is capped and periods
|
|
43
|
+
floored at 500 ms (2 Hz — under the WCAG 3 Hz flash threshold). Every entity
|
|
44
|
+
gets its own seeded phase — and with `pulse.wave`, a position-derived phase —
|
|
45
|
+
so a layer will generally not pulse in unison (edge case: if spawn positions
|
|
46
|
+
alias the wavelength, phases may coincide). `ghosting` only *smooths* luminance
|
|
47
|
+
changes (it composites frames over a faded copy of the last), never sharpens
|
|
48
|
+
them.
|
|
49
|
+
3. **Motion is bounded.** Speeds are capped (4000 px/sec; warp at 1.5
|
|
50
|
+
depth-units/sec; orbit at 180 deg/sec; path laps at ≥ 2 s).
|
|
51
|
+
4. **Work is bounded.** ≤ 36 layers, ≤ 400 entities per layer, ≤ 800 total,
|
|
52
|
+
links layers ≤ 200 entities, trails ≤ 24 samples.
|
|
40
53
|
5. **Determinism.** All randomness comes from a seeded PRNG. Same spec + same
|
|
41
54
|
seed ⇒ identical entity streams and identical frames at any time `t`
|
|
42
|
-
(compiled savers expose `renderFrame(t, seed)`).
|
|
55
|
+
(compiled savers expose `renderFrame(t, seed)`). With `ghosting`, seeks
|
|
56
|
+
replay a bounded fixed-step warm-up so even the accumulated smear is
|
|
57
|
+
reproducible.
|
|
43
58
|
|
|
44
59
|
## Validation semantics
|
|
45
60
|
|
|
@@ -52,9 +67,10 @@ Two validators exist with deliberately different strictness:
|
|
|
52
67
|
|
|
53
68
|
Author against the JSON Schema; ship through the runtime validator.
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
total entities across all layers ≤ 800
|
|
57
|
-
|
|
70
|
+
Rules the JSON Schema cannot fully express (the runtime enforces them):
|
|
71
|
+
total entities across all layers ≤ 800; every `[min, max]` range must satisfy
|
|
72
|
+
`min ≤ max`; `colorWeights` length must match `colors`; orbit layer-parents
|
|
73
|
+
must exist, have `count: 1`, and not themselves orbit a layer.
|
|
58
74
|
|
|
59
75
|
## Structure
|
|
60
76
|
|
|
@@ -65,17 +81,32 @@ satisfy `min ≤ max`.
|
|
|
65
81
|
"label": "Snowfall",
|
|
66
82
|
"seed": 42, // optional; falls back to the host's seed
|
|
67
83
|
"motionIntensity": "calm", // optional: calm | moderate | energetic
|
|
84
|
+
"units": "viewport", // optional: viewport (default) | px
|
|
85
|
+
"referenceViewport": 1080, // optional; design resolution for density scaling
|
|
86
|
+
"ghosting": 0.9, // optional 0..0.95; frame-persistence smear
|
|
68
87
|
"background": { ... }, // optional; defaults to black
|
|
69
88
|
"layers": [ { ... }, ... ] // 1..36, rendered back-to-front
|
|
70
89
|
}
|
|
71
90
|
```
|
|
72
91
|
|
|
92
|
+
With `units: "viewport"` (the default) every dimensional value — sizes, radii,
|
|
93
|
+
speeds, distances, wavelengths, stroke widths — is a fraction of
|
|
94
|
+
`min(width, height)`, so specs scale to any display. Exception: text sprites
|
|
95
|
+
with an explicit `px` size in their `font` string (e.g. `"bold 14px monospace"`)
|
|
96
|
+
use that CSS font verbatim and do not scale with the viewport.
|
|
97
|
+
|
|
98
|
+
`ghosting` paints each frame over a faded copy of the previous one instead of
|
|
99
|
+
clearing: moving entities leave decaying after-images (Mystify smears, Matrix
|
|
100
|
+
trails, long-exposure light). 0.85–0.95 is the useful range; 0 (default) is off.
|
|
101
|
+
|
|
73
102
|
### `background`
|
|
74
103
|
|
|
75
104
|
- `{ "type": "solid", "color": "#06060c" }`
|
|
76
|
-
- `{ "type": "gradient", "stops": [{ "at": 0, "color": "#06121e" }, { "at": 1, "color": "#0d2436" }], "band": { "color": "#3a2d18", "height":
|
|
105
|
+
- `{ "type": "gradient", "stops": [{ "at": 0, "color": "#06121e" }, { "at": 1, "color": "#0d2436" }], "band": { "color": "#3a2d18", "height": 0.08 }, "drift": { "period": 30000, "amount": 0.12 } }`
|
|
77
106
|
— vertical gradient (`at` 0 = top → 1 = bottom); optional solid `band` at the
|
|
78
|
-
bottom (e.g. an aquarium seafloor)
|
|
107
|
+
bottom (e.g. an aquarium seafloor); optional `drift` slowly oscillates the
|
|
108
|
+
stop positions (period ≥ 10 s) so the background breathes. All colours are
|
|
109
|
+
hex (`#rgb` / `#rrggbb`).
|
|
79
110
|
|
|
80
111
|
### `layers[]`
|
|
81
112
|
|
|
@@ -84,52 +115,157 @@ satisfy `min ≤ max`.
|
|
|
84
115
|
| `count` | int 1..400 | — | entities in this layer |
|
|
85
116
|
| `sprite` | SpriteSpec | — | what each entity looks like |
|
|
86
117
|
| `motion` | MotionSpec | — | how entities move |
|
|
87
|
-
| `size` | `[min,max]`
|
|
118
|
+
| `size` | `[min,max]` > 0 | sprite-dependent | font size for emoji/text; ignored for shaped sprites |
|
|
88
119
|
| `wrap` | boolean | `true` | wrap to the opposite edge when leaving the viewport |
|
|
89
120
|
| `flip` | boolean | `false` | mirror the sprite to face its heading |
|
|
90
121
|
| `alpha` | `[min,max]` 0..1 | `[1,1]` | per-entity opacity range |
|
|
91
|
-
| `blend` | `
|
|
122
|
+
| `blend` | `lighter` \| `screen` \| `multiply` | source-over | additive glow / gentle additive / darkening |
|
|
92
123
|
| `region` | `{x?, y?}` ranges 0..1 | full viewport | fractional spawn window (placement only, not travel) |
|
|
93
|
-
| `pulse` | `{amp ≤ 0.5, period ≥ 500}` | none |
|
|
124
|
+
| `pulse` | `{amp ≤ 0.5, period ≥ 500, wave?}` | none | opacity breathing; `wave: {wavelength, angle?}` turns it into a traveling wave across the field |
|
|
125
|
+
| `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) |
|
|
126
|
+
| `grow` | `{amp ≤ 0.8, period ≥ 500}` | none | size breathing (seeded phase) |
|
|
127
|
+
| `trail` | `{length ≤ 5000, fade?}` | none | analytic afterglow trail (ms of history) |
|
|
128
|
+
| `links` | see below | none | inter-entity lines |
|
|
129
|
+
| `layout` | `{type: "grid", columns?, jitter?}` | scatter | grid placement; `jitter` scalar or `{x?, y?}` 0..1 per axis |
|
|
130
|
+
| `life` | `{enter?, exit?, fade?}` ms | always on | act structure: fade the layer in at `enter`, out at `exit` |
|
|
94
131
|
| `key` | string | none | addressable name → `setParam("key.field", …)` |
|
|
95
|
-
| `position` | `{x, y}` 0..1 | none | exact placement; **requires `count: 1`**; overrides `region` |
|
|
132
|
+
| `position` | `{x, y}` 0..1 | none | exact placement; **requires `count: 1`**; overrides `region`/`layout` |
|
|
133
|
+
|
|
134
|
+
`links`: `{ k: 1..8, maxDist, color?, alpha?, width?, mode?, falloff?, closed? }`.
|
|
135
|
+
`mode: "nearest"` (default) wires each entity to its k nearest neighbors within
|
|
136
|
+
`maxDist`; `"chain"` wires entities in order (Mystify polygons — ignores
|
|
137
|
+
`k`/`maxDist`, `closed: true` closes the loop); `"random"` uses a fixed
|
|
138
|
+
deterministic wiring filtered by `maxDist`. `falloff: true` fades link alpha
|
|
139
|
+
with distance, removing pop-in at the cutoff.
|
|
96
140
|
|
|
97
141
|
### `sprite` (one of)
|
|
98
142
|
|
|
99
|
-
- `{ "kind": "emoji", "glyphs": ["🐟", "🐠"]
|
|
100
|
-
|
|
101
|
-
- `{ "kind": "
|
|
143
|
+
- `{ "kind": "emoji", "glyphs": ["🐟", "🐠"], "cycle": { "period": 800 } }` —
|
|
144
|
+
glyph picked per entity (seeded); optional `cycle` rotates variants over time
|
|
145
|
+
- `{ "kind": "text", "strings": ["HELLO"], "color": "#e6e8ef", "font": "bold monospace", "align": "center", "baseline": "middle", "maxWidth": 300, "cycle": ... }`
|
|
146
|
+
— a `font` **with** a px size is used verbatim; a family/weight only
|
|
147
|
+
(`"bold monospace"`) composes with the seeded per-entity `size`
|
|
148
|
+
- `{ "kind": "circle", "radius": [0.001, 0.003], "color": "#ffffff", "soft": true }` —
|
|
102
149
|
`soft` renders a radial-falloff glow orb instead of a hard disc
|
|
150
|
+
- `{ "kind": "ring", "radius": [0.002, 0.005], "color": "#d8f6ff", "width": 0.001 }` —
|
|
151
|
+
unfilled circle (bubbles, portals, sonar)
|
|
152
|
+
- `{ "kind": "streak", "length": [0.01, 0.03], "color": "#cfd8ff", "width": 0.002 }` —
|
|
153
|
+
a line oriented along the entity's analytic heading with a faded tail
|
|
154
|
+
(rain that reads as rain, shooting stars, warp stars)
|
|
155
|
+
- `{ "kind": "rect", "width": [0.012, 0.02], "aspect": [1.3, 1.7], "color": "#ffb347" }` —
|
|
156
|
+
rectangle; `aspect` is the height/width ratio range (rotates with `spin`)
|
|
157
|
+
|
|
158
|
+
`circle`, `ring`, `streak`, and `rect` all accept `colors: [...]` (seeded
|
|
159
|
+
per-entity palette pick) and `colorWeights: [...]` (relative weights, same
|
|
160
|
+
length — "mostly cool tones, occasional ember").
|
|
103
161
|
|
|
104
162
|
### `motion` (one of)
|
|
105
163
|
|
|
106
|
-
- `{ "type": "drift", "speed": [
|
|
164
|
+
- `{ "type": "drift", "speed": [0.02, 0.06], "angle": 0, "bidirectional": true, "bob": 0.004 }`
|
|
107
165
|
— heading in degrees (0 = right, 90 = down); `bidirectional` flips horizontal
|
|
108
|
-
direction per entity (fish); `bob` adds vertical wobble
|
|
109
|
-
- `{ "type": "rise", "speed": [
|
|
110
|
-
- `{ "type": "bounce", "speed": [
|
|
166
|
+
direction per entity (fish); `bob` adds vertical wobble
|
|
167
|
+
- `{ "type": "rise", "speed": [0.01, 0.03], "sway": 0.006 }` — upward, with horizontal sway (bubbles)
|
|
168
|
+
- `{ "type": "bounce", "speed": [0.12, 0.24] }` — diagonal, reflecting off edges
|
|
111
169
|
- `{ "type": "static" }` — stays exactly where placed (use with `position` for HUD text)
|
|
170
|
+
- `{ "type": "orbit", "speed": [10, 40], "radius": [0.05, 0.2], "center": { "x": 0.5, "y": 0.5 } }`
|
|
171
|
+
— angular speed in deg/sec; `center` may instead be `{ "layer": "planet" }`
|
|
172
|
+
to orbit a single-entity parent layer (moons around a wandering planet;
|
|
173
|
+
strictly one level deep)
|
|
174
|
+
- `{ "type": "wander", "speed": [0.004, 0.012], "angle": 0, "meander": 0.1, "coherence": 0.7 }`
|
|
175
|
+
— organic harmonic drift: base velocity plus 3 seeded sine octaves per axis.
|
|
176
|
+
Flowing curved paths with zero simulation state. `meander` scales the
|
|
177
|
+
curvature; `coherence` 0..1 blends every entity toward a shared harmonic set
|
|
178
|
+
(1 = the layer undulates as one body — fake flocking)
|
|
179
|
+
- `{ "type": "warp", "speed": [0.1, 0.5], "center": { "x": 0.5, "y": 0.5 } }`
|
|
180
|
+
— perspective starfield: entities stream toward the viewer on a depth axis,
|
|
181
|
+
size/alpha/velocity scaling with 1/z. Pair with `streak` sprites for the
|
|
182
|
+
classic warp tunnel
|
|
183
|
+
- `{ "type": "path", "points": [{"x":0.1,"y":0.7}, ...], "duration": 30000, "curve": "smooth", "closed": true, "scatter": 0.02 }`
|
|
184
|
+
— choreographed spline (Catmull-Rom, or `linear`); 2..24 waypoints, lap time
|
|
185
|
+
≥ 2 s; `closed` loops (default), open paths ping-pong; `scatter` offsets
|
|
186
|
+
entities sharing the path; each entity gets a seeded phase along it
|
|
112
187
|
|
|
113
|
-
All speeds are
|
|
188
|
+
All speeds are ranges; each entity draws its own value (seeded).
|
|
114
189
|
|
|
115
190
|
## Determinism contract
|
|
116
191
|
|
|
117
192
|
Entity construction consumes the seeded RNG in a **fixed draw order** per layer.
|
|
118
193
|
New optional fields only consume RNG draws when present, so adding features to
|
|
119
194
|
the format never changes the entity stream of an existing spec — a spec authored
|
|
120
|
-
against an older runtime renders bit-identically on a newer one
|
|
195
|
+
against an older runtime renders bit-identically on a newer one
|
|
196
|
+
(`determinism-baseline.test.ts` guards this with snapshot streams).
|
|
197
|
+
|
|
198
|
+
Positions are **analytic**: `positionAt(entity, t)` is a pure function, so
|
|
199
|
+
`renderFrame(t, seed)` can seek anywhere instantly, trails sample the past for
|
|
200
|
+
free, and streak headings derive from a finite difference. `ghosting` is the
|
|
201
|
+
one frame-coupled effect, and it preserves seekability by replaying a bounded
|
|
202
|
+
fixed-step warm-up (≤ 120 frames) from a full clear on any non-contiguous seek.
|
|
121
203
|
|
|
122
204
|
## Steering
|
|
123
205
|
|
|
124
206
|
Compiled specs accept live parameter changes via dot-paths —
|
|
125
207
|
`layers.0.count`, `background.stops.0.color`, or `key`-based paths like
|
|
126
208
|
`cpu-gauge.color` when layers declare `key`. Changes interpolate over a
|
|
127
|
-
control-track (`step` | `linear` | `smooth`).
|
|
128
|
-
|
|
209
|
+
control-track (`step` | `linear` | `smooth`). Placement/motion changes trigger
|
|
210
|
+
a deterministic rebuild (same seed → same stream). See `@idle-screens/core`
|
|
211
|
+
for `ControlTrack` and the idlescreens.com MCP `setParam` tool.
|
|
212
|
+
|
|
213
|
+
## Seeing without eyes — the perception API
|
|
214
|
+
|
|
215
|
+
`src/perceive.ts` translates a spec into modalities a **non-vision agent** can
|
|
216
|
+
reason about, computed analytically from the entity model (no canvas, no
|
|
217
|
+
renderer, deterministic, Node-safe). Note: the analytical model approximates
|
|
218
|
+
but does not perfectly match canvas rendering — blend modes are simplified
|
|
219
|
+
(`screen` ≈ `lighter`), wrapped link segments use straight interpolation, and
|
|
220
|
+
background drift is sampled at rest. These are documented trade-offs for a
|
|
221
|
+
zero-dependency, renderer-free analysis tool.
|
|
222
|
+
|
|
223
|
+
- `perceiveScene(spec, {t?, viewport?, seed?})` — one-call bundle: everything below.
|
|
224
|
+
- `luminanceGrid(spec, opts)` — an 80×48 luminance image of the composed frame
|
|
225
|
+
(background gradient + entities + link lines, blend-aware), with coverage,
|
|
226
|
+
visual-mass centroid, and **row/column deviation profiles** (1D transects of
|
|
227
|
+
the composition).
|
|
228
|
+
- `renderBrailleMap(grid)` — the compact picture: 12 lines × 40 braille chars,
|
|
229
|
+
each char a 2×4 dot cell, ordered-dithered with auto-exposure. An agent reads
|
|
230
|
+
the whole frame at once instead of reconstructing it entity by entity.
|
|
231
|
+
- `renderDensityMap(grid)` — a **higher-fidelity** picture: one ASCII density
|
|
232
|
+
char (` .:-=+*#%@`) per grid cell, so it's 1:1 with the grid. Pair with a
|
|
233
|
+
larger `cols`/`rows` (e.g. `{cols: 120, rows: 48}`) to resolve structure the
|
|
234
|
+
braille map smears — concentric rings, grids, text blocks. This is the sharper
|
|
235
|
+
read when composition detail matters.
|
|
236
|
+
- `dominanceRanking(spec, opts)` — layers ranked by estimated visual weight
|
|
237
|
+
(area × alpha × contrast × glow/motion boosts; links count) — *where the eye
|
|
238
|
+
goes*, normalized to shares. Thin-but-bright structures (rings, streaks, link
|
|
239
|
+
lines) get a line-salience boost so they aren't crushed by filled discs.
|
|
240
|
+
- `motionStats(spec, opts)` — per-layer mean/max on-screen speed from analytic
|
|
241
|
+
displacement — choreography as numbers.
|
|
242
|
+
- `textSprites(spec, opts)` — the literal strings and rendered sizes of every
|
|
243
|
+
text layer. Glyphs are invisible in the luminance maps, so this is the only
|
|
244
|
+
way to confirm *what words* are on screen and how big. (Also on
|
|
245
|
+
`perceiveScene().text`.)
|
|
246
|
+
- `diffScenes(a, b, opts)` — **relative sight**: coverage/luminance deltas,
|
|
247
|
+
visual-balance shift, 3×3 region deltas, dominance-rank movement, and
|
|
248
|
+
advisory codes added/removed. Agents judge "is B better than A" far more
|
|
249
|
+
reliably than "is A good"; this is the edit loop's primary instrument.
|
|
250
|
+
|
|
251
|
+
Coverage is **additive-glow-calibrated**: soft circles drawn with
|
|
252
|
+
`blend: lighter`/`screen` bloom past their radius (as they do on the real
|
|
253
|
+
canvas), so `coverage` tracks what the audience sees instead of under-reporting
|
|
254
|
+
glow-heavy scenes by an order of magnitude.
|
|
255
|
+
|
|
256
|
+
Known approximations (deliberate): trails and ghosting are not sampled, text ink
|
|
257
|
+
is estimated from font size × string length. Good enough to perceive
|
|
258
|
+
composition, focus, balance, and motion — not a substitute for a human (or VLM)
|
|
259
|
+
judgement of beauty.
|
|
129
260
|
|
|
130
261
|
## Examples
|
|
131
262
|
|
|
132
|
-
Shipped working specs (also exposed as `EXAMPLE_SPECS`
|
|
133
|
-
`snowfall`, `lanterns`, `sakura`,
|
|
134
|
-
|
|
263
|
+
Shipped working specs (also exposed as `EXAMPLE_SPECS` /
|
|
264
|
+
`SCHEMA_EXAMPLES`): `aquarium`, `rain`, `snowfall`, `lanterns`, `sakura`,
|
|
265
|
+
`dev-dashboard`, `orrery`, `constellation`, `comets`, plus the v1-ceiling
|
|
266
|
+
showcases — `aurora` (wander + coherence + ghosting + pulse.wave),
|
|
267
|
+
`warp-tunnel` (warp + streaks), `polygons` (chain links + heavy ghosting),
|
|
268
|
+
`matrix-rain` (grid layout + glyph cycle + ghosting), and `procession`
|
|
269
|
+
(path + layer-parented orbit + life staging + ring/rect sprites). See
|
|
270
|
+
[`src/examples/`](./src/examples/). The dashboard exercises the static/HUD
|
|
135
271
|
subset at scale (34 layers of keyed, positioned text).
|