@idle-screens/schema 3.6.1 → 3.8.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
@@ -1,4 +1,4 @@
1
- import { Rng, SaverPlugin, SaverManifest } from '@idle-screens/core';
1
+ import { Rng, SaverContext, SaverInstance, ControlTrack, SaverPlugin, SaverManifest } from '@idle-screens/core';
2
2
 
3
3
  /**
4
4
  * Declarative, agent-authorable saver format. A `SaverSpec` describes a saver as DATA
@@ -40,6 +40,12 @@ interface SaverSpec {
40
40
  * `renderFrame(t)` stays deterministic for seeks via a fixed-step warm-up replay.
41
41
  */
42
42
  ghosting?: number;
43
+ /**
44
+ * The print finish — grain and a dither screen composited over the
45
+ * finished frame as the last step of every render. See `FinishSpec`.
46
+ * Absent ⇒ the frame is presented exactly as drawn.
47
+ */
48
+ finish?: FinishSpec;
43
49
  /** Dimensional unit system. 'viewport' (default) = all sizes/speeds/distances are fractions of min(w,h). */
44
50
  units?: 'viewport' | 'px';
45
51
  /**
@@ -49,6 +55,25 @@ interface SaverSpec {
49
55
  */
50
56
  referenceViewport?: number;
51
57
  }
58
+ /**
59
+ * A print finish over the finished frame: `grain` (0..1) screens a seeded
60
+ * zero-mean noise tile at up to 35 % alpha; `dither` (0..1) screens an 8×8
61
+ * ordered Bayer tile at up to 25 % — a stylistic screen, not per-pixel
62
+ * dithering of the image. Both composite with a symmetric blend (`overlay`)
63
+ * so the frame's mean luminance is unchanged: flash-safe by construction,
64
+ * and invisible to `luminanceGrid`. The grain tile is generated once per
65
+ * mount from the spec seed and sits still; `animate: true` steps its offset
66
+ * deterministically from the frame's time bucket (12 Hz), never randomly —
67
+ * ignored on the `basic` / `minimal` capability tiers (static tile). The
68
+ * finish is applied on a presentation canvas, never fed back into
69
+ * `ghosting`'s persistence. `grain` and `dither` are numeric paint paths
70
+ * (`finish.grain`) and glide.
71
+ */
72
+ interface FinishSpec {
73
+ grain?: number;
74
+ dither?: number;
75
+ animate?: boolean;
76
+ }
52
77
  type BackgroundSpec = {
53
78
  type: 'solid';
54
79
  color: string;
@@ -57,7 +82,45 @@ type BackgroundSpec = {
57
82
  stops: GradientStop[];
58
83
  band?: BandSpec;
59
84
  drift?: BackgroundDrift;
60
- };
85
+ } | FieldBackground;
86
+ /**
87
+ * A scalar-field background: seeded, warped value noise quantised into
88
+ * `bands` — thermal maps, contour terrain, sonar landmasses, riso washes.
89
+ * Pure in `(x, y, t, seed)`: the renderer paints it as a low-res raster
90
+ * (short side 96 px; 48 on the `basic` / `minimal` tiers) scaled to the
91
+ * canvas, recomputed once per 100 ms bucket while drifting and once ever
92
+ * otherwise, and the perception grid samples the same function directly.
93
+ * The sampler uses its own hash, never the entity RNG, so adding a field to
94
+ * a spec leaves every entity where it was.
95
+ */
96
+ interface FieldBackground {
97
+ type: 'field';
98
+ /** Noise features across the short side, 0.5..8 (LIMITS.minFieldScale / maxFieldScale). */
99
+ scale: number;
100
+ /** Octaves of detail, integer 1..4 (default 2). */
101
+ octaves?: number;
102
+ /** Domain-warp amount 0..1 (default 0) — folds round blobs into smeared contours. */
103
+ warp?: number;
104
+ /**
105
+ * Levels to posterise the field into, integer 2..8, or 0 for smooth
106
+ * interpolation between bands (Aura, Mist). Default `bands.length`: one
107
+ * hard band per level.
108
+ */
109
+ quantize?: number;
110
+ /** 2..8 hex colours, darkest-to-lightest or whatever ramp the look wants. */
111
+ bands: string[];
112
+ /** Slow domain travel — the field's "animate". Period floored at 10 s (flash guard). */
113
+ drift?: FieldDrift;
114
+ /** Field seed; defaults to the spec seed. Independent of the entity stream. */
115
+ seed?: number;
116
+ }
117
+ /** Slow travel of a field's sample domain: a circle of radius `amount` feature units per `period`. */
118
+ interface FieldDrift {
119
+ /** Full loop period in ms. Floor: LIMITS.minDriftPeriod (10 s) — the flash guard. */
120
+ period: number;
121
+ /** Radius of the domain travel in feature units, 0..1. Default 0.3. */
122
+ amount?: number;
123
+ }
61
124
  /** A vertical gradient stop (`at` 0 = top, 1 = bottom). */
62
125
  interface GradientStop {
63
126
  at: number;
@@ -131,6 +194,17 @@ interface LayerSpec {
131
194
  * form draws one extra seeded value, only when present).
132
195
  */
133
196
  spin?: number | [number, number];
197
+ /**
198
+ * Static per-entity rotation in degrees (positive = clockwise), ±360. A
199
+ * scalar turns every entity the same way; a `[min, max]` range gives each
200
+ * entity a seeded angle — thrown blades, scattered glyphs, a tilted grid.
201
+ * Composes with `spin` (added to its seeded start angle). Note that `spin:
202
+ * [0, 0]` does NOT do this: a zero spin speed renders at angle 0, its
203
+ * seeded phase unused. Structural (baked into entities). The range form
204
+ * draws one seeded value per entity, only when declared, so existing
205
+ * specs keep identical streams.
206
+ */
207
+ rotate?: number | [number, number];
134
208
  /**
135
209
  * Sinusoidal size breathing, parallel to `pulse` for opacity. `amp` is a
136
210
  * fraction of base size (0.3 = ±30 %). `period` in ms with the same
@@ -291,6 +365,8 @@ type SpriteSpec = {
291
365
  baseline?: 'top' | 'middle' | 'bottom';
292
366
  maxWidth?: number;
293
367
  cycle?: CycleSpec;
368
+ /** See `TextRole`. Absent ⇒ no legibility advisories, no pixel change. */
369
+ role?: TextRole;
294
370
  }
295
371
  /** `soft` renders a radial falloff (glow orb) instead of a hard disc.
296
372
  * `colorWeights` (same length as `colors`) biases the seeded per-entity pick —
@@ -409,13 +485,49 @@ type SpriteSpec = {
409
485
  | {
410
486
  kind: 'textBlock';
411
487
  text: string;
488
+ /** Wrap width as a fraction of `min(w,h)` (up to 2.0 — `text-off-screen` catches overflow). */
412
489
  maxWidth: number;
