@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 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 };