@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.
@@ -1,29 +1,16 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://idlescreens.com/schemas/saver-spec-1.schema.json",
4
- "title": "SaverSpec (format version 1)",
5
- "description": "Declarative, agent-authorable screensaver: a static background plus layers of moving sprites. No code, no network, no strobe primitive — safety by construction. This schema is stricter than the runtime validator (unknown properties are rejected here to catch authoring typos; the runtime ignores them). Cross-field rules the schema cannot express: total entities across all layers must be <= 800; range pairs must satisfy min <= max.",
6
- "type": "object",
7
- "required": ["schemaVersion", "id", "label", "layers"],
8
- "additionalProperties": false,
9
- "properties": {
10
- "schemaVersion": { "const": 1, "description": "Format version. Independent of the npm package version." },
11
- "id": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Unique kebab-case saver id." },
12
- "label": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Human-readable name." },
13
- "seed": { "type": "number", "description": "Deterministic RNG seed. Same spec + same seed = identical frames. Falls back to the host's seed." },
14
- "motionIntensity": { "enum": ["calm", "moderate", "energetic"], "description": "Advisory intensity for capability/reduced-motion gating." },
15
- "units": { "enum": ["px", "viewport"], "description": "Dimensional unit system. 'viewport' = all sizes/speeds/distances are fractions of min(w,h)." },
16
- "referenceViewport": { "type": "number", "minimum": 100, "maximum": 8640, "description": "Resolution the spec was designed at (default 1080). Entity counts density-scale above it." },
17
- "ghosting": { "type": "number", "minimum": 0, "maximum": 0.95, "description": "Frame persistence 0..0.95: each frame composites over a faded copy of the last, so moving entities smear (Mystify trails, long-exposure light). Flash-safe — only smooths luminance. Seeks stay deterministic via a fixed-step warm-up replay." },
18
- "background": { "$ref": "#/definitions/background" },
19
- "layers": {
20
- "type": "array",
21
- "minItems": 1,
22
- "maxItems": 36,
23
- "items": { "$ref": "#/definitions/layer" },
24
- "description": "Rendered back-to-front. At most 36 layers; at most 800 entities total."
4
+ "title": "Idle Screens document (format version 1)",
5
+ "description": "Either a SaverSpec (declarative screensaver scene) or an idle-sequence envelope (multi-segment timeline over SaverSpecs). Discriminated by the presence of `format: \"idle-sequence\"`. This schema is stricter than the runtime validators (unknown properties are rejected here to catch authoring typos; the runtime ignores them). Cross-field rules the schema cannot express for SaverSpecs: total entities across all layers must be <= 800; range pairs must satisfy min <= max. For sequences: unique segment keys; durationless only on the final segment; loop requires all durations.",
6
+ "oneOf": [
7
+ {
8
+ "$ref": "#/definitions/saverSpec"
9
+ },
10
+ {
11
+ "$ref": "#/definitions/idleSequence"
25
12
  }
26
- },
13
+ ],
27
14
  "definitions": {
28
15
  "hexColor": {
29
16
  "type": "string",
@@ -32,21 +19,32 @@
32
19
  },
33
20
  "range01": {
34
21
  "type": "array",
35
- "items": { "type": "number", "minimum": 0, "maximum": 1 },
22
+ "items": {
23
+ "type": "number",
24
+ "minimum": 0,
25
+ "maximum": 1
26
+ },
36
27
  "minItems": 2,
37
28
  "maxItems": 2,
38
29
  "description": "[min, max] with min <= max, both 0..1."
39
30
  },
40
31
  "positiveRange": {
41
32
  "type": "array",
42
- "items": { "type": "number", "exclusiveMinimum": 0 },
33
+ "items": {
34
+ "type": "number",
35
+ "exclusiveMinimum": 0
36
+ },
43
37
  "minItems": 2,
44
38
  "maxItems": 2,
45
39
  "description": "[min, max] px with 0 < min <= max."
46
40
  },
47
41
  "speedRange": {
48
42
  "type": "array",
49
- "items": { "type": "number", "minimum": 0, "maximum": 4000 },
43
+ "items": {
44
+ "type": "number",
45
+ "minimum": 0,
46
+ "maximum": 4000
47
+ },
50
48
  "minItems": 2,
51
49
  "maxItems": 2,
52
50
  "description": "[min, max] px/sec, capped at 4000 so nothing teleports."
@@ -55,51 +53,93 @@
55
53
  "oneOf": [
56
54
  {
57
55
  "type": "object",
58
- "required": ["type", "color"],
56
+ "required": [
57
+ "type",
58
+ "color"
59
+ ],
59
60
  "additionalProperties": false,
60
61
  "properties": {
61
- "type": { "const": "solid" },
62
- "color": { "$ref": "#/definitions/hexColor" }
62
+ "type": {
63
+ "const": "solid"
64
+ },
65
+ "color": {
66
+ "$ref": "#/definitions/hexColor"
67
+ }
63
68
  }
64
69
  },
65
70
  {
66
71
  "type": "object",
67
- "required": ["type", "stops"],
72
+ "required": [
73
+ "type",
74
+ "stops"
75
+ ],
68
76
  "additionalProperties": false,
69
77
  "properties": {
70
- "type": { "const": "gradient" },
78
+ "type": {
79
+ "const": "gradient"
80
+ },
71
81
  "stops": {
72
82
  "type": "array",
73
83
  "minItems": 2,
74
84
  "items": {
75
85
  "type": "object",
76
- "required": ["at", "color"],
86
+ "required": [
87
+ "at",
88
+ "color"
89
+ ],
77
90
  "additionalProperties": false,
78
91
  "properties": {
79
- "at": { "type": "number", "minimum": 0, "maximum": 1, "description": "0 = top, 1 = bottom." },
80
- "color": { "$ref": "#/definitions/hexColor" }
92
+ "at": {
93
+ "type": "number",
94
+ "minimum": 0,
95
+ "maximum": 1,
96
+ "description": "0 = top, 1 = bottom."
97
+ },
98
+ "color": {
99
+ "$ref": "#/definitions/hexColor"
100
+ }
81
101
  }
82
102
  },
83
103
  "description": "Vertical gradient stops, top to bottom."
84
104
  },
85
105
  "band": {
86
106
  "type": "object",
87
- "required": ["color", "height"],
107
+ "required": [
108
+ "color",
109
+ "height"
110
+ ],
88
111
  "additionalProperties": false,
89
112
  "properties": {
90
- "color": { "$ref": "#/definitions/hexColor" },
91
- "height": { "type": "number", "exclusiveMinimum": 0, "description": "Band height in px (e.g. an aquarium seafloor)." }
113
+ "color": {
114
+ "$ref": "#/definitions/hexColor"
115
+ },
116
+ "height": {
117
+ "type": "number",
118
+ "exclusiveMinimum": 0,
119
+ "description": "Band height in px (e.g. an aquarium seafloor)."
120
+ }
92
121
  }
93
122
  },
94
123
  "drift": {
95
124
  "type": "object",
96
- "required": ["period"],
125
+ "required": [
126
+ "period"
127
+ ],
97
128
  "additionalProperties": false,
98
129
  "properties": {
99
- "period": { "type": "number", "minimum": 10000, "description": "Full cycle period in ms. Floor of 10 s." },
100
- "amount": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.3, "description": "How far stops shift (fraction of 0..1 range). Default 0.15, max 0.3." }
130
+ "period": {
131
+ "type": "number",
132
+ "minimum": 10000,
133
+ "description": "Full cycle period in ms. Floor of 10 s."
134
+ },
135
+ "amount": {
136
+ "type": "number",
137
+ "exclusiveMinimum": 0,
138
+ "maximum": 0.3,
139
+ "description": "How far stops shift (fraction of 0..1 range). Default 0.15, max 0.3."
140
+ }
101
141
  },
102
- "description": "Slow oscillation of gradient stop positions — makes the background feel alive."
142
+ "description": "Slow oscillation of gradient stop positions \u2014 makes the background feel alive."
103
143
  }
104
144
  }
105
145
  }
@@ -107,235 +147,627 @@
107
147
  },
108
148
  "layer": {
109
149
  "type": "object",
110
- "required": ["count", "sprite", "motion"],
150
+ "required": [
151
+ "count",
152
+ "sprite",
153
+ "motion"
154
+ ],
111
155
  "additionalProperties": false,
112
156
  "properties": {
113
- "count": { "type": "integer", "minimum": 1, "maximum": 400, "description": "Entities in this layer (max 400 per layer, 800 total)." },
114
- "sprite": { "$ref": "#/definitions/sprite" },
115
- "motion": { "$ref": "#/definitions/motion" },
116
- "size": { "$ref": "#/definitions/positiveRange", "description": "Sprite size range in px (font size for emoji/text; ignored for circle)." },
117
- "wrap": { "type": "boolean", "description": "Wrap to the opposite edge when leaving the viewport. Default true." },
118
- "flip": { "type": "boolean", "description": "Flip the sprite horizontally to face its heading. Default false." },
119
- "alpha": { "$ref": "#/definitions/range01", "description": "Per-entity opacity range. Default [1, 1]." },
120
- "blend": { "enum": ["lighter", "screen", "multiply"], "description": "Compositing: 'lighter' = additive glow, 'screen' = gentle additive for pale backgrounds, 'multiply' = darkening (shadows). Omit for normal source-over." },
157
+ "count": {
158
+ "type": "integer",
159
+ "minimum": 1,
160
+ "maximum": 400,
161
+ "description": "Entities in this layer (max 400 per layer, 800 total)."
162
+ },
163
+ "sprite": {
164
+ "$ref": "#/definitions/sprite"
165
+ },
166
+ "motion": {
167
+ "$ref": "#/definitions/motion"
168
+ },
169
+ "size": {
170
+ "$ref": "#/definitions/positiveRange",
171
+ "description": "Sprite size range in px (font size for emoji/text; ignored for circle)."
172
+ },
173
+ "wrap": {
174
+ "type": "boolean",
175
+ "description": "Wrap to the opposite edge when leaving the viewport. Default true."
176
+ },
177
+ "flip": {
178
+ "type": "boolean",
179
+ "description": "Flip the sprite horizontally to face its heading. Default false."
180
+ },
181
+ "alpha": {
182
+ "$ref": "#/definitions/range01",
183
+ "description": "Per-entity opacity range. Default [1, 1]."
184
+ },
185
+ "blend": {
186
+ "enum": [
187
+ "lighter",
188
+ "screen",
189
+ "multiply"
190
+ ],
191
+ "description": "Compositing: 'lighter' = additive glow, 'screen' = gentle additive for pale backgrounds, 'multiply' = darkening (shadows). Omit for normal source-over."
192
+ },
121
193
  "region": {
122
194
  "type": "object",
123
195
  "additionalProperties": false,
124
196
  "properties": {
125
- "x": { "$ref": "#/definitions/range01" },
126
- "y": { "$ref": "#/definitions/range01" }
197
+ "x": {
198
+ "$ref": "#/definitions/range01"
199
+ },
200
+ "y": {
201
+ "$ref": "#/definitions/range01"
202
+ }
127
203
  },
128
204
  "description": "Fractional spawn window (constrains placement, not travel). Default full viewport."
129
205
  },
130
206
  "pulse": {
131
207
  "type": "object",
132
- "required": ["amp", "period"],
208
+ "required": [
209
+ "amp",
210
+ "period"
211
+ ],
133
212
  "additionalProperties": false,
134
213
  "properties": {
135
- "amp": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.5, "description": "Opacity breathing amplitude, capped at 0.5." },
136
- "period": { "type": "number", "minimum": 500, "description": "Breathing period in ms; floor of 500 (max 2 Hz — flash-safety)." },
214
+ "amp": {
215
+ "type": "number",
216
+ "exclusiveMinimum": 0,
217
+ "maximum": 0.5,
218
+ "description": "Opacity breathing amplitude, capped at 0.5."
219
+ },
220
+ "period": {
221
+ "type": "number",
222
+ "minimum": 500,
223
+ "description": "Breathing period in ms; floor of 500 (max 2 Hz \u2014 flash-safety)."
224
+ },
137
225
  "wave": {
138
226
  "type": "object",
139
- "required": ["wavelength"],
227
+ "required": [
228
+ "wavelength"
229
+ ],
140
230
  "additionalProperties": false,
141
231
  "properties": {
142
- "wavelength": { "type": "number", "exclusiveMinimum": 0, "description": "Spatial wavelength (px or viewport-units)." },
143
- "angle": { "type": "number", "description": "Propagation direction in degrees (0 = rightward). Default 0." }
232
+ "wavelength": {
233
+ "type": "number",
234
+ "exclusiveMinimum": 0,
235
+ "description": "Spatial wavelength (px or viewport-units)."
236
+ },
237
+ "angle": {
238
+ "type": "number",
239
+ "description": "Propagation direction in degrees (0 = rightward). Default 0."
240
+ }
144
241
  },
145
- "description": "Derive each entity's phase from its position: breathing becomes a traveling wave across the field (ripples, wind through grass). Neighbors are out of phase by construction — still flash-safe."
242
+ "description": "Derive each entity's phase from its position: breathing becomes a traveling wave across the field (ripples, wind through grass). Neighbors are out of phase by construction \u2014 still flash-safe."
146
243
  }
147
244
  },
148
- "description": "Sinusoidal opacity breathing. Per-entity seeded phase — a layer can never strobe in unison."
245
+ "description": "Sinusoidal opacity breathing. Per-entity seeded phase \u2014 a layer can never strobe in unison."
149
246
  },
150
247
  "spin": {
151
248
  "description": "Per-entity rotation speed in degrees/sec (positive = clockwise). A scalar applies one speed to all entities; a [min,max] range gives each entity a seeded speed. Each entity gets a seeded starting angle.",
152
249
  "oneOf": [
153
- { "type": "number", "minimum": -360, "maximum": 360 },
154
- { "type": "array", "items": { "type": "number", "minimum": -360, "maximum": 360 }, "minItems": 2, "maxItems": 2 }
250
+ {
251
+ "type": "number",
252
+ "minimum": -360,
253
+ "maximum": 360
254
+ },
255
+ {
256
+ "type": "array",
257
+ "items": {
258
+ "type": "number",
259
+ "minimum": -360,
260
+ "maximum": 360
261
+ },
262
+ "minItems": 2,
263
+ "maxItems": 2
264
+ }
155
265
  ]
156
266
  },
157
267
  "grow": {
158
268
  "type": "object",
159
- "required": ["amp", "period"],
269
+ "required": [
270
+ "amp",
271
+ "period"
272
+ ],
160
273
  "additionalProperties": false,
161
274
  "properties": {
162
- "amp": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.8, "description": "Size breathing amplitude as fraction of base size (0.3 = ±30%)." },
163
- "period": { "type": "number", "minimum": 500, "description": "Breathing period in ms; same flash-safety floor as pulse." }
275
+ "amp": {
276
+ "type": "number",
277
+ "exclusiveMinimum": 0,
278
+ "maximum": 0.8,
279
+ "description": "Size breathing amplitude as fraction of base size (0.3 = \u00b130%)."
280
+ },
281
+ "period": {
282
+ "type": "number",
283
+ "minimum": 500,
284
+ "description": "Breathing period in ms; same flash-safety floor as pulse."
285
+ }
164
286
  },
165
287
  "description": "Sinusoidal size breathing. Per-entity seeded phase. Composes with any sprite kind."
166
288
  },
167
- "key": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Addressable name so setParam can use key.field paths." },
289
+ "key": {
290
+ "type": "string",
291
+ "minLength": 1,
292
+ "pattern": "\\S",
293
+ "description": "Addressable name so setParam can use key.field paths."
294
+ },
168
295
  "position": {
169
296
  "type": "object",
170
- "required": ["x", "y"],
297
+ "required": [
298
+ "x",
299
+ "y"
300
+ ],
171
301
  "additionalProperties": false,
172
302
  "properties": {
173
- "x": { "type": "number", "minimum": 0, "maximum": 1 },
174
- "y": { "type": "number", "minimum": 0, "maximum": 1 }
303
+ "x": {
304
+ "type": "number",
305
+ "minimum": 0,
306
+ "maximum": 1
307
+ },
308
+ "y": {
309
+ "type": "number",
310
+ "minimum": 0,
311
+ "maximum": 1
312
+ }
175
313
  },
176
314
  "description": "Exact fractional position for a single entity. Requires count: 1. Overrides region scatter."
177
315
  },
178
316
  "trail": {
179
317
  "type": "object",
180
- "required": ["length"],
318
+ "required": [
319
+ "length"
320
+ ],
181
321
  "additionalProperties": false,
182
322
  "properties": {
183
- "length": { "type": "number", "exclusiveMinimum": 0, "maximum": 5000, "description": "Trail duration in ms. How far back the afterglow extends." },
184
- "fade": { "type": "number", "minimum": 0, "maximum": 1, "description": "Opacity fade factor along the trail. 1 = full fade to 0 (default), 0 = uniform opacity." }
323
+ "length": {
324
+ "type": "number",
325
+ "exclusiveMinimum": 0,
326
+ "maximum": 5000,
327
+ "description": "Trail duration in ms. How far back the afterglow extends."
328
+ },
329
+ "fade": {
330
+ "type": "number",
331
+ "minimum": 0,
332
+ "maximum": 1,
333
+ "description": "Opacity fade factor along the trail. 1 = full fade to 0 (default), 0 = uniform opacity."
334
+ }
185
335
  },
186
- "description": "Afterglow trail behind moving entities. Sampled analytically — no state, fully deterministic."
336
+ "description": "Afterglow trail behind moving entities. Sampled analytically \u2014 no state, fully deterministic."
187
337
  },
188
338
  "links": {
189
339
  "type": "object",
190
- "required": ["k", "maxDist"],
340
+ "required": [
341
+ "k",
342
+ "maxDist"
343
+ ],
191
344
  "additionalProperties": false,
192
345
  "properties": {
193
- "k": { "type": "integer", "minimum": 1, "maximum": 8, "description": "Connect to the k nearest neighbors." },
194
- "maxDist": { "type": "number", "exclusiveMinimum": 0, "description": "Maximum link distance (px or viewport-units)." },
195
- "color": { "$ref": "#/definitions/hexColor", "description": "Link color. Defaults to per-entity color." },
196
- "alpha": { "type": "number", "minimum": 0, "maximum": 1, "description": "Link opacity. Defaults to entity alpha." },
197
- "width": { "type": "number", "exclusiveMinimum": 0, "description": "Stroke width (px or viewport-units). Default 1." },
198
- "mode": { "enum": ["nearest", "chain", "random"], "description": "'nearest' (default) k-nearest within maxDist; 'chain' connects entities in order (Mystify polygons — ignores k/maxDist); 'random' a fixed deterministic wiring filtered by maxDist." },
199
- "falloff": { "type": "boolean", "description": "Fade link alpha with distance/maxDist — removes pop-in at the cutoff." },
200
- "closed": { "type": "boolean", "description": "Chain mode: join the last entity back to the first (closed polygon)." }
346
+ "k": {
347
+ "type": "integer",
348
+ "minimum": 1,
349
+ "maximum": 8,
350
+ "description": "Connect to the k nearest neighbors."
351
+ },
352
+ "maxDist": {
353
+ "type": "number",
354
+ "exclusiveMinimum": 0,
355
+ "description": "Maximum link distance (px or viewport-units)."
356
+ },
357
+ "color": {
358
+ "$ref": "#/definitions/hexColor",
359
+ "description": "Link color. Defaults to per-entity color."
360
+ },
361
+ "alpha": {
362
+ "type": "number",
363
+ "minimum": 0,
364
+ "maximum": 1,
365
+ "description": "Link opacity. Defaults to entity alpha."
366
+ },
367
+ "width": {
368
+ "type": "number",
369
+ "exclusiveMinimum": 0,
370
+ "description": "Stroke width (px or viewport-units). Default 1."
371
+ },
372
+ "mode": {
373
+ "enum": [
374
+ "nearest",
375
+ "chain",
376
+ "random"
377
+ ],
378
+ "description": "'nearest' (default) k-nearest within maxDist; 'chain' connects entities in order (Mystify polygons \u2014 ignores k/maxDist); 'random' a fixed deterministic wiring filtered by maxDist."
379
+ },
380
+ "falloff": {
381
+ "type": "boolean",
382
+ "description": "Fade link alpha with distance/maxDist \u2014 removes pop-in at the cutoff."
383
+ },
384
+ "closed": {
385
+ "type": "boolean",
386
+ "description": "Chain mode: join the last entity back to the first (closed polygon)."
387
+ }
201
388
  },
202
389
  "description": "Draw lines between entities. The layer's entity count (count field) must be <= 200 for performance."
203
390
  },
204
391
  "layout": {
205
392
  "type": "object",
206
- "required": ["type"],
393
+ "required": [
394
+ "type"
395
+ ],
207
396
  "additionalProperties": false,
208
397
  "properties": {
209
- "type": { "const": "grid" },
210
- "columns": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Grid columns. Default: aspect-fit square-ish grid." },
398
+ "type": {
399
+ "const": "grid"
400
+ },
401
+ "columns": {
402
+ "type": "integer",
403
+ "minimum": 1,
404
+ "maximum": 100,
405
+ "description": "Grid columns. Default: aspect-fit square-ish grid."
406
+ },
211
407
  "jitter": {
212
408
  "oneOf": [
213
- { "type": "number", "minimum": 0, "maximum": 1 },
409
+ {
410
+ "type": "number",
411
+ "minimum": 0,
412
+ "maximum": 1
413
+ },
214
414
  {
215
415
  "type": "object",
216
416
  "additionalProperties": false,
217
417
  "properties": {
218
- "x": { "type": "number", "minimum": 0, "maximum": 1 },
219
- "y": { "type": "number", "minimum": 0, "maximum": 1 }
418
+ "x": {
419
+ "type": "number",
420
+ "minimum": 0,
421
+ "maximum": 1
422
+ },
423
+ "y": {
424
+ "type": "number",
425
+ "minimum": 0,
426
+ "maximum": 1
427
+ }
220
428
  }
221
429
  }
222
430
  ],
223
431
  "description": "Seeded per-entity offset as a fraction of cell size (scalar or per-axis). { y: 1 } keeps columns crisp while scattering vertically (Matrix rain). Default 0."
224
432
  }
225
433
  },
226
- "description": "Place entities on a grid (row-major, within region) instead of random scatter — Matrix columns, LED walls, uniform dot fields."
434
+ "description": "Place entities on a grid (row-major, within region) instead of random scatter \u2014 Matrix columns, LED walls, uniform dot fields."
227
435
  },
228
436
  "life": {
229
437
  "type": "object",
230
438
  "additionalProperties": false,
231
439
  "properties": {
232
- "enter": { "type": "number", "minimum": 0, "description": "Layer fades in starting at this time (ms)." },
233
- "exit": { "type": "number", "minimum": 0, "description": "Layer fades out starting at this time (ms). Must be > enter." },
234
- "fade": { "type": "number", "minimum": 0, "description": "Fade ramp duration in ms. Default 1000." }
440
+ "enter": {
441
+ "type": "number",
442
+ "minimum": 0,
443
+ "description": "Layer fades in starting at this time (ms)."
444
+ },
445
+ "exit": {
446
+ "type": "number",
447
+ "minimum": 0,
448
+ "description": "Layer fades out starting at this time (ms). Must be > enter."
449
+ },
450
+ "fade": {
451
+ "type": "number",
452
+ "minimum": 0,
453
+ "description": "Fade ramp duration in ms. Default 1000."
454
+ }
235
455
  },
236
- "description": "Layer lifecycle for act structure (stars first, dancers at t=3000). Pure function of t — deterministic and seekable."
456
+ "description": "Layer lifecycle for act structure (stars first, dancers at t=3000). Pure function of t \u2014 deterministic and seekable."
237
457
  }
238
458
  },
239
- "if": { "required": ["position"] },
240
- "then": { "properties": { "count": { "const": 1 } } }
459
+ "if": {
460
+ "required": [
461
+ "position"
462
+ ]
463
+ },
464
+ "then": {
465
+ "properties": {
466
+ "count": {
467
+ "const": 1
468
+ }
469
+ }
470
+ }
241
471
  },