413
490
  fontSize: number;
414
491
  lineHeight?: number;
492
+ /** Moves the painted lines inside the wrap width (ragged edge); never the block. */
415
493
  align?: 'left' | 'center' | 'right';
416
494
  color?: string;
417
495
  reveal?: TextRevealSpec;
496
+ /**
497
+ * Which point of the rendered text (widest line × lines·lineHeight, as
498
+ * `align` lays it out) `position` names. Absent: today's behaviour —
499
+ * `position` is the top-left of the `maxWidth` layout box. With
500
+ * `'center'` the block is centred at `position` on every aspect ratio.
501
+ * Placement, so it is in the structural signature.
502
+ */
503
+ anchor?: TextBlockAnchor;
504
+ /**
505
+ * CSS font family and/or weight/style only (`"bold monospace"`,
506
+ * `"300 'Inter', sans-serif"`). A size inside it is rejected —
507
+ * `fontSize` owns size. Absent: `system-ui, sans-serif`.
508
+ */
509
+ font?: string;
510
+ /**
511
+ * 0..1 multiplier on the block's paint alpha (default 1). Paint, not
512
+ * carpentry: excluded from the structural signature, so steering it
513
+ * with `dur` glides the block in or out without a rebuild. Declare it
514
+ * (`"opacity": 1`) to make the path steerable.
515
+ */
516
+ opacity?: number;
517
+ /** See `TextRole`. Absent ⇒ no legibility advisories, no pixel change. */
518
+ role?: TextRole;
418
519
  };
520
+ /**
521
+ * What a text layer is *for* — a declaration, not a pixel. `read` says the
522
+ * words must be readable from across the room and opts the layer into
523
+ * `adviseSpec`'s legibility advisories (`text-legibility`, `text-safe-area`).
524
+ * `atmosphere` says the text is texture (a haiku fading in a corner, dim
525
+ * labels on a wall board) and is the same as absent: silent. Changes no
526
+ * pixel anywhere, on any client; never part of the structural signature.
527
+ */
528
+ type TextRole = 'read' | 'atmosphere';
529
+ /** Compass points of a `textBlock`'s rendered box that `position` may name. */
530
+ type TextBlockAnchor = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
419
531
  /**
420
532
  * Animated typing/deleting for `textBlock`. Layout always runs on the FULL
421
533
  * text — reveal only masks which glyphs are painted, so lines never reflow
@@ -582,6 +694,20 @@ interface SpecWarning {
582
694
  path: string;
583
695
  code: string;
584
696
  message: string;
697
+ /**
698
+ * Viewport-fraction boxes (`x`, `y`, `w`, `h` of `width`/`height`) the
699
+ * warning is about — `text-overlap` carries the two text boxes so an agent
700
+ * can move one without re-deriving the layout. Present only where a
701
+ * warning has geometry to report.
702
+ */
703
+ boxes?: WarningBox[];
704
+ }
705
+ /** A rectangle in viewport fractions (x/w of width, y/h of height). */
706
+ interface WarningBox {
707
+ x: number;
708
+ y: number;
709
+ w: number;
710
+ h: number;
585
711
  }
586
712
  /** Perf/safety caps enforced by `validateSpec`. */
