@idle-screens/schema 2.1.0 → 2.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.
package/dist/index.d.ts CHANGED
@@ -22,8 +22,22 @@ interface SaverSpec {
22
22
  background?: BackgroundSpec;
23
23
  layers: LayerSpec[];
24
24
  motionIntensity?: 'calm' | 'moderate' | 'energetic';
25
- /** Dimensional unit system. 'viewport' = all sizes/speeds/distances are fractions of min(w,h). */
26
- units?: 'px' | 'viewport';
25
+ /**
26
+ * Frame persistence (0..1, capped at LIMITS.maxGhosting). Instead of fully clearing,
27
+ * each frame the background is painted at reduced alpha so moving entities leave
28
+ * decaying after-images (Mystify smears, long-exposure light). Inherently
29
+ * flash-safe: ghosting can only smooth luminance changes, never sharpen them.
30
+ * `renderFrame(t)` stays deterministic for seeks via a fixed-step warm-up replay.
31
+ */
32
+ ghosting?: number;
33
+ /** Dimensional unit system. 'viewport' (default) = all sizes/speeds/distances are fractions of min(w,h). */
34
+ units?: 'viewport' | 'px';
35
+ /**
36
+ * Resolution at which this spec was designed. Density scaling kicks in above this
37
+ * threshold: entity counts scale by min(w,h) / referenceViewport. Default: 1080.
38
+ * Set to your design resolution so a spec authored at 4K doesn't over-densify.
39
+ */
40
+ referenceViewport?: number;
27
41
  }
