@idle-screens/schema 1.0.0 → 2.0.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 +135 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.js +354 -191
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
- package/saver-spec.schema.json +224 -0
package/FORMAT.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# SaverSpec — Format Specification (version 1)
|
|
2
|
+
|
|
3
|
+
The declarative, agent-authorable screensaver format compiled by
|
|
4
|
+
`@idle-screens/schema`. A spec describes a saver as **data** — a static
|
|
5
|
+
background plus layers of moving sprites — which `compileSaver()` turns into a
|
|
6
|
+
seeded, deterministic, flash-safe `SaverPlugin`. There is no code in a spec: no
|
|
7
|
+
scripting, no network access, no DOM access.
|
|
8
|
+
|
|
9
|
+
- **Machine-readable schema:** [`saver-spec.schema.json`](./saver-spec.schema.json)
|
|
10
|
+
(JSON Schema draft-07), importable as
|
|
11
|
+
`@idle-screens/schema/saver-spec.schema.json`.
|
|
12
|
+
- **Runtime validator:** `validateSpec(spec)` / `assertValidSpec(spec)` — used by
|
|
13
|
+
`compileSaver()`, which refuses to run an invalid spec.
|
|
14
|
+
|
|
15
|
+
## Versioning
|
|
16
|
+
|
|
17
|
+
The **format version** (`schemaVersion`, currently `1`) is independent of the
|
|
18
|
+
npm package version. The package may ship breaking API changes without bumping
|
|
19
|
+
the format; the format only bumps when existing specs would parse or render
|
|
20
|
+
differently. New *optional* fields may be added within a format version —
|
|
21
|
+
older runtimes ignore unknown fields (see Validation semantics).
|
|
22
|
+
|
|
23
|
+
Additions to version 1 so far, all optional and backward compatible:
|
|
24
|
+
`alpha`, `blend`, `pulse`, `region` (layer appearance/placement);
|
|
25
|
+
`static` motion, `position`, `key` (dashboard/HUD support);
|
|
26
|
+
`align`, `baseline`, `maxWidth` (text sprites).
|
|
27
|
+
|
|
28
|
+
## Safety invariants
|
|
29
|
+
|
|
30
|
+
These hold **by construction** — no spec can violate them:
|
|
31
|
+
|
|
32
|
+
1. **No flash primitive.** The background is static and entities are bounded
|
|
33
|
+
sprites, so a compiled spec cannot strobe the full field. Provable by
|
|
34
|
+
sampling any compiled spec through `@idle-screens/validator`.
|
|
35
|
+
2. **Pulse is bounded.** Opacity breathing amplitude is capped at 0.5 and its
|
|
36
|
+
period floored at 500 ms (2 Hz — under the WCAG 3 Hz flash threshold), and
|
|
37
|
+
every entity gets its own seeded phase, so a layer can never pulse in unison.
|
|
38
|
+
3. **Motion is bounded.** Speeds are capped at 4000 px/sec.
|
|
39
|
+
4. **Work is bounded.** ≤ 36 layers, ≤ 400 entities per layer, ≤ 800 total.
|
|
40
|
+
5. **Determinism.** All randomness comes from a seeded PRNG. Same spec + same
|
|
41
|
+
seed ⇒ identical entity streams and identical frames at any time `t`
|
|
42
|
+
(compiled savers expose `renderFrame(t, seed)`).
|
|
43
|
+
|
|
44
|
+
## Validation semantics
|
|
45
|
+
|
|
46
|
+
Two validators exist with deliberately different strictness:
|
|
47
|
+
|
|
48
|
+
| | unknown fields | everything else |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `saver-spec.schema.json` | **rejected** (catches authoring typos) | identical |
|
|
51
|
+
| `validateSpec()` (runtime) | **ignored** (forward compatibility) | identical |
|
|
52
|
+
|
|
53
|
+
Author against the JSON Schema; ship through the runtime validator.
|
|
54
|
+
|
|
55
|
+
Two rules the JSON Schema cannot fully express (the runtime enforces them):
|
|
56
|
+
total entities across all layers ≤ 800, and every `[min, max]` range must
|
|
57
|
+
satisfy `min ≤ max`.
|
|
58
|
+
|
|
59
|
+
## Structure
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
{
|
|
63
|
+
"schemaVersion": 1,
|
|
64
|
+
"id": "snowfall", // unique kebab-case id
|
|
65
|
+
"label": "Snowfall",
|
|
66
|
+
"seed": 42, // optional; falls back to the host's seed
|
|
67
|
+
"motionIntensity": "calm", // optional: calm | moderate | energetic
|
|
68
|
+
"background": { ... }, // optional; defaults to black
|
|
69
|
+
"layers": [ { ... }, ... ] // 1..36, rendered back-to-front
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `background`
|
|
74
|
+
|
|
75
|
+
- `{ "type": "solid", "color": "#06060c" }`
|
|
76
|
+
- `{ "type": "gradient", "stops": [{ "at": 0, "color": "#06121e" }, { "at": 1, "color": "#0d2436" }], "band": { "color": "#3a2d18", "height": 90 } }`
|
|
77
|
+
— vertical gradient (`at` 0 = top → 1 = bottom); optional solid `band` at the
|
|
78
|
+
bottom (e.g. an aquarium seafloor). All colours are hex (`#rgb` / `#rrggbb`).
|
|
79
|
+
|
|
80
|
+
### `layers[]`
|
|
81
|
+
|
|
82
|
+
| Field | Type | Default | Meaning |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `count` | int 1..400 | — | entities in this layer |
|
|
85
|
+
| `sprite` | SpriteSpec | — | what each entity looks like |
|
|
86
|
+
| `motion` | MotionSpec | — | how entities move |
|
|
87
|
+
| `size` | `[min,max]` px > 0 | sprite-dependent | font size for emoji/text; ignored for circle |
|
|
88
|
+
| `wrap` | boolean | `true` | wrap to the opposite edge when leaving the viewport |
|
|
89
|
+
| `flip` | boolean | `false` | mirror the sprite to face its heading |
|
|
90
|
+
| `alpha` | `[min,max]` 0..1 | `[1,1]` | per-entity opacity range |
|
|
91
|
+
| `blend` | `"lighter"` | source-over | additive compositing (glow stacking) |
|
|
92
|
+
| `region` | `{x?, y?}` ranges 0..1 | full viewport | fractional spawn window (placement only, not travel) |
|
|
93
|
+
| `pulse` | `{amp ≤ 0.5, period ≥ 500}` | none | seeded-phase opacity breathing |
|
|
94
|
+
| `key` | string | none | addressable name → `setParam("key.field", …)` |
|
|
95
|
+
| `position` | `{x, y}` 0..1 | none | exact placement; **requires `count: 1`**; overrides `region` |
|
|
96
|
+
|
|
97
|
+
### `sprite` (one of)
|
|
98
|
+
|
|
99
|
+
- `{ "kind": "emoji", "glyphs": ["🐟", "🐠"] }` — glyph picked per entity (seeded)
|
|
100
|
+
- `{ "kind": "text", "strings": ["HELLO"], "color": "#e6e8ef", "font": "bold 24px monospace", "align": "center", "baseline": "middle", "maxWidth": 300 }`
|
|
101
|
+
- `{ "kind": "circle", "radius": [1, 3], "color": "#ffffff", "soft": true }` —
|
|
102
|
+
`soft` renders a radial-falloff glow orb instead of a hard disc
|
|
103
|
+
|
|
104
|
+
### `motion` (one of)
|
|
105
|
+
|
|
106
|
+
- `{ "type": "drift", "speed": [20, 60], "angle": 0, "bidirectional": true, "bob": 4 }`
|
|
107
|
+
— heading in degrees (0 = right, 90 = down); `bidirectional` flips horizontal
|
|
108
|
+
direction per entity (fish); `bob` adds vertical wobble (px)
|
|
109
|
+
- `{ "type": "rise", "speed": [10, 30], "sway": 6 }` — upward, with horizontal sway (bubbles)
|
|
110
|
+
- `{ "type": "bounce", "speed": [120, 240] }` — diagonal, reflecting off edges
|
|
111
|
+
- `{ "type": "static" }` — stays exactly where placed (use with `position` for HUD text)
|
|
112
|
+
|
|
113
|
+
All speeds are px/sec ranges; each entity draws its own value (seeded).
|
|
114
|
+
|
|
115
|
+
## Determinism contract
|
|
116
|
+
|
|
117
|
+
Entity construction consumes the seeded RNG in a **fixed draw order** per layer.
|
|
118
|
+
New optional fields only consume RNG draws when present, so adding features to
|
|
119
|
+
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.
|
|
121
|
+
|
|
122
|
+
## Steering
|
|
123
|
+
|
|
124
|
+
Compiled specs accept live parameter changes via dot-paths —
|
|
125
|
+
`layers.0.count`, `background.stops.0.color`, or `key`-based paths like
|
|
126
|
+
`cpu-gauge.color` when layers declare `key`. Changes interpolate over a
|
|
127
|
+
control-track (`step` | `linear` | `smooth`). See `@idle-screens/core` for
|
|
128
|
+
`ControlTrack` and the idlescreens.com MCP `setParam` tool.
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
Shipped working specs (also exposed as `EXAMPLE_SPECS`): `aquarium`, `rain`,
|
|
133
|
+
`snowfall`, `lanterns`, `sakura`, `dev-dashboard` — see
|
|
134
|
+
[`src/examples.ts`](./src/examples.ts). The dashboard exercises the static/HUD
|
|
135
|
+
subset at scale (34 layers of keyed, positioned text).
|
package/dist/index.d.ts
CHANGED
|
@@ -204,16 +204,57 @@ declare function manifestFor(spec: SaverSpec): SaverManifest;
|
|
|
204
204
|
*/
|
|
205
205
|
declare function compileSaver(spec: unknown): SaverPlugin;
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Pure helpers for LIVE steering of compiled specs (setParam / applyTrack).
|
|
209
|
+
* A track's deltas address spec fields by dot-path ("layers.0.count",
|
|
210
|
+
* "background.stops.1.color") or layer key ("fireflies.count"). Steering
|
|
211
|
+
* changes existing values only — unknown paths are ignored (the server
|
|
212
|
+
* validates and rejects them; the runtime stays lenient).
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
interface PathTarget {
|
|
216
|
+
parent: Record<string, unknown> | unknown[];
|
|
217
|
+
key: string | number;
|
|
218
|
+
}
|
|
219
|
+
/** Resolve a dot-path (key-aware) to its parent + final key; null if absent. */
|
|
220
|
+
declare function resolveSpecPath(spec: unknown, path: string): PathTarget | null;
|
|
221
|
+
/** A steering delta as carried on a channel control-track. */
|
|
222
|
+
interface SteerDelta {
|
|
223
|
+
t: number;
|
|
224
|
+
path: string;
|
|
225
|
+
value: unknown;
|
|
226
|
+
ease?: string;
|
|
227
|
+
dur?: number;
|
|
228
|
+
}
|
|
229
|
+
/** Apply deltas to a deep copy of the spec (last-wins, target values). */
|
|
230
|
+
declare function applyDeltasToSpec(spec: SaverSpec, deltas: SteerDelta[]): SaverSpec;
|
|
231
|
+
/**
|
|
232
|
+
* Interpolate between two structurally-similar specs at progress k (0..1).
|
|
233
|
+
* Numbers lerp, hex colours lerp per channel, everything else steps to the
|
|
234
|
+
* target immediately (k > 0). Layer counts round to integers.
|
|
235
|
+
*/
|
|
236
|
+
declare function lerpSpec(from: SaverSpec, to: SaverSpec, k: number): SaverSpec;
|
|
237
|
+
/** Smooth (ease-in-out) progress curve used for glides. */
|
|
238
|
+
declare function easeSmooth(k: number): number;
|
|
239
|
+
/**
|
|
240
|
+
* Fields that require re-seeding entities when they change (placement/motion
|
|
241
|
+
* are baked at build time). Colour/alpha/pulse-only changes redraw in place.
|
|
242
|
+
*/
|
|
243
|
+
declare function structuralSignature(spec: SaverSpec): string;
|
|
244
|
+
|
|
207
245
|
/**
|
|
208
246
|
* Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
|
|
209
247
|
* that the schema is actually expressive: two layers (fish drifting both ways at varied
|
|
210
248
|
* speeds with a bob + flip, over a gradient with a seafloor band; bubbles rising with sway).
|
|
211
249
|
*/
|
|
212
250
|
declare const AQUARIUM_SPEC: SaverSpec;
|
|
251
|
+
|
|
213
252
|
/** Rain: dense text streaks falling straight down over a near-black sky. */
|
|
214
253
|
declare const RAIN_SPEC: SaverSpec;
|
|
254
|
+
|
|
215
255
|
/** Snowfall: three depth layers of white circles drifting down with horizontal bob. */
|
|
216
256
|
declare const SNOWFALL_SPEC: SaverSpec;
|
|
257
|
+
|
|
217
258
|
/**
|
|
218
259
|
* Night Lanterns (Yi Peng festival sky): depth from four correlated parallax cues —
|
|
219
260
|
* closer layers are LARGER, FASTER, SWAY WIDER, and glow WARMER/BRIGHTER. v2 of this
|
|
@@ -224,6 +265,7 @@ declare const SNOWFALL_SPEC: SaverSpec;
|
|
|
224
265
|
* the water line).
|
|
225
266
|
*/
|
|
226
267
|
declare const LANTERNS_SPEC: SaverSpec;
|
|
268
|
+
|
|
227
269
|
/**
|
|
228
270
|
* Sakura Drift: cherry-blossom petals carried on a spring twilight breeze. Five
|
|
229
271
|
* correlated-parallax layers build the depth — the far backdrop is a barely-moving
|
|
@@ -236,6 +278,7 @@ declare const LANTERNS_SPEC: SaverSpec;
|
|
|
236
278
|
* diagonal breeze, and from lanterns by falling rather than rising.
|
|
237
279
|
*/
|
|
238
280
|
declare const SAKURA_SPEC: SaverSpec;
|
|
281
|
+
|
|
239
282
|
/**
|
|
240
283
|
* Dev Dashboard: a dense ops-style HUD exercising static positioning, keyed layers,
|
|
241
284
|
* and text alignment at scale. Two ambient particle layers provide depth behind 28
|
|
@@ -243,6 +286,18 @@ declare const SAKURA_SPEC: SaverSpec;
|
|
|
243
286
|
* file list, and scrolling event log.
|
|
244
287
|
*/
|
|
245
288
|
declare const DASHBOARD_SPEC: SaverSpec;
|
|
289
|
+
|
|
290
|
+
/** Catalog entry for a bundled schema example. */
|
|
291
|
+
interface SchemaExample {
|
|
292
|
+
id: string;
|
|
293
|
+
label: string;
|
|
294
|
+
spec: SaverSpec;
|
|
295
|
+
}
|
|
296
|
+
/** Ordered catalog of bundled schema examples. Add new specs as standalone files, then register here. */
|
|
297
|
+
declare const SCHEMA_EXAMPLES: readonly SchemaExample[];
|
|
298
|
+
/** All example specs in catalog order (tests, batch compile). */
|
|
246
299
|
declare const EXAMPLE_SPECS: SaverSpec[];
|
|
300
|
+
/** Lookup by saver id. */
|
|
301
|
+
declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
|
|
247
302
|
|
|
248
|
-
export { AQUARIUM_SPEC, type BackgroundSpec, type BandSpec, DASHBOARD_SPEC, EXAMPLE_SPECS, type Entity, type GradientStop, LANTERNS_SPEC, LIMITS, type LayerSpec, type MotionSpec, type Placed, RAIN_SPEC, SAKURA_SPEC, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SpecError, type SpriteSpec, type ValidationResult, alphaAt, assertValidSpec, buildEntities, compileSaver, manifestFor, positionAt, spriteVariants, validateSpec };
|
|
303
|
+
export { AQUARIUM_SPEC, type BackgroundSpec, type BandSpec, DASHBOARD_SPEC, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, LANTERNS_SPEC, LIMITS, type LayerSpec, type MotionSpec, type Placed, RAIN_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SchemaExample, type SpecError, type SpriteSpec, type SteerDelta, type ValidationResult, alphaAt, applyDeltasToSpec, assertValidSpec, buildEntities, compileSaver, easeSmooth, lerpSpec, manifestFor, positionAt, resolveSpecPath, spriteVariants, structuralSignature, validateSpec };
|