587
713
  declare const LIMITS: {
@@ -592,6 +718,7 @@ declare const LIMITS: {
592
718
  readonly maxPulseAmp: 0.5;
593
719
  readonly minPulsePeriod: 500;
594
720
  readonly maxSpin: 360;
721
+ readonly maxRotate: 360;
595
722
  readonly maxGrowAmp: 0.8;
596
723
  readonly maxOrbitSpeed: 180;
597
724
  readonly maxLinksK: 8;
@@ -602,6 +729,13 @@ declare const LIMITS: {
602
729
  readonly maxTrailSamples: 24;
603
730
  readonly minDriftPeriod: 10000;
604
731
  readonly maxDriftAmount: 0.3;
732
+ readonly minFieldScale: 0.5;
733
+ readonly maxFieldScale: 8;
734
+ readonly maxFieldOctaves: 4;
735
+ readonly minFieldBands: 2;
736
+ readonly maxFieldBands: 8;
737
+ readonly maxFieldQuantize: 8;
738
+ readonly maxFieldDriftAmount: 1;
605
739
  readonly maxGhosting: 0.95;
606
740
  readonly maxGhostReplayFrames: 120;
607
741
  readonly maxMeander: 500;
@@ -613,7 +747,7 @@ declare const LIMITS: {
613
747
  readonly maxTextBlockLength: 2000;
614
748
  readonly minTextBlockFontSize: 0.01;
615
749
  readonly maxTextBlockFontSize: 0.2;
616
- readonly maxTextBlockMaxWidth: 1;
750
+ readonly maxTextBlockMaxWidth: 2;
617
751
  readonly maxRevealSpeed: 120;
618
752
  readonly maxCaretBlinkHz: 3;
619
753
  readonly maxSegments: 24;
@@ -637,9 +771,30 @@ declare const LIMITS: {
637
771
  };
638
772
  type SequenceTransition = {
639
773
  type: 'cut';
640
- } | {
774
+ }
775
+ /**
776
+ * Interpolate numbers and hex colours into the next (structurally
777
+ * identical) segment over `dur` ms. Strings step on the first frame —
778
+ * unless `text: 'crossfade'`, which draws each `text` / `textBlock` layer
779
+ * whose string(s) differ twice for the window: the outgoing string at
780
+ * `1 − k`, the incoming at `k` (k = the morph's eased progress). Default
781
+ * `'step'`, today's behaviour, byte for byte.
782
+ */
783
+ | {
641
784
  type: 'morph';
642
785
  dur: number;
786
+ text?: 'step' | 'crossfade';
787
+ }
788
+ /**
789
+ * Cross-fade into the next segment over `dur` ms: the outgoing segment
790
+ * stays alive on its own canvas and is composited over the incoming one
791
+ * at `1 − easeSmooth(localT / dur)`. Works between unlike segments (no
792
+ * structural requirement). Hosts on the `basic`/`minimal` capability tier
793
+ * render it as `cut` (see `SequenceMountContext.capabilityTier`).
794
+ */
795
+ | {
796
+ type: 'fade';
797
+ dur: number;
643
798
  };
644
799
  interface SequenceSegment {
645
800
  key: string;
@@ -657,6 +812,33 @@ interface IdleSequence {
657
812
  label: string;
658
813
  seed?: number;
659
814
  loop: boolean;
815
+ /**
816
+ * What the sequence clock is anchored to. `mount` (default — today's
817
+ * behaviour): every viewer starts at T = 0 when it mounts, so a joiner sees
818
+ * segment 0 (pre-roll semantics). `epoch`: a viewer handed `sequenceBaseT`
819
+ * on its mount context (the host's `Date.now() − epoch`) starts its clock
820
+ * there, so every screen in a room shows the same segment — a late joiner
821
+ * lands mid-loop. `advance: 'input'` holds stay armed under either mode.
822
+ */
823
+ sync?: 'mount' | 'epoch';
824
+ /**
825
+ * A scene drawn under every segment on the sequence's **global** clock —
826
+ * the ground that does not reset at a boundary or under a `sequence.segment`
827
+ * steer. Segments render over it transparently (their `background` and
828
+ * `ghosting` are ignored; the bed may declare its own). Its entities count
829
+ * toward the perf cap together with the largest segment's, since the two
830
+ * are live at once. Steer it with `bed.<path>`. Absent ⇒ every segment
831
+ * paints its own ground, exactly as before the field existed.
832
+ */
833
+ bed?: SaverSpec;
834
+ /**
835
+ * A print finish applied once per composed frame — over bed, segment and
836
+ * any fade together, on the sequence's presentation canvas. Overrides
837
+ * every segment's own `finish` while set; absent, the active segment's
838
+ * `finish` applies when it declares one (a bed's is ignored — the bed is
839
+ * ground, finish the sequence). Children never apply their own.
840
+ */
841
+ finish?: FinishSpec;
660
842
  segments: SequenceSegment[];
661
843
  }
662
844
 
@@ -673,6 +855,8 @@ declare function assertValidSequence(seq: unknown): IdleSequence;
673
855
 
674
856
  /** Near plane for warp motion. z lives in [WARP_NEAR, 1]; screen scale is 1/z. */
675
857
  declare const WARP_NEAR = 0.08;
858
+ /** Cap on the 1/z size multiplier so near stars don't blow out. */
859
+ declare const WARP_MAX_SCALE = 8;
676
860
  /** One axis-pair of seeded harmonic octaves for wander motion (amps px, freqs rad/ms). */
677
861
  interface WanderOsc {
678
862
  ax: number[];
@@ -754,6 +938,8 @@ interface Entity {
754
938
  type: 'settle' | 'buoyant';
755
939
  tau: number;
756
940
  };
941
+ /** Static rotation (radians), added to any spin. Only set when `layer.rotate` is declared. */
942
+ rotate?: number;
757
943
  /** Spline params (pts in px, shared per layer). Only set for path motion. */
758
944
  path?: {
759
945
  pts: Array<{
@@ -790,7 +976,13 @@ declare function emitEnvelope(u: number): number;
790
976
  declare function alphaAt(e: Entity, t: number): number;
791
977
  /** Analytic size multiplier at time `t` (ms). Pure, clamped to > 0. */
792
978
  declare function sizeAt(e: Entity, t: number): number;
793
- /** Analytic rotation angle at time `t` (ms) in radians. Pure. */
979
+ /**
980
+ * Analytic rotation angle at time `t` (ms) in radians. Pure. A static
981
+ * `rotate` is the base; a spinning entity adds its seeded start angle and
982
+ * speed × t on top. With neither, 0 — and with `spin: [0, 0]` also 0: a zero
983
+ * spin speed never rotates, so its seeded phase is not applied (that is what
984
+ * `rotate` is for).
985
+ */
794
986
  declare function rotationAt(e: Entity, t: number): number;
795
987
  /** Layer lifecycle alpha multiplier at time `t` (ms). Pure. 1 when `life` is unset. */
796
988
  declare function lifeAlphaAt(life: {
@@ -835,8 +1027,26 @@ declare function linkPairs(positions: Array<{
835
1027
  x: number;
836
1028
  y: number;
837
1029
  }>, k: number, maxDist: number, doWrap: boolean, w: number, h: number): Array<[number, number]>;
838
- /** Em-width of a string under the same character-class table `breakTextBlock` uses. */
839
- declare function textWidthEm(str: string): number;
1030
+ /**
1031
+ * Which advance table the line-breaker uses. `proportional` is the
1032
+ * character-class table below (the default, and the only table until
1033
+ * `textBlock.font` existed); `mono` is a uniform 0.6 em advance — the classic
1034
+ * monospace cell (Courier, Menlo, SF Mono all sit within a percent of it).
1035
+ */
1036
+ type TextMetricsClass = 'proportional' | 'mono';
1037
+ /**
1038
+ * Metrics class for a `textBlock.font` string: `mono` when the family names
1039
+ * a monospace face (the generic `monospace`, or a well-known fixed-pitch
1040
+ * family), else `proportional`. Absent font ⇒ proportional, as before.
1041
+ */
1042
+ declare function textMetricsClassFor(font?: string): TextMetricsClass;
1043
+ /**
1044
+ * Em-width of a string under the same character-class table `breakTextBlock`
1045
+ * uses. `mono` counts grapheme clusters, not UTF-16 code units — an emoji or
1046
+ * combining/ZWJ sequence is one advance-width cell, matching what's painted,
1047
+ * not one cell per surrogate half or combining mark.
1048
+ */
1049
+ declare function textWidthEm(str: string, metrics?: TextMetricsClass): number;
840
1050
  interface TextBlockLine {
841
1051
  text: string;
842
1052
  widthEm: number;
@@ -846,7 +1056,24 @@ interface TextBlockLine {
846
1056
  * whitespace; a single word wider than the limit gets its own line unbroken.
847
1057
  * Explicit `\n` always forces a break. Pure, deterministic, no canvas needed.
848
1058
  */
849
- declare function breakTextBlock(text: string, maxWidthEm: number): TextBlockLine[];
1059
+ declare function breakTextBlock(text: string, maxWidthEm: number, metrics?: TextMetricsClass): TextBlockLine[];
1060
+ /**
1061
+ * Where an anchored textBlock's layout origin sits relative to `position`:
1062
+ * the (dx, dy) to ADD to `position` so that the named point of the rendered
1063
+ * text lands on it. The rendered box is the widest line wide (placed inside
1064
+ * the `maxWidth` box by `align`) and `lines × lineHeight` tall — so `anchor`
1065
+ * says where the block sits and `align` only shapes its ragged edge. Absent
1066
+ * anchor ⇒ (0, 0): `position` stays the layout box's top-left, exactly as
1067
+ * before the field existed. Renderer, perception and advisories all call
1068
+ * this, so their boxes agree by construction.
1069
+ */
1070
+ declare function textBlockAnchorOffset(s: {
1071
+ anchor?: TextBlockAnchor;
1072
+ align?: 'left' | 'center' | 'right';
1073
+ }, maxWPx: number, maxLineW: number, totalH: number): {
1074
+ dx: number;
1075
+ dy: number;
1076
+ };
850
1077
  /**
851
1078
  * Split a string into paint-safe grapheme clusters: surrogate pairs stay
852
1079
  * whole, combining marks / variation selectors / skin tones attach to their
@@ -887,13 +1114,76 @@ declare function revealState(lines: TextBlockLine[], reveal: TextRevealInput, tM
887
1114
 
888
1115
  /** Derive a manifest so a compiled spec composes with @idle-screens/capabilities. */
889
1116
  declare function manifestFor(spec: SaverSpec): SaverManifest;
1117
+ /**
1118
+ * The device's compute tier (`computeTier` from `@idle-screens/capabilities`;
1119
+ * the string union is repeated here so schema does not depend on that
1120
+ * package). Hosts pass it on the mount context; a compiled scene reads it to
1121
+ * step down work that a `basic` / `minimal` device cannot afford.
1122
+ */
1123
+ type CapabilityTier = 'minimal' | 'basic' | 'standard' | 'high';
1124
+ /**
1125
+ * Mount context for a compiled spec. A plain `SaverContext` works — every
1126
+ * field here is optional. `capabilityTier` `basic` / `minimal` halves a
1127
+ * `field` background's raster resolution (48 px short side instead of 96).
1128
+ * Absent ⇒ full resolution.
1129
+ */
1130
+ interface SpecMountContext extends SaverContext {
1131
+ capabilityTier?: CapabilityTier;
1132
+ }
890
1133
  /**
891
1134
  * Compile a declarative spec into a runnable SaverPlugin. Throws if the spec is invalid
892
1135
  * (so an agent-authored spec is validated before it can run). The result is seeded,
893
1136
  * deterministic, `renderFrame(t,seed)`-addressable, and flash-safe by construction.
894
1137
  */
895
1138
  declare function compileSaver(spec: unknown): SaverPlugin;
896
- declare function compileSequence(spec: unknown): SaverPlugin;
1139
+ /**
1140
+ * Mount context for a compiled sequence. A plain `SaverContext` works — the
1141
+ * clock starts at 0, as every SaverPlugin's does. `sequenceBaseT` is read
1142
+ * only when the sequence declares `sync: 'epoch'`; hosts pass the shared
1143
+ * clock's elapsed ms (idlescreens.com: `Date.now() − scene.epoch`) so every
1144
+ * viewer of that sequence resolves the same segment.
1145
+ */
1146
+ interface SequenceMountContext extends SpecMountContext {
1147
+ /** ms already elapsed on the shared sequence clock. Ignored unless `sync: 'epoch'`. */
1148
+ sequenceBaseT?: number;
1149
+ /**
1150
+ * See `SpecMountContext.capabilityTier`. A `fade` transition keeps two
1151
+ * segments live for `dur`, which `basic` (canvas2d only) and `minimal`
1152
+ * cannot afford: on those tiers fade renders as `cut`. The tier is also
1153
+ * handed to every child and the bed (a `field` background's raster steps
1154
+ * down with it). Absent ⇒ fade enabled, full resolution.
1155
+ */
1156
+ capabilityTier?: CapabilityTier;
1157
+ }
1158
+ /**
1159
+ * What `compileSequence().mount()` returns: a `SaverInstance` that can also
1160
+ * take a republished sequence in place. Hosts feature-detect it with
1161
+ * `hasHotSwapSequence` (an older engine's instance simply lacks the method)
1162
+ * and fall through to a remount when it returns `false`.
1163
+ */
1164
+ interface SequenceSaverInstance extends SaverInstance {
1165
+ renderFrame(t: number, seed: number): void;
1166
+ applyTrack(track: ControlTrack): void;
1167
+ /**
1168
+ * Swap `next` in for the running sequence without remounting, when the
1169
+ * two are `sequenceSwapCompatible`: every live segment child (and the bed)
1170
+ * hot-swaps to its new scene, the clock, the active segment, every
1171
+ * released hold and the retained track stay exactly where they were, and
1172
+ * the next frame resolves to the same `(segment, localT)` as if nothing
1173
+ * had happened — a mid-show caption fix that does not send the room back
1174
+ * to segment 0. Returns `false` and changes nothing when the sequences are
1175
+ * not compatible (a structural or timing edit); the host remounts then.
1176
+ */
1177
+ hotSwapSequence(next: IdleSequence): boolean;
1178
+ }
1179
+ /** `compileSequence`'s plugin: `mount` is typed to the sequence instance and `spec` to the validated sequence. */
1180
+ interface SequenceSaverPlugin extends SaverPlugin {
1181
+ mount(ctx: SequenceMountContext): SequenceSaverInstance;
1182
+ spec: IdleSequence;
1183
+ }
1184
+ /** Feature-detect `hotSwapSequence` on any mounted instance (false for a plain scene, or a sequence from an older engine). */
1185
+ declare function hasHotSwapSequence(inst: SaverInstance): inst is SequenceSaverInstance;
1186
+ declare function compileSequence(spec: unknown): SequenceSaverPlugin;
897
1187
 
898
1188
  /**
899
1189
  * Pure helpers for LIVE steering of compiled specs (setParam / applyTrack).
@@ -925,9 +1215,23 @@ declare function applyDeltasToSpec(spec: SaverSpec, deltas: SteerDelta[]): Saver
925
1215
  * target immediately (k > 0). Layer counts round to integers.
926
1216
  */
927
1217
  declare function lerpSpec(from: SaverSpec, to: SaverSpec, k: number): SaverSpec;
1218
+ /**
1219
+ * The string(s) a text layer paints — `strings` of a `text` sprite, `text`
1220
+ * of a `textBlock` — or null for every other sprite.
1221
+ */
1222
+ declare function textStringsOf(sprite: SpriteSpec): string[] | null;
1223
+ /**
1224
+ * Whether the text layer at index `i` paints different words in `a` and
1225
+ * `b` — the layers a morph `text: 'crossfade'` draws twice. False for
1226
+ * non-text layers and when the strings match.
1227
+ */
1228
+ declare function textStringsDiffer(a: SaverSpec, b: SaverSpec, i: number): boolean;
1229
+ declare function morphNothingMorphable(a: SaverSpec, b: SaverSpec, opts?: {
1230
+ textCrossfade?: boolean;
1231
+ }): boolean;
928
1232
  /**
929
1233
  * Enumerate all steerable leaf paths in a (resolved) spec. Returns dot-paths
930
- * like "layers.0.count", "background.stops.1.color", etc. Metadata fields
1234
+ * like "layers.0.count", "background.stops.1.color", "background.bands.2", etc. Metadata fields
931
1235
  * (id, label, schemaVersion, seed, units, kind, type, key) are excluded —
932
1236
  * they describe structure, not tuneable values.
933
1237
  *
@@ -935,6 +1239,14 @@ declare function lerpSpec(from: SaverSpec, to: SaverSpec, k: number): SaverSpec;
935
1239
  * Consumers can map to key-based paths via resolveSpecPath if needed.
936
1240
  */
937
1241
  declare function steerablePaths(spec: unknown): string[];
1242
+ /**
1243
+ * Every path a sequence accepts on its track: `sequence.segment` (the
1244
+ * clicker), the bed's paths under the `bed.` prefix, and the union of every
1245
+ * segment's paths (a segment path lands on whichever segment owns it — see
1246
+ * `SequenceInstance.applyTrack`). Deduplicated, numeric layer indices as in
1247
+ * `steerablePaths`.
1248
+ */
1249
+ declare function sequenceSteerablePaths(seq: IdleSequence): string[];
938
1250
  /** Smooth (ease-in-out) progress curve used for glides. */
939
1251
  declare function easeSmooth(k: number): number;
940
1252
  /**
@@ -956,10 +1268,18 @@ declare function adviseSpec(spec: SaverSpec, viewport?: {
956
1268
  * describe the same scene its other channels do — without them a caller
957
1269
  * perceiving at t = 30 s would get `form` for that instant beside an
958
1270
  * `overlap-seams` advisory computed for a different one.
1271
+ *
1272
+ * `backgroundSeed` is separate from `seed` because a sequence's
1273
+ * `inkOverBed` (the segment's ink layers judged against the bed's
1274
+ * background — see `perceiveSequenceFrame`) mixes two scenes with
1275
+ * potentially different seeds: `seed` drives the ink layers' own entity
1276
+ * stream, `backgroundSeed` samples the bed's field. Defaults to `seed` —
1277
+ * every other caller has one scene and one seed for both.
959
1278
  */
960
1279
  opts?: {
961
1280
  t?: number;
962
1281
  seed?: number;
1282
+ backgroundSeed?: number;
963
1283
  }): SpecWarning[];
964
1284
  /**
965
1285
  * Non-blocking advisory warnings for a validated sequence envelope.
@@ -1070,10 +1390,12 @@ interface SceneSnapshot {
1070
1390
  warnings: string[];
1071
1391
  }
1072
1392
  interface SceneDescription {
1393
+ /** `finish` is present only when the spec declares one — a print screen the luminance numbers do not include. */
1073
1394
  spec: {
1074
1395
  id: string;
1075
1396
  label: string;
1076
1397
  units: string;
1398
+ finish?: FinishSpec;
1077
1399
  };
1078
1400
  snapshots: SceneSnapshot[];
1079
1401
  }
@@ -1105,8 +1427,15 @@ interface LuminanceGrid {
1105
1427
  rows: number;
1106
1428
  /** Row-major luminance 0..1 including the background. */
1107
1429
  cells: number[];
1108
- /** Per-row background luminance (what an empty scene would be). */
1430
+ /** Per-row background luminance (what an empty scene would be). For a `field` background: each row's mean. */
1109
1431
  background: number[];
1432
+ /**
1433
+ * Per-cell background luminance, present only for a `field` background
1434
+ * (a solid or gradient varies by row alone, and `background` says it all).
1435
+ * Coverage, centroid and the profiles deviate from THIS, so the field's own
1436
+ * contours never count as content — a bright field is ground, not ink.
1437
+ */
1438
+ backgroundCells?: number[];
1110
1439
  meanLuminance: number;
1111
1440
  /** Fraction of cells deviating perceptibly (> 0.03) from the background. */
1112
1441
  coverage: number;
@@ -1289,6 +1618,211 @@ interface ScenePerception {
1289
1618
  * Intended as the payload behind an MCP previewScene.
1290
1619
  */
1291
1620
  declare function perceiveScene(spec: SaverSpec, opts?: LuminanceGridOptions): ScenePerception;
1621
+ interface SequenceFramePerception extends ScenePerception {
1622
+ /** Which segment the global time `T` resolved to, and where in it. */
1623
+ segment: {
1624
+ index: number;
1625
+ key: string;
1626
+ localT: number;
1627
+ held?: true;
1628
+ };
1629
+ /** True when a bed was composed under the segment. */
1630
+ bed: boolean;
1631
+ }
1632
+ interface PerceiveSequenceOptions extends Omit<LuminanceGridOptions, 't'> {
1633
+ /** Holds released by the clicker so far — see `ResolveOptions.releasedBelow`. */
1634
+ releasedBelow?: number;
1635
+ }
1636
+ /**
1637
+ * Perceive one frame of a sequence at global time `T`: resolve the segment,
1638
+ * then — when the sequence has a `bed` — compose the bed at `T` (its clock
1639
+ * never resets) under the segment's ink at `localT`, the segment's own
1640
+ * background dropped, exactly as the renderer stacks them. The luminance maps
1641
+ * add the two grids (clamped), so coverage, centroid and the profiles are of
1642
+ * the composite; dominance ranks bed and segment layers together by their raw
1643
+ * weights, bed layers keyed `bed:<key|index>`; text, motion and form list bed
1644
+ * layers first with the same prefix. Bed and segment are assumed to share
1645
+ * `units`/`referenceViewport`, as the renderer's shared canvas assumes. The
1646
+ * bed and segment seeds come from `segmentRenderSeed`/`bedRenderSeed`
1647
+ * (`./sequence`) — the same derivation `SequenceInstance` uses, chain roots
1648
+ * and zero-seed normalization included — so the composed frame matches what
1649
+ * the renderer actually draws at `T`, not just what the raw specs would show.
1650
+ * Without a bed this is `perceiveScene(segment, localT)` plus the `segment`
1651
+ * field. Intended as the payload behind a sequence-aware previewScene.
1652
+ *
1653
+ * Two known approximations, both accepted for a coarse, cheap, renderer-free
1654
+ * tool rather than fixed here:
1655
+ * - **The transition WINDOW isn't composed.** During a live `fade` or `morph`
1656
+ * (i.e. `localT < transition.dur`) the renderer also paints the outgoing
1657
+ * (or interpolating) segment, but this only ever perceives the segment
1658
+ * `resolveSegment` resolves to — call it at a `T` outside the transition's
1659
+ * `dur` for an accurate read. (Outside that window, a morph-chained
1660
+ * segment's entities and seed ARE correct — `segmentRenderSeed` already
1661
+ * resolves the chain root.) Composing the live window properly needs the
1662
+ * same capability-tier gate `SequenceInstance` uses to decide whether
1663
+ * `fade` even plays; this analytical path has no such context.
1664
+ * - **Ink composites onto the bed by addition, not by blend mode.** This
1665
+ * matches a plain `source-over`, opaque-ink segment (the common case), but
1666
+ * a segment layer with `blend: 'multiply'`/`'screen'`/etc. or partial alpha
1667
+ * composites directionally differently on the real canvas than adding
1668
+ * luminance values ever can (`multiply` darkens; addition only brightens).
1669
+ * The per-cell grids don't retain per-layer blend/alpha to composite
1670
+ * correctly at this resolution.
1671
+ */
1672
+ declare function perceiveSequenceFrame(seq: IdleSequence, T: number, opts?: PerceiveSequenceOptions): SequenceFramePerception;
1673
+
1674
+ /**
1675
+ * The `field` background sampler — seeded, dependency-free value noise that
1676
+ * is a PURE function of `(u, v, t, config, seed)`.
1677
+ *
1678
+ * Why it is its own module, and why it never touches the entity RNG: a
1679
+ * background must be able to change without re-seating a single entity. The
1680
+ * sampler hashes an integer lattice with its own mixing function, so a spec
1681
+ * that adds (or steers) a field keeps the exact `buildEntities` stream it
1682
+ * had before — `field.test.ts` asserts it. Because the value at any point is
1683
+ * closed-form, the renderer (a low-res raster, upscaled) and the perception
1684
+ * grid (`luminanceGrid`) sample the SAME function and agree by construction;
1685
+ * that is the analytic guarantee FORMAT.md states for the field.
1686
+ *
1687
+ * Coordinates: `u`, `v` are in short-side units — `xPx / min(w, h)`,
1688
+ * `yPx / min(w, h)` — so `scale` reads as "features across the short side"
1689
+ * on every aspect ratio, and a 16:9 frame simply shows more of the same field
1690
+ * than a square one does. Callers on both sides normalise the same way.
1691
+ */
1692
+
1693
+ /** Cache bucket for a drifting field: the raster is recomputed at most this often. */
1694
+ declare const FIELD_BUCKET_MS = 100;
1695
+ /** Raster short side (px) the renderer paints the field at, then upscales. */
1696
+ declare const FIELD_RASTER_SHORT_SIDE = 96;
1697
+ /** Raster short side on the `basic` / `minimal` capability tiers. */
1698
+ declare const FIELD_RASTER_SHORT_SIDE_LOW = 48;
1699
+ /** Default `drift.amount`. */
1700
+ declare const FIELD_DRIFT_DEFAULT_AMOUNT = 0.3;
1701
+ /**
1702
+ * Integer-lattice hash → 0..1. Every product goes through `Math.imul` so the
1703
+ * arithmetic stays in int32 and the result is identical on every platform
1704
+ * (a float multiply of large ints would drift by ULPs across engines).
1705
+ */
1706
+ declare function latticeHash(ix: number, iy: number, seed: number): number;
1707
+ /** The clamped, rounded octave count a config resolves to (steering may glide it through fractions). */
1708
+ declare function fieldOctaves(cfg: Pick<FieldBackground, 'octaves'>): number;
1709
+ /**
1710
+ * The time the field is sampled at for scene time `t`: with `drift` at a
1711
+ * nonzero `amount`, the start of the 100 ms bucket `t` falls in (the renderer
1712
+ * recomputes its raster once per bucket and holds it, so the perception grid
1713
+ * samples the same instant); without `drift`, or with `drift.amount: 0` (the
1714
+ * domain never moves), the field is static and the answer is always 0 — so a
1715
+ * legally-static drift config isn't rebucketed, and re-rastered, ten times a
1716
+ * second for nothing.
1717
+ */
1718
+ declare function fieldSampleTime(cfg: Pick<FieldBackground, 'drift'>, t: number): number;
1719
+ /**
1720
+ * The scalar field at short-side coordinates `(u, v)` and time `t` (ms),
1721
+ * 0..1. `seed` is the spec's seed; `cfg.seed` overrides it. Pure — no state,
1722
+ * no RNG stream, so the same inputs give the same value in every call on
1723
+ * every platform.
1724
+ */
1725
+ declare function fieldAt(u: number, v: number, t: number, cfg: FieldBackground, seed: number): number;
1726
+ /** `#rgb` / `#rrggbb` → integer channels 0..255. Malformed input reads as mid grey. */
1727
+ declare function hexToRgb255(hex: string): [number, number, number];
1728
+ /** Perceptual luma (0..1) of integer RGB — the same weights `hexLuma` uses, so a band's luma matches its hex's. */
1729
+ declare function rgb255Luma(rgb: readonly [number, number, number]): number;
1730
+ /**
1731
+ * The colour a field value paints: `quantize ≥ 2` posterises the value into
1732
+ * that many levels and reads each level off the `bands` ramp (`quantize ===
1733
+ * bands.length`, the default, is exactly one band per level — hard, crisp
1734
+ * contours); `quantize: 0` interpolates the ramp smoothly (Aura, Mist). A
1735
+ * steered `quantize` gliding through fractions rounds to the nearest level
1736
+ * count. Integer channels, so a quantised band is byte-exact its hex.
1737
+ */
1738
+ declare function fieldRgb(value: number, bands: readonly string[], quantize: number | undefined): [number, number, number];
1739
+ /** The painted colour at short-side coordinates `(u, v)`: `fieldRgb(fieldAt(...))`. */
1740
+ declare function fieldRgbAt(u: number, v: number, t: number, cfg: FieldBackground, seed: number): [number, number, number];
1741
+
1742
+ /** Grain tile side (px). Repeats across the frame. */
1743
+ declare const GRAIN_TILE = 256;
1744
+ /** Ordered-dither tile side (px): an 8×8 Bayer matrix. */
1745
+ declare const DITHER_TILE = 8;
1746
+ /** Composite alpha at `grain: 1`. */
1747
+ declare const GRAIN_ALPHA = 0.35;
1748
+ /** Composite alpha at `dither: 1`. */
1749
+ declare const DITHER_ALPHA = 0.25;
1750
+ /** Offset step rate under `animate: true`. */
1751
+ declare const FINISH_ANIMATE_HZ = 12;
1752
+ /** The clamped strengths a spec's finish resolves to (0 when absent). */
1753
+ declare function finishStrength(f: FinishSpec | undefined): {
1754
+ grain: number;
1755
+ dither: number;
1756
+ };
1757
+ /**
1758
+ * RGBA pixels of the grain tile for `seed`: zero-mean grey noise (the sum of
1759
+ * two lattice hashes — a triangular distribution, softer than uniform —
1760
+ * centred on 128), alpha 255. Pure; the same seed gives the same tile on
1761
+ * every platform.
1762
+ */
1763
+ declare function grainTilePixels(seed: number): Uint8ClampedArray;
1764
+ /** The 8×8 Bayer ordered-dither matrix (values 0..63, recursively built). */
1765
+ declare function bayerMatrix(): number[][];
1766
+ /**
1767
+ * RGBA pixels of the dither tile: the Bayer matrix as grey levels
1768
+ * `(v + 0.5) / 64 × 255`, so the tile's mean is mid grey (zero-mean under a
1769
+ * symmetric blend). A stylistic screen — it is composited at low alpha, not
1770
+ * used to threshold the image per pixel.
1771
+ */
1772
+ declare function bayerTilePixels(): Uint8ClampedArray;
1773
+ /**
1774
+ * Where the grain tile sits for scene time `t`: fixed at the origin unless
1775
+ * `animate`, in which case it steps to a seeded offset (0..255 per axis)
1776
+ * once per 1/12 s bucket — a pure function of `(t, seed)`.
1777
+ */
1778
+ declare function grainOffset(t: number, seed: number, animate: boolean): {
1779
+ x: number;
1780
+ y: number;
1781
+ };
1782
+ type ScreenOp = 'overlay' | 'soft-light' | 'multiply';
1783
+ /**
1784
+ * The symmetric blend the finish composites with. `overlay` is what every
1785
+ * shipping canvas2d implements; the readback detect exists for the odd
1786
+ * context that rejects it (an assignment the context does not support leaves
1787
+ * the previous value in place).
1788
+ */
1789
+ declare function pickScreenOp(ctx: {
1790
+ globalCompositeOperation: string;
1791
+ }): ScreenOp;
1792
+ type Canvas2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
1793
+ type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
1794
+ /**
1795
+ * A presentation pass: the visible canvas and the tiles (as repeat
1796
+ * patterns, created lazily and cached — patterns are bound to the context
1797
+ * that made them). Owned by the instance that presents; the scene itself is
1798
+ * drawn on a separate canvas that this pass copies from.
1799
+ */
1800
+ interface FinishPass {
1801
+ canvas: AnyCanvas;
1802
+ ctx: Canvas2D;
1803
+ op: ScreenOp;
1804
+ grain: CanvasPattern | null;
1805
+ dither: CanvasPattern | null;
1806
+ /** Seed the grain tile was generated for. */
1807
+ seed: number;
1808
+ }
1809
+ /** Open a presentation pass on `visible`; null if it has no 2d context. */
1810
+ declare function createFinishPass(visible: AnyCanvas, seed: number): FinishPass | null;
1811
+ /** A fresh scene canvas for the instance to draw into (never attached to the DOM). */
1812
+ declare function createSceneCanvas(width: number, height: number): AnyCanvas;
1813
+ /**
1814
+ * Copy `scene` onto the pass's canvas and screen the finish over it:
1815
+ * grain at `grain × 0.35` (tile offset per `grainOffset`), then dither at
1816
+ * `dither × 0.25`, both with the pass's blend op and no image smoothing (a
1817
+ * crisp screen). With nothing to apply it is a plain copy. `w`/`h` are the
1818
+ * logical size the context transform maps to the canvas. `seed` is the
1819
+ * effective seed the *current* finish resolves to — a sequence with a
1820
+ * segment-level finish can present a different seed per frame (one segment
1821
+ * to the next); it defaults to the pass's own seed, so a single-scene
1822
+ * caller (whose seed never changes) can omit it. The cached grain pattern
1823
+ * is rebuilt whenever the seed changes.
1824
+ */
1825
+ declare function presentWithFinish(fp: FinishPass, scene: AnyCanvas, w: number, h: number, t: number, finish: FinishSpec | undefined, animateAllowed: boolean, seed?: number): void;
1292
1826
 
1293
1827
  interface ResolvedSegment {
1294
1828
  index: number;
@@ -1333,6 +1867,63 @@ declare function segmentStart(seq: IdleSequence, index: number): number;
1333
1867
  * mode (validated elsewhere).
1334
1868
  */
1335
1869
  declare function resolveSegment(seq: IdleSequence, T: number, opts?: ResolveOptions): ResolvedSegment;
1870
+ /** Whether the boundary from `from` to `from + 1` morphs: same scene structure, `morph` transition. */
1871
+ declare function canMorph(seq: IdleSequence, from: number): boolean;
1872
+ /**
1873
+ * Walk back through consecutive `morph` boundaries to the chain's origin. A
1874
+ * morph-chained segment's entities are never re-seeded at the boundary —
1875
+ * they're the same stream continued from wherever the chain started — so its
1876
+ * actual render seed (see `segmentRenderSeed`) is always the chain root's,
1877
+ * never its own index's.
1878
+ */
1879
+ declare function morphChainRoot(seq: IdleSequence, index: number): number;
1880
+ /** `SpecInstance`'s seed normalization: 0 is falsy, so a valid `seed: 0` lands on 1. */
1881
+ declare function normalizeSeed(seed: number): number;
1882
+ /**
1883
+ * The seed segment `index`'s entities actually render with — its chain
1884
+ * root's own seed, else `seq.seed + ` the chain root's index, normalized
1885
+ * exactly like `SpecInstance`. `undefined` when neither is set: the same
1886
+ * "unknowable ahead of time" case a bare `SaverSpec` has via `ctx.seed`,
1887
+ * which callers fall back to their own default for (as `perceiveScene` does).
1888
+ */
1889
+ declare function segmentRenderSeed(seq: IdleSequence, index: number): number | undefined;
1890
+ /**
1891
+ * The seed the bed's entities actually render with — its own seed, else
1892
+ * `seq.seed + LIMITS.maxSegments` (past every segment's `seq.seed + index`,
1893
+ * so a bed and segment 0 never share a stream), normalized like
1894
+ * `SpecInstance`. `undefined` when there's no bed or no derivable seed.
1895
+ */
1896
+ declare function bedRenderSeed(seq: IdleSequence): number | undefined;
1897
+ /**
1898
+ * Whether a republished sequence can be swapped into a live
1899
+ * `SequenceInstance` in place of `prev` without a remount
1900
+ * (`SequenceInstance.hotSwapSequence`). True only when every part of the
1901
+ * running instance that is fixed at mount — or that a running clock is
1902
+ * measured against — is unchanged:
1903
+ *
1904
+ * - the same number of segments;
1905
+ * - every segment's scene is a structural twin (`structuralSignature`) of
1906
+ * its counterpart, so the live child's entities survive a `hotSwapSpec`;
1907
+ * - the same render seed per segment (`segmentRenderSeed`) — a child's seed
1908
+ * is fixed at construction, so a changed seed could only take effect at
1909
+ * the next boundary, leaving the room half-swapped;
1910
+ * - a `bed` on both sides or neither, structural twins with the same seed
1911
+ * when present;
1912
+ * - the same `loop` and `sync` (absent `sync` is `'mount'`);
1913
+ * - the same `duration`, `advance` (absent is `'auto'`) and `transition`
1914
+ * (absent is `cut`; `text` on a morph defaults to `'step'`) on every
1915
+ * segment. Durations move segment boundaries under a clock that keeps
1916
+ * running, and `clockOffset`/`releasedBelow` on the instance are measured
1917
+ * in that timeline — so a timing edit is a remount for now.
1918
+ *
1919
+ * Paint stays free: colours, words, `background`, `ghosting`, ids and
1920
+ * labels, segment keys. Layer `alpha` is a per-entity range baked at build
1921
+ * time (like `size`), so it is already covered by the structural twin check
1922
+ * above and is not free — an alpha-only edit correctly falls back to a
1923
+ * remount. Mirrors idle-server's `sequenceSignaturesEqual` precondition and
1924
+ * adds the timing terms.
1925
+ */
1926
+ declare function sequenceSwapCompatible(prev: IdleSequence, next: IdleSequence): boolean;
1336
1927
 
1337
1928
  /**
1338
1929
  * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
@@ -1372,6 +1963,23 @@ declare const SNOWFALL_SPEC: SaverSpec;
1372
1963
  */
1373
1964
  declare const LANTERNS_SPEC: SaverSpec;
1374
1965
 
1966
+ /**
1967
+ * Lobby Talk — one screen of a "quarter in review" that runs on a lobby wall
1968
+ * between meetings: a five-row adoption chart over the same slow haze and
1969
+ * dust the whole talk shares. The words are the point, so every text layer
1970
+ * declares `role: 'read'` — the opt-in that turns on `adviseSpec`'s
1971
+ * legibility advisories (`text-legibility` against the ground *and* the
1972
+ * brightest additive layer that can sit under a line, `text-safe-area` for
1973
+ * the bezel zone). Tuned to zero advisories: the haze is bright enough to
1974
+ * matter and dim enough to leave every label above 4.5:1 at its peak.
1975
+ *
1976
+ * Schema exercises: `role: 'read'` on both `textBlock` and `text`, a
1977
+ * `list`-laid chart in three layers (labels, `bar`s, readouts), additive
1978
+ * atmosphere (`lighter` soft circles with `pulse`) under readable copy.
1979
+ * Steer `bars.sprite.values` to change the numbers live.
1980
+ */
1981
+ declare const LOBBY_TALK_SPEC: SaverSpec;
1982
+
1375
1983
  /**
1376
1984
  * Sakura Drift: cherry-blossom petals carried on a spring twilight breeze.
1377
1985
  * Glow-up pass (v1-ceiling features): petals ride `wander` harmonics with shared
@@ -1467,7 +2075,7 @@ declare const FACETS_SPEC: SaverSpec;
1467
2075
  * Relay Board — a six-row status chart in five layers instead of twenty-five.
1468
2076
  * One `list` of labels, one `list` of `bar`s whose lengths are the numbers,
1469
2077
  * one `list` of readouts, a title block, and a slow dust field behind so the
1470
- * board reads as a live wall, not a document. Steer `bars.values` and the
2078
+ * board reads as a live wall, not a document. Steer `bars.sprite.values` and the
1471
2079
  * bars glide to the new figures; the labels never move.
1472
2080
  */
1473
2081
  declare const RELAY_BOARD_SPEC: SaverSpec;
@@ -1495,6 +2103,17 @@ declare const PROCESSION_SPEC: SaverSpec;
1495
2103
  */
1496
2104
  declare const WARP_TUNNEL_SPEC: SaverSpec;
1497
2105
 
2106
+ /**
2107
+ * Thermal Field: the `field` background — seeded, domain-warped value noise
2108
+ * posterised into six riso inks (`quantize: 6`, one hard band per level) and
2109
+ * drifting once every forty seconds, so the contours crawl like a weather
2110
+ * chart. The foreground is deliberately almost nothing: a handful of pale
2111
+ * motes screened over the bands, so the ground IS the piece. A house
2112
+ * `thermal` look; the same field with `quantize: 0` is a smooth aura. A
2113
+ * `finish` of grain and dither screens the whole frame like a riso print.
2114
+ */
2115
+ declare const THERMAL_FIELD_SPEC: SaverSpec;
2116
+
1498
2117
  /** Catalog entry for a bundled schema example. */
1499
2118
  interface SchemaExample {
1500
2119
  id: string;
@@ -1508,4 +2127,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
1508
2127
  /** Lookup by saver id. */
1509
2128
  declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
1510
2129
 
1511
- export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, type CohesionRead, type CycleSpec, DASHBOARD_SPEC, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, FACETS_SPEC, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, type LayerCohesion, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionEase, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type Placed, RAIN_SPEC, RELAY_BOARD_SPEC, type ResolveOptions, type ResolvedSegment, type RevealState, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SequenceSegment, type SequenceTransition, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type TextBlockLine, type TextRevealInput, type TextRevealSpec, type TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, breakTextBlock, buildEntities, cohesionOf, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, emitEnvelope, emitWindow, graphemeClusters, headingAt, layerCohesion, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rotationAt, seamCauseOf, seamsWorthWarning, segmentStart, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, textWidthEm, validateSequence, validateSpec };
2130
+ export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, type CapabilityTier, type CohesionRead, type CycleSpec, DASHBOARD_SPEC, DITHER_ALPHA, DITHER_TILE, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, type FieldBackground, type FieldDrift, type FinishPass, type FinishSpec, GRAIN_ALPHA, GRAIN_TILE, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, type LayerCohesion, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionEase, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type PerceiveSequenceOptions, type Placed, RAIN_SPEC, RELAY_BOARD_SPEC, type ResolveOptions, type ResolvedSegment, type RevealState, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type ScreenOp, type SequenceFramePerception, type SequenceMountContext, type SequenceSaverInstance, type SequenceSaverPlugin, type SequenceSegment, type SequenceTransition, type SpecError, type SpecMountContext, type SpecWarning, type SpriteSpec, type SteerDelta, THERMAL_FIELD_SPEC, type TextBlockAnchor, type TextBlockLine, type TextMetricsClass, type TextRevealInput, type TextRevealSpec, type TextRole, type TextSpriteInfo, type ValidationResult, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, type WarningBox, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, canMorph, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, easeSmooth, emitEnvelope, emitWindow, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, latticeHash, layerCohesion, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, normalizeSeed, perceiveScene, perceiveSequenceFrame, pickScreenOp, positionAt, presentWithFinish, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rgb255Luma, rotationAt, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textBlockAnchorOffset, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, validateSequence, validateSpec };