28
42
  type BackgroundSpec = {
29
43
  type: 'solid';
@@ -32,6 +46,7 @@ type BackgroundSpec = {
32
46
  type: 'gradient';
33
47
  stops: GradientStop[];
34
48
  band?: BandSpec;
49
+ drift?: BackgroundDrift;
35
50
  };
36
51
  /** A vertical gradient stop (`at` 0 = top, 1 = bottom). */
37
52
  interface GradientStop {
@@ -41,9 +56,16 @@ interface GradientStop {
41
56
  /** An optional solid band at the bottom (e.g. an aquarium seafloor). */
42
57
  interface BandSpec {
43
58
  color: string;
44
- /** Band height in px. */
59
+ /** Band height — px or a viewport fraction, per the spec's `units`. */
45
60
  height: number;
46
61
  }
62
+ /** Slow oscillation of gradient stop positions — makes the background feel alive. */
63
+ interface BackgroundDrift {
64
+ /** Full cycle period in ms. Floor: LIMITS.minDriftPeriod (10 s). */
65
+ period: number;
66
+ /** How far stops shift (fraction of the 0..1 range). Default 0.15, max 0.3. */
67
+ amount?: number;
68
+ }
47
69
  interface LayerSpec {
48
70
  count: number;
49
71
  sprite: SpriteSpec;
@@ -56,8 +78,12 @@ interface LayerSpec {
56
78
  flip?: boolean;
57
79
  /** Per-entity opacity range, both 0..1. Default [1,1]. */
58
80
  alpha?: [number, number];
59
- /** Compositing for this layer. 'lighter' = additive (glow stacking). Default source-over. */
60
- blend?: 'lighter';
81
+ /**
82
+ * Compositing for this layer. 'lighter' = additive (glow stacking); 'screen' =
83
+ * gentler additive for pale backgrounds; 'multiply' = darkening (shadows,
84
+ * silhouettes). Default source-over. None of these can strobe.
85
+ */
86
+ blend?: 'lighter' | 'screen' | 'multiply';
61
87
  /**
62
88
  * Fractional spawn window (0 = left/top, 1 = right/bottom). Constrains where
63
89
  * entities are PLACED, not where they may travel. Default full viewport.
@@ -71,10 +97,20 @@ interface LayerSpec {
71
97
  * period has a floor (LIMITS.minPulsePeriod — max 2 Hz), and every entity gets
72
98
  * its own seeded phase, so a layer can never strobe in unison. Effective alpha
73
99
  * is clamped to 0..1.
100
+ *
101
+ * `wave` derives each entity's phase from its spawn position instead of a seeded
102
+ * draw, turning independent breathing into a traveling wave across the field
103
+ * (ripples, wind through grass). `wavelength` is dimensional (px or viewport
104
+ * units); `angle` is the propagation direction in degrees (0 = rightward,
105
+ * default 0). Still flash-safe: neighbors are out of phase by construction.
74
106
  */
75
107
  pulse?: {
76
108
  amp: number;
77
109
  period: number;
110
+ wave?: {
111
+ wavelength: number;
112
+ angle?: number;
113
+ };
78
114
  };
79
115
  /**
80
116
  * Per-entity rotation speed in degrees/sec (positive = clockwise).
@@ -104,7 +140,13 @@ interface LayerSpec {
104
140
  y: number;
105
141
  };
106
142
  /**
107
- * Inter-entity links: draw lines to each entity's k nearest neighbors within maxDist.
143
+ * Inter-entity links. `mode` picks the wiring:
144
+ * - 'nearest' (default): each entity's k nearest neighbors within maxDist.
145
+ * - 'chain': entities connected in order (0-1-2-…) — Mystify polygons, string
146
+ * art. Ignores k/maxDist; `closed` joins the last entity back to the first.
147
+ * - 'random': a fixed golden-ratio-stride wiring (k partners per entity,
148
+ * deterministic, no RNG draws) — breaks the crystalline k-nearest look.
149
+ * `falloff` fades link alpha with distance/maxDist, killing pop-in at the cutoff.
108
150
  * Capped at LIMITS.maxLinksK. Layer count must be <= LIMITS.maxLinkLayerCount when set.
109
151
  */
110
152
  links?: {
@@ -113,6 +155,42 @@ interface LayerSpec {
113
155
  color?: string;
114
156
  alpha?: number;
115
157
  width?: number;
158
+ mode?: 'nearest' | 'chain' | 'random';
159
+ falloff?: boolean;
160
+ closed?: boolean;
161
+ };
162
+ /**
163
+ * Afterglow trail behind moving entities. Samples past positions analytically
164
+ * (no state, fully deterministic). `length` in ms, `fade` 0..1 (default 1 = full fade).
165
+ */
166
+ trail?: {
167
+ length: number;
168
+ fade?: number;
169
+ };
170
+ /**
171
+ * Grid placement instead of random scatter: entities fill cells row-major within
172
+ * `region`. `columns` defaults to an aspect-fit square-ish grid; `jitter` (0..1,
173
+ * scalar or per-axis) offsets each entity within its cell by a seeded fraction of
174
+ * the cell size — `{ y: 1 }` keeps columns crisp while scattering vertically
175
+ * (Matrix rain). Unlocks column effects, LED walls, mosaics, uniform dot fields.
176
+ */
177
+ layout?: {
178
+ type: 'grid';
179
+ columns?: number;
180
+ jitter?: number | {
181
+ x?: number;
182
+ y?: number;
183
+ };
184
+ };
185
+ /**
186
+ * Layer lifecycle for act structure — a pure function of t, no state. Alpha is 0
187
+ * before `enter` (ms), ramps up over `fade` ms (default 1000), holds at 1, then
188
+ * ramps down starting at `exit`. Entities are skipped entirely while at alpha 0.
189
+ */
190
+ life?: {
191
+ enter?: number;
192
+ exit?: number;
193
+ fade?: number;
116
194
  };
117
195
  }
118
196
  type SpriteSpec = {
@@ -129,13 +207,52 @@ type SpriteSpec = {
129
207
  maxWidth?: number;
130
208
  cycle?: CycleSpec;
131
209
  }
132
- /** `soft` renders a radial falloff (glow orb) instead of a hard disc. */
210
+ /** `soft` renders a radial falloff (glow orb) instead of a hard disc.
211
+ * `colorWeights` (same length as `colors`) biases the seeded per-entity pick —
212
+ * "mostly cool tones, occasional ember" without duplicating entries. */
133
213
  | {
134
214
  kind: 'circle';
135
215
  radius: [number, number];
136
216
  color: string;
137
217
  soft?: boolean;
138
218
  colors?: string[];
219
+ colorWeights?: number[];
220
+ }
221
+ /** Unfilled circle (bubbles, portals, sonar pings). `width` = stroke width. */
222
+ | {
223
+ kind: 'ring';
224
+ radius: [number, number];
225
+ color: string;
226
+ width?: number;
227
+ colors?: string[];
228
+ colorWeights?: number[];
229
+ }
230
+ /**
231
+ * A line segment oriented along the entity's instantaneous heading (derived
232
+ * analytically from its motion) with a faded tail — rain that reads as rain,
233
+ * shooting stars, warp stars. `length` is the segment length range; `width` the
234
+ * stroke width.
235
+ */
236
+ | {
237
+ kind: 'streak';
238
+ length: [number, number];
239
+ color: string;
240
+ width?: number;
241
+ colors?: string[];
242
+ colorWeights?: number[];
243
+ }
244
+ /**
245
+ * Axis-aligned rectangle (rotates with `spin`). `width` is the horizontal size
246
+ * range; `aspect` the height/width ratio range (default [1,1] = squares).
247
+ * Mondrian blocks, confetti, city lights.
248
+ */
249
+ | {
250
+ kind: 'rect';
251
+ width: [number, number];
252
+ aspect?: [number, number];
253
+ color: string;
254
+ colors?: string[];
255
+ colorWeights?: number[];
139
256
  };
140
257
  /** Rotate through sprite variants over time. Each entity offsets by its seeded phase. */
141
258
  interface CycleSpec {
@@ -174,7 +291,10 @@ type MotionSpec =
174
291
  /**
175
292
  * Orbit around a center point. Each entity gets a seeded radius from `radius`
176
293
  * and a seeded phase. `speed` is angular velocity in degrees/sec. `center` is
177
- * fractional {x, y} (default {0.5, 0.5} = viewport center).
294
+ * fractional {x, y} (default {0.5, 0.5} = viewport center) — or
295
+ * `{ layer: key }` to orbit a single-entity parent layer (moons around a
296
+ * wandering planet). Strictly one level deep: the parent may not itself
297
+ * orbit a layer, and must have `count: 1`.
178
298
  */
179
299
  | {
180
300
  type: 'orbit';
@@ -183,7 +303,59 @@ type MotionSpec =
183
303
  center?: {
184
304
  x: number;
185
305
  y: number;
306
+ } | {
307
+ layer: string;
186
308
  };
309
+ }
310
+ /**
311
+ * Organic harmonic drift — the analytic flow field. Each entity moves at a base
312
+ * velocity from `speed` plus 3 seeded sine octaves per axis, producing flowing
313
+ * curved paths (Flurry streams, aurora, jellyfish) with zero simulation state.
314
+ * `angle` fixes the base heading in degrees (omit for a seeded heading per
315
+ * entity); `meander` scales the harmonic amplitude (dimensional, default 0.05
316
+ * viewport units); `coherence` (0..1) blends every entity's harmonics toward a
317
+ * shared layer-level set — at 1 the field undulates in unison (fake flocking).
318
+ */
319
+ | {
320
+ type: 'wander';
321
+ speed: [number, number];
322
+ angle?: number;
323
+ meander?: number;
324
+ coherence?: number;
325
+ }
326
+ /**
327
+ * Perspective starfield: entities live on a depth axis and stream toward the
328
+ * viewer, projected as `screen = center + offset / z`. Size and alpha scale
329
+ * with 1/z (small and faint at the far plane, large and fast up close), and z
330
+ * wraps — the honest warp tunnel. `speed` is in depth-units/sec
331
+ * (1 = full near-to-far span per second, capped at LIMITS.maxWarpSpeed).
332
+ */
333
+ | {
334
+ type: 'warp';
335
+ speed: [number, number];
336
+ center?: {
337
+ x: number;
338
+ y: number;
339
+ };
340
+ }
341
+ /**
342
+ * Choreographed spline motion: entities traverse `points` (fractional {x,y})
343
+ * over `duration` ms. `curve: 'smooth'` (default) uses Catmull-Rom through the
344
+ * points; 'linear' uses straight segments. `closed` (default true) loops the
345
+ * path; open paths ping-pong. Each entity gets a seeded phase offset along the
346
+ * path, and `scatter` (dimensional) adds a seeded per-entity offset so shared
347
+ * paths don't stack. Figure-eights, sweeping arcs, patrol routes.
348
+ */
349
+ | {
350
+ type: 'path';
351
+ points: Array<{
352
+ x: number;
353
+ y: number;
354
+ }>;
355
+ duration: number;
356
+ curve?: 'linear' | 'smooth';
357
+ closed?: boolean;
358
+ scatter?: number;
187
359
  };
188
360
  /** A validation problem, pointing at a JSON path within the spec. */
189
361
  interface SpecError {
@@ -193,6 +365,8 @@ interface SpecError {
193
365
  interface ValidationResult {
194
366
  valid: boolean;
195
367
  errors: SpecError[];
368
+ /** Non-blocking warnings about unknown/misplaced properties or likely authoring mistakes. */
369
+ warnings?: SpecWarning[];
196
370
  }
197
371
  /** An advisory warning (non-blocking). Returned by `adviseSpec`. */
198
372
  interface SpecWarning {
@@ -215,18 +389,46 @@ declare const LIMITS: {
215
389
  readonly maxLinkLayerCount: 200;
216
390
  readonly minCyclePeriod: 500;
217
391
  readonly referenceViewport: 1080;
392
+ readonly maxTrailLength: 5000;
393
+ readonly maxTrailSamples: 24;
394
+ readonly minDriftPeriod: 10000;
395
+ readonly maxDriftAmount: 0.3;
396
+ readonly maxGhosting: 0.95;
397
+ readonly maxGhostReplayFrames: 120;
398
+ readonly maxMeander: 500;
399
+ readonly maxWarpSpeed: 1.5;
400
+ readonly minPathPoints: 2;
401
+ readonly maxPathPoints: 24;
402
+ readonly minPathDuration: 2000;
403
+ readonly maxGridColumns: 100;
218
404
  };
219
405
 
220
406
  /**
221
407
  * Validate an untrusted (agent- or JSON-authored) spec structurally + semantically.
222
- * Returns typed errors; never throws. `compileSaver` refuses to run an invalid spec.
408
+ * Returns typed errors and warnings; never throws. `compileSaver` refuses to run an invalid spec.
409
+ *
410
+ * Warnings are non-blocking advisories about unknown/misplaced properties and
411
+ * near-zero speeds. They don't prevent compilation but indicate likely authoring mistakes.
223
412
  */
224
413
  declare function validateSpec(spec: unknown): ValidationResult;
225
414
  /** Narrowing helper: validate + cast. Throws with a joined message when invalid. */
226
415
  declare function assertValidSpec(spec: unknown): SaverSpec;
227
416
 
417
+ /** Near plane for warp motion. z lives in [WARP_NEAR, 1]; screen scale is 1/z. */
418
+ declare const WARP_NEAR = 0.08;
419
+ /** One axis-pair of seeded harmonic octaves for wander motion (amps px, freqs rad/ms). */
420
+ interface WanderOsc {
421
+ ax: number[];
422
+ fx: number[];
423
+ phx: number[];
424
+ ay: number[];
425
+ fy: number[];
426
+ phy: number[];
427
+ }
228
428
  /** A seeded entity: initial state + resolved velocity. `positionAt` is a pure function
229
- * of this + time, so the rAF loop and renderFrame(t) produce identical results. */
429
+ * of this + time, so the rAF loop and renderFrame(t) produce identical results.
430
+ * IMPORTANT (determinism/compat): fields for newer features are OPTIONAL and only
431
+ * set when the feature is declared, so older specs snapshot byte-identically. */
230
432
  interface Entity {
231
433
  x0: number;
232
434
  y0: number;
@@ -236,7 +438,7 @@ interface Entity {
236
438
  vx: number;
237
439
  vy: number;
238
440
  bob: number;
239
- motion: 'drift' | 'rise' | 'bounce' | 'static' | 'orbit';
441
+ motion: 'drift' | 'rise' | 'bounce' | 'static' | 'orbit' | 'wander' | 'warp' | 'path';
240
442
  headingLeft: boolean;
241
443
  alpha: number;
242
444
  pulseAmp: number;
@@ -252,6 +454,39 @@ interface Entity {
252
454
  orbitCy: number;
253
455
  colorIndex: number;
254
456
  cyclePeriod: number;
457
+ /** Rect sprite second dimension (height px). Only set for rect sprites. */
458
+ size2?: number;
459
+ /** Orbit parent layer key (motion.center = { layer }). Center resolved at render time. */
460
+ orbitParent?: string;
461
+ /** Harmonic drift params. Only set for wander motion. */
462
+ wander?: {
463
+ own: WanderOsc;
464
+ shared: WanderOsc;
465
+ coherence: number;
466
+ margin: number;
467
+ };
468
+ /** Depth-axis params. Only set for warp motion. */
469
+ warp?: {
470
+ ux: number;
471
+ uy: number;
472
+ z0: number;
473
+ vz: number;
474
+ cx: number;
475
+ cy: number;
476
+ };
477
+ /** Spline params (pts in px, shared per layer). Only set for path motion. */
478
+ path?: {
479
+ pts: Array<{
480
+ x: number;
481
+ y: number;
482
+ }>;
483
+ duration: number;
484
+ closed: boolean;
485
+ smooth: boolean;
486
+ phase: number;
487
+ offX: number;
488
+ offY: number;
489
+ };
255
490
  }
256
491
  interface Placed {
257
492
  x: number;
@@ -260,18 +495,52 @@ interface Placed {
260
495
  }
261
496
  declare function spriteVariants(sprite: SpriteSpec): number;
262
497
  /** Deterministically place a layer's entities using the seeded RNG (never Math.random).
263
- * `scale` multiplies every dimensional draw (default 1). Used for viewport units. */
264
- declare function buildEntities(layer: LayerSpec, rng: Rng, w: number, h: number, scale?: number): Entity[];
498
+ * `scale` multiplies every dimensional draw (default 1). Used for viewport units.
499
+ * `countScale` multiplies entity count for density-aware scaling (default 1). */
500
+ declare function buildEntities(layer: LayerSpec, rng: Rng, w: number, h: number, scale?: number, countScale?: number): Entity[];
265
501
  /** Analytic opacity of an entity at logical time `t` (ms). Pure, clamped to 0..1. */
266
502
  declare function alphaAt(e: Entity, t: number): number;
267
503
  /** Analytic size multiplier at time `t` (ms). Pure, clamped to > 0. */
268
504
  declare function sizeAt(e: Entity, t: number): number;
269
505
  /** Analytic rotation angle at time `t` (ms) in radians. Pure. */
270
506
  declare function rotationAt(e: Entity, t: number): number;
507
+ /** Layer lifecycle alpha multiplier at time `t` (ms). Pure. 1 when `life` is unset. */
508
+ declare function lifeAlphaAt(life: {
509
+ enter?: number;
510
+ exit?: number;
511
+ fade?: number;
512
+ } | undefined, t: number): number;
271
513
  /** Analytic position of an entity at logical time `t` (ms). Pure & deterministic. */
272
514
  declare function positionAt(e: Entity, t: number, w: number, h: number): Placed;
515
+ /**
516
+ * Analytic heading (radians) at time `t`, from a finite difference of positionAt.
517
+ * Used to orient streak sprites. Returns null across a wrap seam (position jump
518
+ * larger than half the viewport) — callers should skip orientation that frame.
519
+ */
520
+ declare function headingAt(e: Entity, t: number, w: number, h: number): number | null;
273
521
  /** Time-varying sprite index for text/emoji cycling. Returns static index when cyclePeriod is 0. */
274
522
  declare function spriteIndexAt(e: Entity, t: number, variants: number): number;
523
+ /** An inter-entity link edge with its (wrap-aware) distance, for falloff alpha. */
524
+ interface LinkEdge {
525
+ i: number;
526
+ j: number;
527
+ dist: number;
528
+ }
529
+ /**
530
+ * Edge list for a layer's `links`, dispatching on mode:
531
+ * 'nearest' (default) k-nearest within maxDist; 'chain' sequential order
532
+ * (Mystify — ignores k/maxDist, `closed` joins last to first); 'random' a fixed
533
+ * golden-ratio-stride wiring filtered by maxDist (deterministic, no RNG draws).
534
+ */
535
+ declare function linkEdges(links: {
536
+ k: number;
537
+ maxDist: number;
538
+ mode?: 'nearest' | 'chain' | 'random';
539
+ closed?: boolean;
540
+ }, positions: Array<{
541
+ x: number;
542
+ y: number;
543
+ }>, maxDistPx: number, doWrap: boolean, w: number, h: number): LinkEdge[];
275
544
  /** K-nearest-neighbor edge list. Deterministic tie-break: (dist, index).
276
545
  * When `doWrap` is true, uses toroidal distance so links don't streak across wrap seams. */
277
546
  declare function linkPairs(positions: Array<{
@@ -318,6 +587,16 @@ declare function applyDeltasToSpec(spec: SaverSpec, deltas: SteerDelta[]): Saver
318
587
  * target immediately (k > 0). Layer counts round to integers.
319
588
  */
320
589
  declare function lerpSpec(from: SaverSpec, to: SaverSpec, k: number): SaverSpec;
590
+ /**
591
+ * Enumerate all steerable leaf paths in a (resolved) spec. Returns dot-paths
592
+ * like "layers.0.count", "background.stops.1.color", etc. Metadata fields
593
+ * (id, label, schemaVersion, seed, units, kind, type, key) are excluded —
594
+ * they describe structure, not tuneable values.
595
+ *
596
+ * Layer keys are NOT substituted: paths always use numeric indices.
597
+ * Consumers can map to key-based paths via resolveSpecPath if needed.
598
+ */
599
+ declare function steerablePaths(spec: unknown): string[];
321
600
  /** Smooth (ease-in-out) progress curve used for glides. */
322
601
  declare function easeSmooth(k: number): number;
323
602
  /**
@@ -335,6 +614,202 @@ declare function adviseSpec(spec: SaverSpec, viewport?: {
335
614
  height: number;
336
615
  }): SpecWarning[];
337
616
 
617
+ interface LayerSnapshot {
618
+ key: string | undefined;
619
+ count: number;
620
+ centroid: {
621
+ x: number;
622
+ y: number;
623
+ };
624
+ coverage: number;
625
+ meanAlpha: number;
626
+ linksDrawn: number | null;
627
+ linksExpected: number | null;
628
+ connectedComponents: number | null;
629
+ isolatedNodes: number | null;
630
+ }
631
+ interface SceneSnapshot {
632
+ t: number;
633
+ viewport: {
634
+ w: number;
635
+ h: number;
636
+ };
637
+ layers: LayerSnapshot[];
638
+ warnings: string[];
639
+ }
640
+ interface SceneDescription {
641
+ spec: {
642
+ id: string;
643
+ label: string;
644
+ units: string;
645
+ };
646
+ snapshots: SceneSnapshot[];
647
+ }
648
+ /**
649
+ * Deterministic scene analysis at one or more time values. Reuses `buildEntities`
650
+ * and `linkPairs` — no render needed. Returns structured data a non-vision model
651
+ * can use to understand what a spec looks like when rendered.
652
+ */
653
+ declare function describeScene(spec: SaverSpec, options?: {
654
+ viewport?: {
655
+ width: number;
656
+ height: number;
657
+ };
658
+ seed?: number;
659
+ times?: number[];
660
+ }): SceneDescription;
661
+
662
+ interface PerceiveOptions {
663
+ viewport?: {
664
+ width: number;
665
+ height: number;
666
+ };
667
+ seed?: number;
668
+ /** Sample time in ms. Default 5000 (past all typical life.enter staging). */
669
+ t?: number;
670
+ }
671
+ interface LuminanceGrid {
672
+ cols: number;
673
+ rows: number;
674
+ /** Row-major luminance 0..1 including the background. */
675
+ cells: number[];
676
+ /** Per-row background luminance (what an empty scene would be). */
677
+ background: number[];
678
+ meanLuminance: number;
679
+ /** Fraction of cells deviating perceptibly (> 0.03) from the background. */
680
+ coverage: number;
681
+ /** Deviation-weighted centre of visual mass as viewport fractions, or null for a flat scene. */
682
+ centroid: {
683
+ x: number;
684
+ y: number;
685
+ } | null;
686
+ /** Mean deviation-from-background per row/column — 1D transects of the composition. */
687
+ rowProfile: number[];
688
+ colProfile: number[];
689
+ }
690
+ interface LuminanceGridOptions extends PerceiveOptions {
691
+ /** Grid resolution. Defaults 80×48 (→ a 40×12 braille map). */
692
+ cols?: number;
693
+ rows?: number;
694
+ }
695
+ /**
696
+ * Sample the spec into a coarse luminance image — analytically, no renderer.
697
+ * Approximations (documented, deliberate): trails and ghosting are ignored,
698
+ * soft circles use a linear falloff, background drift is sampled at its rest
699
+ * position. Good enough to perceive composition, focus, and balance.
700
+ */
701
+ declare function luminanceGrid(spec: SaverSpec, opts?: LuminanceGridOptions): LuminanceGrid;
702
+ /**
703
+ * Encode a luminance grid as a braille "image": each output character is a 2×4
704
+ * pixel cell, ordered-dithered so mid-tones render as dot density. A grid of
705
+ * 80×48 becomes 12 lines of 40 chars — a picture a text model can read whole.
706
+ *
707
+ * Auto-exposure: most savers live in the bottom of the luminance range, so the
708
+ * cells are normalized (min → 98th percentile) before dithering — like a
709
+ * camera exposing for the scene. Stats on the grid itself stay raw.
710
+ */
711
+ declare function renderBrailleMap(grid: LuminanceGrid): string;
712
+ interface DominanceEntry {
713
+ rank: number;
714
+ layerIndex: number;
715
+ key: string | undefined;
716
+ /** Normalized share of total visual weight, 0..1 (all entries sum to 1). */
717
+ share: number;
718
+ entityCount: number;
719
+ meanLuma: number;
720
+ factors: {
721
+ area: number;
722
+ contrast: number;
723
+ blendBoost: number;
724
+ motionBoost: number;
725
+ };
726
+ }
727
+ /**
728
+ * Rank layers by estimated visual weight — where the eye goes. Weight is
729
+ * on-screen area × alpha, scaled by contrast against the background, an
730
+ * additive-glow boost, and a motion boost. Link lines count as area.
731
+ */
732
+ declare function dominanceRanking(spec: SaverSpec, opts?: PerceiveOptions): DominanceEntry[];
733
+ interface LayerMotionStats {
734
+ layerIndex: number;
735
+ key: string | undefined;
736
+ /** Mean / max on-screen speed in px per second (wrap jumps excluded). */
737
+ meanSpeed: number;
738
+ maxSpeed: number;
739
+ moving: boolean;
740
+ }
741
+ /** Per-layer displacement between t and t+dt — choreography as numbers. */
742
+ declare function motionStats(spec: SaverSpec, opts?: PerceiveOptions & {
743
+ dt?: number;
744
+ }): LayerMotionStats[];
745
+ declare const REGION_NAMES: readonly ["top-left", "top-center", "top-right", "mid-left", "center", "mid-right", "bottom-left", "bottom-center", "bottom-right"];
746
+ interface SceneDiff {
747
+ t: number;
748
+ coverage: {
749
+ a: number;
750
+ b: number;
751
+ delta: number;
752
+ };
753
+ meanLuminance: {
754
+ a: number;
755
+ b: number;
756
+ delta: number;
757
+ };
758
+ /** Shift of the visual-mass centroid from A to B, in viewport fractions. */
759
+ balanceShift: {
760
+ dx: number;
761
+ dy: number;
762
+ } | null;
763
+ /** Mean deviation-from-background per third-by-third region, with deltas. */
764
+ regions: Array<{
765
+ region: (typeof REGION_NAMES)[number];
766
+ a: number;
767
+ b: number;
768
+ delta: number;
769
+ }>;
770
+ /** Dominance changes, matched by layer key (falls back to index). Sorted by |share delta|. */
771
+ dominance: Array<{
772
+ key: string;
773
+ rankA: number | null;
774
+ rankB: number | null;
775
+ shareA: number;
776
+ shareB: number;
777
+ }>;
778
+ advisories: {
779
+ added: string[];
780
+ removed: string[];
781
+ };
782
+ }
783
+ /**
784
+ * Compare two specs at the same time/viewport — relative perception. A text
785
+ * model is far better at "is B better than A" than "is A good"; this returns
786
+ * the deltas that question needs: coverage, balance, per-region visual mass,
787
+ * dominance-rank movement, and advisory changes.
788
+ */
789
+ declare function diffScenes(a: SaverSpec, b: SaverSpec, opts?: LuminanceGridOptions): SceneDiff;
790
+ interface ScenePerception {
791
+ t: number;
792
+ /** The picture: a braille luminance map (each char = 2×4 pixels). */
793
+ braille: string;
794
+ coverage: number;
795
+ meanLuminance: number;
796
+ centroid: {
797
+ x: number;
798
+ y: number;
799
+ } | null;
800
+ rowProfile: number[];
801
+ colProfile: number[];
802
+ dominance: DominanceEntry[];
803
+ motion: LayerMotionStats[];
804
+ advisories: ReturnType<typeof adviseSpec>;
805
+ }
806
+ /**
807
+ * Everything a non-vision agent needs to "see" a spec in one deterministic
808
+ * call: the braille picture, composition transects, dominance ranking, motion
809
+ * stats, and advisories. Intended as the payload behind an MCP previewScene.
810
+ */
811
+ declare function perceiveScene(spec: SaverSpec, opts?: LuminanceGridOptions): ScenePerception;
812
+
338
813
  /**
339
814
  * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
340
815
  * that the schema is actually expressive: two layers (fish drifting both ways at varied
@@ -342,10 +817,17 @@ declare function adviseSpec(spec: SaverSpec, viewport?: {
342
817
  */
343
818
  declare const AQUARIUM_SPEC: SaverSpec;
344
819
 
820
+ /**
821
+ * Comet Shower: showcases trails (afterglow behind moving entities) and background
822
+ * drift (gradient stops breathing slowly). Three layers: a static star field, fast
823
+ * comets with long trails, and slow fireflies with short trails and pulse.
824
+ */
825
+ declare const COMETS_SPEC: SaverSpec;
826
+
345
827
  /**
346
828
  * Constellation: drifting stars connected by faint k-nearest links. Exercises
347
829
  * colors[] (multi-hued stars), links (k-nearest neighbor lines), and cycle
348
- * (emoji asterisms rotate glyphs). Stars are soft circles in pixel units.
830
+ * (emoji asterisms rotate glyphs). Stars are soft circles in viewport units.
349
831
  */
350
832
  declare const CONSTELLATION_SPEC: SaverSpec;
351
833
 
@@ -367,23 +849,24 @@ declare const SNOWFALL_SPEC: SaverSpec;
367
849
  declare const LANTERNS_SPEC: SaverSpec;
368
850
 
369
851
  /**
370
- * Sakura Drift: cherry-blossom petals carried on a spring twilight breeze. Five
371
- * correlated-parallax layers build the depth — the far backdrop is a barely-moving
372
- * field of soft blossom-glow orbs (additive, breathing out of phase), then three
373
- * petal layers grow LARGER, FASTER, BRIGHTER and FLUTTER WIDER as they approach
374
- * (the near gust), over a blue-hour gradient with a warm rose horizon and a dark
375
- * grass band. A final low layer of dim pink glow settles at the grass line — fallen
376
- * petals catching the last light. Wind direction is consistent (down-left) across
377
- * every moving layer; petals differ from snowfall by glyph, colour, palette and the
378
- * diagonal breeze, and from lanterns by falling rather than rising.
852
+ * Sakura Drift: cherry-blossom petals carried on a spring twilight breeze.
853
+ * Glow-up pass (v1-ceiling features): petals ride `wander` harmonics with shared
854
+ * `coherence` so gusts move the whole fall together, `spin` tumbles the glyphs,
855
+ * faint `streak` wind-lines cross the frame, the blossom-glow backdrop shimmers
856
+ * as a traveling `pulse.wave`, and dusk fireflies wander the lower field. The
857
+ * ground is a warm plum earth blended out of the sunset (no more abrupt dark
858
+ * band), with slow background `drift` keeping the sky alive.
379
859
  */
380
860
  declare const SAKURA_SPEC: SaverSpec;
381
861
 
382
862
  /**
383
- * Dev Dashboard: a dense ops-style HUD exercising static positioning, keyed layers,
384
- * and text alignment at scale. Two ambient particle layers provide depth behind 28
385
- * pinned text elements arranged in a three-column layout with header, metrics,
386
- * file list, and scrolling event log.
863
+ * Control Center: a general-purpose ambient status HUD (home / ops / anything —
864
+ * not dev-specific). Exercises the static/HUD subset at scale plus the
865
+ * v1-ceiling features: `life` staggers the panels in like a boot sequence, a
866
+ * grid-`layout` LED wall breathes behind everything via a traveling
867
+ * `pulse.wave`, `ring` sprites form the central gauge, and a rect "equalizer"
868
+ * animates with a wave-phased pulse. Every panel is keyed so an agent can
869
+ * steer any reading live (`power-value`, `gauge-value`, `feed-1`, ...).
387
870
  */
388
871
  declare const DASHBOARD_SPEC: SaverSpec;
389
872
 
@@ -395,6 +878,44 @@ declare const DASHBOARD_SPEC: SaverSpec;
395
878
  */
396
879
  declare const ORRERY_SPEC: SaverSpec;
397
880
 
881
+ /**
882
+ * Aurora Borealis: the "fake fluid" recipe — showcases `wander` motion (harmonic
883
+ * drift) with high `coherence` so the curtain undulates as one body, `pulse.wave`
884
+ * (a traveling shimmer across the star field), weighted palettes, additive blend,
885
+ * and `ghosting` smearing the soft glows into continuous curtains of light.
886
+ */
887
+ declare const AURORA_SPEC: SaverSpec;
888
+
889
+ /**
890
+ * Matrix Rain: showcases `layout: grid` with per-axis jitter ({ y: 1 } keeps the
891
+ * columns crisp while de-syncing them vertically), glyph `cycle`, and `ghosting`
892
+ * doing the classic fading-trail work. Two depths of rain plus rare bright heads.
893
+ */
894
+ declare const MATRIX_RAIN_SPEC: SaverSpec;
895
+
896
+ /**
897
+ * Polygons: the classic Mystify-style bouncing-polygon saver, expressed as chain links + heavy
898
+ * ghosting. Showcases `links.mode: 'chain'` with `closed` (vertices wired in order
899
+ * into a polygon), `bounce` motion, and high `ghosting` (the decaying after-images
900
+ * ARE the effect — no per-entity trail sampling involved).
901
+ */
902
+ declare const POLYGONS_SPEC: SaverSpec;
903
+
904
+ /**
905
+ * Night Procession: a choreographed scene. Showcases `path` motion (a smooth
906
+ * figure-eight lantern route with scatter), layer-parented `orbit` (moths circling
907
+ * a wandering lantern-bearer), `life` staging (stars first, the procession enters
908
+ * at 4 s, moths at 7 s), plus `ring` and `rect` sprites.
909
+ */
910
+ declare const PROCESSION_SPEC: SaverSpec;
911
+
912
+ /**
913
+ * Warp Tunnel: showcases `warp` motion (perspective depth — size/alpha/speed scale
914
+ * with 1/z), `streak` sprites oriented along the analytic heading, `colorWeights`
915
+ * (mostly white stars, occasional blue/amber), and light `ghosting` for smear.
916
+ */
917
+ declare const WARP_TUNNEL_SPEC: SaverSpec;
918
+
398
919
  /** Catalog entry for a bundled schema example. */
399
920
  interface SchemaExample {
400
921
  id: string;
@@ -408,4 +929,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
408
929
  /** Lookup by saver id. */
409
930
  declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
410
931
 
411
- export { AQUARIUM_SPEC, type BackgroundSpec, type BandSpec, CONSTELLATION_SPEC, type CycleSpec, DASHBOARD_SPEC, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, LANTERNS_SPEC, LIMITS, type LayerSpec, type MotionSpec, ORRERY_SPEC, type Placed, RAIN_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SchemaExample, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type ValidationResult, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSpec, buildEntities, compileSaver, easeSmooth, lerpSpec, linkPairs, manifestFor, positionAt, resolveSpecPath, rotationAt, sizeAt, spriteIndexAt, spriteVariants, structuralSignature, validateSpec };
932
+ export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COMETS_SPEC, CONSTELLATION_SPEC, type CycleSpec, DASHBOARD_SPEC, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, LANTERNS_SPEC, LIMITS, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionSpec, ORRERY_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type Placed, RAIN_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSpec, buildEntities, compileSaver, describeScene, diffScenes, dominanceRanking, easeSmooth, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, resolveSpecPath, rotationAt, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, validateSpec };