242
472
  "sprite": {
243
473
  "oneOf": [
244
474
  {
245
475
  "type": "object",
246
- "required": ["kind", "glyphs"],
476
+ "required": [
477
+ "kind",
478
+ "glyphs"
479
+ ],
247
480
  "additionalProperties": false,
248
481
  "properties": {
249
- "kind": { "const": "emoji" },
250
- "glyphs": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Emoji picked per entity (seeded)." },
251
- "cycle": { "$ref": "#/definitions/cycle" }
482
+ "kind": {
483
+ "const": "emoji"
484
+ },
485
+ "glyphs": {
486
+ "type": "array",
487
+ "minItems": 1,
488
+ "items": {
489
+ "type": "string"
490
+ },
491
+ "description": "Emoji picked per entity (seeded)."
492
+ },
493
+ "cycle": {
494
+ "$ref": "#/definitions/cycle"
495
+ }
252
496
  }
253
497
  },
254
498
  {
255
499
  "type": "object",
256
- "required": ["kind", "strings"],
500
+ "required": [
501
+ "kind",
502
+ "strings"
503
+ ],
257
504
  "additionalProperties": false,
258
505
  "properties": {
259
- "kind": { "const": "text" },
260
- "strings": { "type": "array", "minItems": 1, "items": { "type": "string" } },
261
- "color": { "$ref": "#/definitions/hexColor" },
262
- "font": { "type": "string", "description": "CSS font shorthand, e.g. 'bold 24px monospace'." },
263
- "align": { "enum": ["left", "center", "right"] },
264
- "baseline": { "enum": ["top", "middle", "bottom"] },
265
- "maxWidth": { "type": "number", "exclusiveMinimum": 0 },
266
- "cycle": { "$ref": "#/definitions/cycle" }
506
+ "kind": {
507
+ "const": "text"
508
+ },
509
+ "strings": {
510
+ "type": "array",
511
+ "minItems": 1,
512
+ "items": {
513
+ "type": "string"
514
+ }
515
+ },
516
+ "color": {
517
+ "$ref": "#/definitions/hexColor"
518
+ },
519
+ "font": {
520
+ "type": "string",
521
+ "description": "CSS font shorthand, e.g. 'bold 24px monospace'."
522
+ },
523
+ "align": {
524
+ "enum": [
525
+ "left",
526
+ "center",
527
+ "right"
528
+ ]
529
+ },
530
+ "baseline": {
531
+ "enum": [
532
+ "top",
533
+ "middle",
534
+ "bottom"
535
+ ]
536
+ },
537
+ "maxWidth": {
538
+ "type": "number",
539
+ "exclusiveMinimum": 0
540
+ },
541
+ "cycle": {
542
+ "$ref": "#/definitions/cycle"
543
+ }
267
544
  }
268
545
  },
269
546
  {
270
547
  "type": "object",
271
- "required": ["kind", "radius", "color"],
548
+ "required": [
549
+ "kind",
550
+ "radius",
551
+ "color"
552
+ ],
272
553
  "additionalProperties": false,
273
554
  "properties": {
274
- "kind": { "const": "circle" },
275
- "radius": { "$ref": "#/definitions/positiveRange" },
276
- "color": { "$ref": "#/definitions/hexColor" },
277
- "soft": { "type": "boolean", "description": "Radial falloff (glow orb) instead of a hard disc." },
278
- "colors": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/hexColor" }, "description": "Per-entity color palette (seeded pick). Falls back to color." },
279
- "colorWeights": { "$ref": "#/definitions/colorWeights" }
555
+ "kind": {
556
+ "const": "circle"
557
+ },
558
+ "radius": {
559
+ "$ref": "#/definitions/positiveRange"
560
+ },
561
+ "color": {
562
+ "$ref": "#/definitions/hexColor"
563
+ },
564
+ "soft": {
565
+ "type": "boolean",
566
+ "description": "Radial falloff (glow orb) instead of a hard disc."
567
+ },
568
+ "colors": {
569
+ "type": "array",
570
+ "minItems": 1,
571
+ "items": {
572
+ "$ref": "#/definitions/hexColor"
573
+ },
574
+ "description": "Per-entity color palette (seeded pick). Falls back to color."
575
+ },
576
+ "colorWeights": {
577
+ "$ref": "#/definitions/colorWeights"
578
+ }
280
579
  }
281
580
  },
282
581
  {
283
582
  "type": "object",
284
- "required": ["kind", "radius", "color"],
583
+ "required": [
584
+ "kind",
585
+ "radius",
586
+ "color"
587
+ ],
285
588
  "additionalProperties": false,
286
589
  "properties": {
287
- "kind": { "const": "ring" },
288
- "radius": { "$ref": "#/definitions/positiveRange" },
289
- "color": { "$ref": "#/definitions/hexColor" },
290
- "width": { "type": "number", "exclusiveMinimum": 0, "description": "Stroke width (px or viewport-units). Default ~2px." },
291
- "colors": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/hexColor" }, "description": "Per-entity color palette (seeded pick)." },
292
- "colorWeights": { "$ref": "#/definitions/colorWeights" }
590
+ "kind": {
591
+ "const": "ring"
592
+ },
593
+ "radius": {
594
+ "$ref": "#/definitions/positiveRange"
595
+ },
596
+ "color": {
597
+ "$ref": "#/definitions/hexColor"
598
+ },
599
+ "width": {
600
+ "type": "number",
601
+ "exclusiveMinimum": 0,
602
+ "description": "Stroke width (px or viewport-units). Default ~2px."
603
+ },
604
+ "colors": {
605
+ "type": "array",
606
+ "minItems": 1,
607
+ "items": {
608
+ "$ref": "#/definitions/hexColor"
609
+ },
610
+ "description": "Per-entity color palette (seeded pick)."
611
+ },
612
+ "colorWeights": {
613
+ "$ref": "#/definitions/colorWeights"
614
+ }
293
615
  },
294
- "description": "Unfilled circle — bubbles, portals, sonar pings."
616
+ "description": "Unfilled circle \u2014 bubbles, portals, sonar pings."
295
617
  },
296
618
  {
297
619
  "type": "object",
298
- "required": ["kind", "length", "color"],
620
+ "required": [
621
+ "kind",
622
+ "length",
623
+ "color"
624
+ ],
299
625
  "additionalProperties": false,
300
626
  "properties": {
301
- "kind": { "const": "streak" },
302
- "length": { "$ref": "#/definitions/positiveRange", "description": "Segment length range (px or viewport-units)." },
303
- "color": { "$ref": "#/definitions/hexColor" },
304
- "width": { "type": "number", "exclusiveMinimum": 0, "description": "Stroke width (px or viewport-units). Default ~2px." },
305
- "colors": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/hexColor" }, "description": "Per-entity color palette (seeded pick)." },
306
- "colorWeights": { "$ref": "#/definitions/colorWeights" }
627
+ "kind": {
628
+ "const": "streak"
629
+ },
630
+ "length": {
631
+ "$ref": "#/definitions/positiveRange",
632
+ "description": "Segment length range (px or viewport-units)."
633
+ },
634
+ "color": {
635
+ "$ref": "#/definitions/hexColor"
636
+ },
637
+ "width": {
638
+ "type": "number",
639
+ "exclusiveMinimum": 0,
640
+ "description": "Stroke width (px or viewport-units). Default ~2px."
641
+ },
642
+ "colors": {
643
+ "type": "array",
644
+ "minItems": 1,
645
+ "items": {
646
+ "$ref": "#/definitions/hexColor"
647
+ },
648
+ "description": "Per-entity color palette (seeded pick)."
649
+ },
650
+ "colorWeights": {
651
+ "$ref": "#/definitions/colorWeights"
652
+ }
307
653
  },
308
- "description": "A line oriented along the entity's heading with a faded tail — rain that reads as rain, shooting stars, warp stars."
654
+ "description": "A line oriented along the entity's heading with a faded tail \u2014 rain that reads as rain, shooting stars, warp stars."
309
655
  },
310
656
  {
311
657
  "type": "object",
312
- "required": ["kind", "width", "color"],
658
+ "required": [
659
+ "kind",
660
+ "width",
661
+ "color"
662
+ ],
313
663
  "additionalProperties": false,
314
664
  "properties": {
315
- "kind": { "const": "rect" },
316
- "width": { "$ref": "#/definitions/positiveRange", "description": "Horizontal size range (px or viewport-units)." },
317
- "aspect": { "$ref": "#/definitions/positiveRange", "description": "Height/width ratio range. Default [1,1] = squares." },
318
- "color": { "$ref": "#/definitions/hexColor" },
319
- "colors": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/hexColor" }, "description": "Per-entity color palette (seeded pick)." },
320
- "colorWeights": { "$ref": "#/definitions/colorWeights" }
665
+ "kind": {
666
+ "const": "rect"
667
+ },
668
+ "width": {
669
+ "$ref": "#/definitions/positiveRange",
670
+ "description": "Horizontal size range (px or viewport-units)."
671
+ },
672
+ "aspect": {
673
+ "$ref": "#/definitions/positiveRange",
674
+ "description": "Height/width ratio range. Default [1,1] = squares."
675
+ },
676
+ "color": {
677
+ "$ref": "#/definitions/hexColor"
678
+ },
679
+ "colors": {
680
+ "type": "array",
681
+ "minItems": 1,
682
+ "items": {
683
+ "$ref": "#/definitions/hexColor"
684
+ },
685
+ "description": "Per-entity color palette (seeded pick)."
686
+ },
687
+ "colorWeights": {
688
+ "$ref": "#/definitions/colorWeights"
689
+ }
321
690
  },
322
- "description": "Axis-aligned rectangle (rotates with spin) — Mondrian blocks, confetti, city lights."
691
+ "description": "Axis-aligned rectangle (rotates with spin) \u2014 Mondrian blocks, confetti, city lights."
692
+ },
693
+ {
694
+ "type": "object",
695
+ "required": [
696
+ "kind",
697
+ "text",
698
+ "maxWidth",
699
+ "fontSize"
700
+ ],
701
+ "additionalProperties": false,
702
+ "properties": {
703
+ "kind": {
704
+ "const": "textBlock"
705
+ },
706
+ "text": {
707
+ "type": "string",
708
+ "minLength": 1,
709
+ "maxLength": 2000,
710
+ "description": "The text to render. Explicit \\n forces a line break."
711
+ },
712
+ "maxWidth": {
713
+ "type": "number",
714
+ "exclusiveMinimum": 0,
715
+ "maximum": 1.0,
716
+ "description": "Block width as a viewport fraction of min(w,h)."
717
+ },
718
+ "fontSize": {
719
+ "type": "number",
720
+ "minimum": 0.01,
721
+ "maximum": 0.2,
722
+ "description": "Font size as a viewport fraction of min(w,h)."
723
+ },
724
+ "lineHeight": {
725
+ "type": "number",
726
+ "minimum": 0.5,
727
+ "maximum": 4,
728
+ "description": "Line-height multiplier (like CSS). Default 1.4."
729
+ },
730
+ "align": {
731
+ "enum": [
732
+ "left",
733
+ "center",
734
+ "right"
735
+ ],
736
+ "description": "Text alignment within the block. Default 'left'."
737
+ },
738
+ "color": {
739
+ "$ref": "#/definitions/hexColor"
740
+ }
741
+ },
742
+ "description": "Multi-line text block with deterministic line-breaking. All dimensions are viewport fractions. Use with count: 1, static motion, and position."
323
743
  }
324
744
  ]
325
745
  },
326
746
  "colorWeights": {
327
747
  "type": "array",
328
748
  "minItems": 1,
329
- "items": { "type": "number", "minimum": 0 },
330
- "contains": { "type": "number", "exclusiveMinimum": 0 },
749
+ "items": {
750
+ "type": "number",
751
+ "minimum": 0
752
+ },
753
+ "contains": {
754
+ "type": "number",
755
+ "exclusiveMinimum": 0
756
+ },
331
757
  "description": "Relative weights for the colors[] pick, same length as colors. At least one weight must be > 0. 'Mostly cool tones, occasional ember' without duplicating entries."
332
758
  },
333
759
  "cycle": {
334
760
  "type": "object",
335
- "required": ["period"],
761
+ "required": [
762
+ "period"
763
+ ],
336
764
  "additionalProperties": false,
337
765
  "properties": {
338
- "period": { "type": "number", "minimum": 500, "description": "Cycling period in ms; floor of 500 (flash-safety)." }
766
+ "period": {
767
+ "type": "number",
768
+ "minimum": 500,
769
+ "description": "Cycling period in ms; floor of 500 (flash-safety)."
770
+ }
339
771
  },
340
772
  "description": "Rotate through sprite variants over time. Each entity offsets by its seeded phase."
341
773
  },
@@ -343,74 +775,143 @@
343
775
  "oneOf": [
344
776
  {
345
777
  "type": "object",
346
- "required": ["type", "speed"],
778
+ "required": [
779
+ "type",
780
+ "speed"
781
+ ],
347
782
  "additionalProperties": false,
348
783
  "properties": {
349
- "type": { "const": "drift" },
350
- "speed": { "$ref": "#/definitions/speedRange" },
351
- "angle": { "type": "number", "description": "Heading in degrees: 0 = right, 90 = down. Default 0." },
352
- "bidirectional": { "type": "boolean", "description": "Randomly flip horizontal direction per entity (fish)." },
353
- "bob": { "type": "number", "description": "Vertical wobble amplitude in px." }
784
+ "type": {
785
+ "const": "drift"
786
+ },
787
+ "speed": {
788
+ "$ref": "#/definitions/speedRange"
789
+ },
790
+ "angle": {
791
+ "type": "number",
792
+ "description": "Heading in degrees: 0 = right, 90 = down. Default 0."
793
+ },
794
+ "bidirectional": {
795
+ "type": "boolean",
796
+ "description": "Randomly flip horizontal direction per entity (fish)."
797
+ },
798
+ "bob": {
799
+ "type": "number",
800
+ "description": "Vertical wobble amplitude in px."
801
+ }
354
802
  }
355
803
  },
356
804
  {
357
805
  "type": "object",
358
- "required": ["type", "speed"],
806
+ "required": [
807
+ "type",
808
+ "speed"
809
+ ],
359
810
  "additionalProperties": false,
360
811
  "properties": {
361
- "type": { "const": "rise" },
362
- "speed": { "$ref": "#/definitions/speedRange" },
363
- "sway": { "type": "number", "description": "Horizontal sway amplitude in px (bubbles)." }
812
+ "type": {
813
+ "const": "rise"
814
+ },
815
+ "speed": {
816
+ "$ref": "#/definitions/speedRange"
817
+ },
818
+ "sway": {
819
+ "type": "number",
820
+ "description": "Horizontal sway amplitude in px (bubbles)."
821
+ }
364
822
  }
365
823
  },
366
824
  {
367
825
  "type": "object",
368
- "required": ["type", "speed"],
826
+ "required": [
827
+ "type",
828
+ "speed"
829
+ ],
369
830
  "additionalProperties": false,
370
831
  "properties": {
371
- "type": { "const": "bounce" },
372
- "speed": { "$ref": "#/definitions/speedRange" }
832
+ "type": {
833
+ "const": "bounce"
834
+ },
835
+ "speed": {
836
+ "$ref": "#/definitions/speedRange"
837
+ }
373
838
  }
374
839
  },
375
840
  {
376
841
  "type": "object",
377
- "required": ["type"],
842
+ "required": [
843
+ "type"
844
+ ],
378
845
  "additionalProperties": false,
379
846
  "properties": {
380
- "type": { "const": "static", "description": "Entity stays exactly where placed. Use with position for pinned elements." }
847
+ "type": {
848
+ "const": "static",
849
+ "description": "Entity stays exactly where placed. Use with position for pinned elements."
850
+ }
381
851
  }
382
852
  },
383
853
  {
384
854
  "type": "object",
385
- "required": ["type", "speed", "radius"],
855
+ "required": [
856
+ "type",
857
+ "speed",
858
+ "radius"
859
+ ],
386
860
  "additionalProperties": false,
387
861
  "properties": {
388
- "type": { "const": "orbit" },
862
+ "type": {
863
+ "const": "orbit"
864
+ },
389
865
  "speed": {
390
866
  "type": "array",
391
- "items": { "type": "number", "minimum": 0, "maximum": 180 },
867
+ "items": {
868
+ "type": "number",
869
+ "minimum": 0,
870
+ "maximum": 180
871
+ },
392
872
  "minItems": 2,
393
873
  "maxItems": 2,
394
874
  "description": "[min, max] angular velocity in degrees/sec, capped at 180."
395
875
  },
396
- "radius": { "$ref": "#/definitions/positiveRange", "description": "[min, max] orbit radius in px." },
876
+ "radius": {
877
+ "$ref": "#/definitions/positiveRange",
878
+ "description": "[min, max] orbit radius in px."
879
+ },
397
880
  "center": {
398
881
  "oneOf": [
399
882
  {
400
883
  "type": "object",
401
- "required": ["x", "y"],
884
+ "required": [
885
+ "x",
886
+ "y"
887
+ ],
402
888
  "additionalProperties": false,
403
889
  "properties": {
404
- "x": { "type": "number", "minimum": 0, "maximum": 1 },
405
- "y": { "type": "number", "minimum": 0, "maximum": 1 }
890
+ "x": {
891
+ "type": "number",
892
+ "minimum": 0,
893
+ "maximum": 1
894
+ },
895
+ "y": {
896
+ "type": "number",
897
+ "minimum": 0,
898
+ "maximum": 1
899
+ }
406
900
  }
407
901
  },
408
902
  {
409
903
  "type": "object",
410
- "required": ["layer"],
904
+ "required": [
905
+ "layer"
906
+ ],
411
907
  "additionalProperties": false,
412
908
  "properties": {
413
- "layer": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Key of a count-1 parent layer to orbit (moons around a wandering planet). One level deep only — the parent may not itself orbit a layer." }
909
+ "layer": {
910
+ "type": "string",
911
+ "minLength": 1,
912
+ "pattern": "\\S",
913
+ "description": "Key of a count-1 parent layer to orbit (moons around a wandering planet). One level deep only \u2014 the parent may not itself orbit a layer."
914
+ }
414
915
  }
415
916
  }
416
917
  ],
@@ -420,37 +921,76 @@
420
921
  },
421
922
  {
422
923
  "type": "object",
423
- "required": ["type", "speed"],
924
+ "required": [
925
+ "type",
926
+ "speed"
927
+ ],
424
928
  "additionalProperties": false,
425
929
  "properties": {
426
- "type": { "const": "wander" },
427
- "speed": { "$ref": "#/definitions/speedRange" },
428
- "angle": { "type": "number", "description": "Base heading in degrees. Omit for a seeded heading per entity." },
429
- "meander": { "type": "number", "exclusiveMinimum": 0, "description": "Harmonic amplitude (px or viewport-units). Default ~0.05 viewport-units. How far paths curve away from the base line." },
430
- "coherence": { "type": "number", "minimum": 0, "maximum": 1, "description": "0 = each entity wanders independently; 1 = the whole layer undulates in unison (fake flocking). Default 0." }
930
+ "type": {
931
+ "const": "wander"
932
+ },
933
+ "speed": {
934
+ "$ref": "#/definitions/speedRange"
935
+ },
936
+ "angle": {
937
+ "type": "number",
938
+ "description": "Base heading in degrees. Omit for a seeded heading per entity."
939
+ },
940
+ "meander": {
941
+ "type": "number",
942
+ "exclusiveMinimum": 0,
943
+ "description": "Harmonic amplitude (px or viewport-units). Default ~0.05 viewport-units. How far paths curve away from the base line."
944
+ },
945
+ "coherence": {
946
+ "type": "number",
947
+ "minimum": 0,
948
+ "maximum": 1,
949
+ "description": "0 = each entity wanders independently; 1 = the whole layer undulates in unison (fake flocking). Default 0."
950
+ }
431
951
  },
432
- "description": "Organic harmonic drift — base velocity plus 3 seeded sine octaves per axis. Flowing curved paths (Flurry streams, aurora, jellyfish) with zero simulation state."
952
+ "description": "Organic harmonic drift \u2014 base velocity plus 3 seeded sine octaves per axis. Flowing curved paths (Flurry streams, aurora, jellyfish) with zero simulation state."
433
953
  },
434
954
  {
435
955
  "type": "object",
436
- "required": ["type", "speed"],
956
+ "required": [
957
+ "type",
958
+ "speed"
959
+ ],
437
960
  "additionalProperties": false,
438
961
  "properties": {
439
- "type": { "const": "warp" },
962
+ "type": {
963
+ "const": "warp"
964
+ },
440
965
  "speed": {
441
966
  "type": "array",
442
- "items": { "type": "number", "minimum": 0, "maximum": 1.5 },
967
+ "items": {
968
+ "type": "number",
969
+ "minimum": 0,
970
+ "maximum": 1.5
971
+ },
443
972
  "minItems": 2,
444
973
  "maxItems": 2,
445
974
  "description": "[min, max] depth-units/sec (1 = full near-to-far span per second), capped at 1.5."
446
975
  },
447
976
  "center": {
448
977
  "type": "object",
449
- "required": ["x", "y"],
978
+ "required": [
979
+ "x",
980
+ "y"
981
+ ],
450
982
  "additionalProperties": false,
451
983
  "properties": {
452
- "x": { "type": "number", "minimum": 0, "maximum": 1 },
453
- "y": { "type": "number", "minimum": 0, "maximum": 1 }
984
+ "x": {
985
+ "type": "number",
986
+ "minimum": 0,
987
+ "maximum": 1
988
+ },
989
+ "y": {
990
+ "type": "number",
991
+ "minimum": 0,
992
+ "maximum": 1
993
+ }
454
994
  },
455
995
  "description": "Vanishing point as fractional viewport position. Default {0.5, 0.5}."
456
996
  }
@@ -459,33 +999,251 @@
459
999
  },
460
1000
  {
461
1001
  "type": "object",
462
- "required": ["type", "points", "duration"],
1002
+ "required": [
1003
+ "type",
1004
+ "points",
1005
+ "duration"
1006
+ ],
463
1007
  "additionalProperties": false,
464
1008
  "properties": {
465
- "type": { "const": "path" },
1009
+ "type": {
1010
+ "const": "path"
1011
+ },
466
1012
  "points": {
467
1013
  "type": "array",
468
1014
  "minItems": 2,
469
1015
  "maxItems": 24,
470
1016
  "items": {
471
1017
  "type": "object",
472
- "required": ["x", "y"],
1018
+ "required": [
1019
+ "x",
1020
+ "y"
1021
+ ],
473
1022
  "additionalProperties": false,
474
1023
  "properties": {
475
- "x": { "type": "number", "minimum": 0, "maximum": 1 },
476
- "y": { "type": "number", "minimum": 0, "maximum": 1 }
1024
+ "x": {
1025
+ "type": "number",
1026
+ "minimum": 0,
1027
+ "maximum": 1
1028
+ },
1029
+ "y": {
1030
+ "type": "number",
1031
+ "minimum": 0,
1032
+ "maximum": 1
1033
+ }
477
1034
  }
478
1035
  },
479
1036
  "description": "Waypoints as fractional viewport positions."
480
1037
  },
481
- "duration": { "type": "number", "minimum": 2000, "description": "Time for one full lap in ms (min 2000)." },
482
- "curve": { "enum": ["linear", "smooth"], "description": "'smooth' (default) = Catmull-Rom spline through the points; 'linear' = straight segments." },
483
- "closed": { "type": "boolean", "description": "Loop the path (default true). Open paths ping-pong." },
484
- "scatter": { "type": "number", "exclusiveMinimum": 0, "description": "Seeded per-entity offset (px or viewport-units) so entities sharing a path don't stack." }
1038
+ "duration": {
1039
+ "type": "number",
1040
+ "minimum": 2000,
1041
+ "description": "Time for one full lap in ms (min 2000)."
1042
+ },
1043
+ "curve": {
1044
+ "enum": [
1045
+ "linear",
1046
+ "smooth"
1047
+ ],
1048
+ "description": "'smooth' (default) = Catmull-Rom spline through the points; 'linear' = straight segments."
1049
+ },
1050
+ "closed": {
1051
+ "type": "boolean",
1052
+ "description": "Loop the path (default true). Open paths ping-pong."
1053
+ },
1054
+ "scatter": {
1055
+ "type": "number",
1056
+ "exclusiveMinimum": 0,
1057
+ "description": "Seeded per-entity offset (px or viewport-units) so entities sharing a path don't stack."
1058
+ }
485
1059
  },
486
- "description": "Choreographed spline motion — figure-eights, sweeping arcs, patrol routes. Each entity gets a seeded phase offset along the path."
1060
+ "description": "Choreographed spline motion \u2014 figure-eights, sweeping arcs, patrol routes. Each entity gets a seeded phase offset along the path."
487
1061
  }
488
1062
  ]
1063
+ },
1064
+ "saverSpec": {
1065
+ "type": "object",
1066
+ "required": [
1067
+ "schemaVersion",
1068
+ "id",
1069
+ "label",
1070
+ "layers"
1071
+ ],
1072
+ "additionalProperties": false,
1073
+ "properties": {
1074
+ "schemaVersion": {
1075
+ "const": 1,
1076
+ "description": "Format version. Independent of the npm package version."
1077
+ },
1078
+ "id": {
1079
+ "type": "string",
1080
+ "minLength": 1,
1081
+ "pattern": "\\S",
1082
+ "description": "Unique kebab-case saver id."
1083
+ },
1084
+ "label": {
1085
+ "type": "string",
1086
+ "minLength": 1,
1087
+ "pattern": "\\S",
1088
+ "description": "Human-readable name."
1089
+ },
1090
+ "seed": {
1091
+ "type": "number",
1092
+ "description": "Deterministic RNG seed. Same spec + same seed = identical frames. Falls back to the host's seed."
1093
+ },
1094
+ "motionIntensity": {
1095
+ "enum": [
1096
+ "calm",
1097
+ "moderate",
1098
+ "energetic"
1099
+ ],
1100
+ "description": "Advisory intensity for capability/reduced-motion gating."
1101
+ },
1102
+ "units": {
1103
+ "enum": [
1104
+ "px",
1105
+ "viewport"
1106
+ ],
1107
+ "description": "Dimensional unit system. 'viewport' = all sizes/speeds/distances are fractions of min(w,h)."
1108
+ },
1109
+ "referenceViewport": {
1110
+ "type": "number",
1111
+ "minimum": 100,
1112
+ "maximum": 8640,
1113
+ "description": "Resolution the spec was designed at (default 1080). Entity counts density-scale above it."
1114
+ },
1115
+ "ghosting": {
1116
+ "type": "number",
1117
+ "minimum": 0,
1118
+ "maximum": 0.95,
1119
+ "description": "Frame persistence 0..0.95: each frame composites over a faded copy of the last, so moving entities smear (Mystify trails, long-exposure light). Flash-safe \u2014 only smooths luminance. Seeks stay deterministic via a fixed-step warm-up replay."
1120
+ },
1121
+ "background": {
1122
+ "$ref": "#/definitions/background"
1123
+ },
1124
+ "layers": {
1125
+ "type": "array",
1126
+ "minItems": 1,
1127
+ "maxItems": 36,
1128
+ "items": {
1129
+ "$ref": "#/definitions/layer"
1130
+ },
1131
+ "description": "Rendered back-to-front. At most 36 layers; at most 800 entities total."
1132
+ }
1133
+ },
1134
+ "description": "A single declarative screensaver scene (layers of sprites over a background)."
1135
+ },
1136
+ "idleSequence": {
1137
+ "type": "object",
1138
+ "required": [
1139
+ "format",
1140
+ "schemaVersion",
1141
+ "id",
1142
+ "label",
1143
+ "loop",
1144
+ "segments"
1145
+ ],
1146
+ "additionalProperties": false,
1147
+ "description": "Multi-segment timeline over unmodified SaverSpecs. Discriminated by format: 'idle-sequence'. Cross-field rules (durationless only on final segment, loop incompatible with durationless tail, unique keys) are enforced by validateSequence(), not this schema.",
1148
+ "properties": {
1149
+ "format": {
1150
+ "const": "idle-sequence",
1151
+ "description": "Discriminator \u2014 distinguishes this envelope from a SaverSpec."
1152
+ },
1153
+ "schemaVersion": {
1154
+ "const": 1,
1155
+ "description": "Format version. Independent of the npm package version."
1156
+ },
1157
+ "id": {
1158
+ "type": "string",
1159
+ "minLength": 1,
1160
+ "pattern": "\\S",
1161
+ "description": "Unique kebab-case sequence id."
1162
+ },
1163
+ "label": {
1164
+ "type": "string",
1165
+ "minLength": 1,
1166
+ "pattern": "\\S",
1167
+ "description": "Human-readable name."
1168
+ },
1169
+ "seed": {
1170
+ "type": "number",
1171
+ "description": "Optional seed forwarded to children that lack their own (offset by segment index)."
1172
+ },
1173
+ "loop": {
1174
+ "type": "boolean",
1175
+ "description": "When true, global time wraps over the sum of segment durations. Requires every segment to have a duration."
1176
+ },
1177
+ "segments": {
1178
+ "type": "array",
1179
+ "minItems": 1,
1180
+ "maxItems": 24,
1181
+ "description": "1\u201324 segments. Only the final segment may omit duration (holds indefinitely).",
1182
+ "items": {
1183
+ "$ref": "#/definitions/sequenceSegment"
1184
+ }
1185
+ }
1186
+ }
1187
+ },
1188
+ "sequenceSegment": {
1189
+ "type": "object",
1190
+ "required": [
1191
+ "key",
1192
+ "scene"
1193
+ ],
1194
+ "additionalProperties": false,
1195
+ "properties": {
1196
+ "key": {
1197
+ "type": "string",
1198
+ "minLength": 1,
1199
+ "pattern": "\\S",
1200
+ "description": "Unique segment key within the sequence."
1201
+ },
1202
+ "scene": {
1203
+ "$ref": "#/definitions/saverSpec",
1204
+ "description": "Unmodified SaverSpec for this segment."
1205
+ },
1206
+ "duration": {
1207
+ "type": "number",
1208
+ "minimum": 1000,
1209
+ "description": "Segment duration in ms (min 1000 for flash safety). Omit only on the final segment."
1210
+ },
1211
+ "advance": {
1212
+ "enum": [
1213
+ "auto",
1214
+ "input",
1215
+ "either"
1216
+ ],
1217
+ "description": "How the segment advances. Validated; runtime drivers planned for a follow-up."
1218
+ },
1219
+ "transition": {
1220
+ "type": "object",
1221
+ "required": [
1222
+ "type"
1223
+ ],
1224
+ "oneOf": [
1225
+ {
1226
+ "properties": {
1227
+ "type": { "const": "cut" }
1228
+ },
1229
+ "additionalProperties": false
1230
+ },
1231
+ {
1232
+ "properties": {
1233
+ "type": { "const": "morph" },
1234
+ "dur": {
1235
+ "type": "number",
1236
+ "minimum": 200,
1237
+ "maximum": 5000,
1238
+ "description": "Morph glide duration in ms. Requires structurally identical adjacent segments."
1239
+ }
1240
+ },
1241
+ "required": ["type", "dur"],
1242
+ "additionalProperties": false
1243
+ }
1244
+ ]
1245
+ }
1246
+ }
489
1247
  }
490
1248
  }
491
1249
  }