@idle-screens/schema 3.7.0 → 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, SaverContext, 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
@@ -644,6 +718,7 @@ declare const LIMITS: {
644
718
  readonly maxPulseAmp: 0.5;
645
719
  readonly minPulsePeriod: 500;
646
720
  readonly maxSpin: 360;
721
+ readonly maxRotate: 360;
647
722
  readonly maxGrowAmp: 0.8;
648
723
  readonly maxOrbitSpeed: 180;
649
724
  readonly maxLinksK: 8;
@@ -654,6 +729,13 @@ declare const LIMITS: {
654
729
  readonly maxTrailSamples: 24;
655
730
  readonly minDriftPeriod: 10000;
656
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;
657
739
  readonly maxGhosting: 0.95;
658
740
  readonly maxGhostReplayFrames: 120;
659
741
  readonly maxMeander: 500;
@@ -749,6 +831,14 @@ interface IdleSequence {
749
831
  * paints its own ground, exactly as before the field existed.
750
832
  */
751
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;
752
842
  segments: SequenceSegment[];
753
843
  }
754
844
 
@@ -848,6 +938,8 @@ interface Entity {
848
938
  type: 'settle' | 'buoyant';
849
939
  tau: number;
850
940
  };
941
+ /** Static rotation (radians), added to any spin. Only set when `layer.rotate` is declared. */
942
+ rotate?: number;
851
943
  /** Spline params (pts in px, shared per layer). Only set for path motion. */
852
944
  path?: {
853
945
  pts: Array<{
@@ -884,7 +976,13 @@ declare function emitEnvelope(u: number): number;
884
976
  declare function alphaAt(e: Entity, t: number): number;
885
977
  /** Analytic size multiplier at time `t` (ms). Pure, clamped to > 0. */
886
978
  declare function sizeAt(e: Entity, t: number): number;
887
- /** 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
+ */
888
986
  declare function rotationAt(e: Entity, t: number): number;
889
987
  /** Layer lifecycle alpha multiplier at time `t` (ms). Pure. 1 when `life` is unset. */
890
988
  declare function lifeAlphaAt(life: {
@@ -1016,6 +1114,22 @@ declare function revealState(lines: TextBlockLine[], reveal: TextRevealInput, tM
1016
1114
 
1017
1115
  /** Derive a manifest so a compiled spec composes with @idle-screens/capabilities. */
1018
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
+ }
1019
1133
  /**
1020
1134
  * Compile a declarative spec into a runnable SaverPlugin. Throws if the spec is invalid
1021
1135
  * (so an agent-authored spec is validated before it can run). The result is seeded,
@@ -1029,19 +1143,47 @@ declare function compileSaver(spec: unknown): SaverPlugin;
1029
1143
  * clock's elapsed ms (idlescreens.com: `Date.now() − scene.epoch`) so every
1030
1144
  * viewer of that sequence resolves the same segment.
1031
1145
  */
1032
- interface SequenceMountContext extends SaverContext {
1146
+ interface SequenceMountContext extends SpecMountContext {
1033
1147
  /** ms already elapsed on the shared sequence clock. Ignored unless `sync: 'epoch'`. */
1034
1148
  sequenceBaseT?: number;
1035
1149
  /**
1036
- * The device's compute tier (`computeTier` from `@idle-screens/capabilities`;
1037
- * the string union is repeated here so schema does not depend on that
1038
- * package). A `fade` transition keeps two segments live for `dur`, which
1039
- * `basic` (canvas2d only) and `minimal` cannot afford: on those tiers fade
1040
- * renders as `cut`. Absent ⇒ fade enabled.
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.
1041
1176
  */
1042
- capabilityTier?: 'minimal' | 'basic' | 'standard' | 'high';
1177
+ hotSwapSequence(next: IdleSequence): boolean;
1043
1178
  }
1044
- declare function compileSequence(spec: unknown): SaverPlugin;
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;
1045
1187
 
1046
1188
  /**
1047
1189
  * Pure helpers for LIVE steering of compiled specs (setParam / applyTrack).
@@ -1089,7 +1231,7 @@ declare function morphNothingMorphable(a: SaverSpec, b: SaverSpec, opts?: {
1089
1231
  }): boolean;
1090
1232
  /**
1091
1233
  * Enumerate all steerable leaf paths in a (resolved) spec. Returns dot-paths
1092
- * 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
1093
1235
  * (id, label, schemaVersion, seed, units, kind, type, key) are excluded —
1094
1236
  * they describe structure, not tuneable values.
1095
1237
  *
@@ -1126,10 +1268,18 @@ declare function adviseSpec(spec: SaverSpec, viewport?: {
1126
1268
  * describe the same scene its other channels do — without them a caller
1127
1269
  * perceiving at t = 30 s would get `form` for that instant beside an
1128
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.
1129
1278
  */
1130
1279
  opts?: {
1131
1280
  t?: number;
1132
1281
  seed?: number;
1282
+ backgroundSeed?: number;
1133
1283
  }): SpecWarning[];
1134
1284
  /**
1135
1285
  * Non-blocking advisory warnings for a validated sequence envelope.
@@ -1240,10 +1390,12 @@ interface SceneSnapshot {
1240
1390
  warnings: string[];
1241
1391
  }
1242
1392
  interface SceneDescription {
1393
+ /** `finish` is present only when the spec declares one — a print screen the luminance numbers do not include. */
1243
1394
  spec: {
1244
1395
  id: string;
1245
1396
  label: string;
1246
1397
  units: string;
1398
+ finish?: FinishSpec;
1247
1399
  };
1248
1400
  snapshots: SceneSnapshot[];
1249
1401
  }
@@ -1275,8 +1427,15 @@ interface LuminanceGrid {
1275
1427
  rows: number;
1276
1428
  /** Row-major luminance 0..1 including the background. */
1277
1429
  cells: number[];
1278
- /** 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. */
1279
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[];
1280
1439
  meanLuminance: number;
1281
1440
  /** Fraction of cells deviating perceptibly (> 0.03) from the background. */
1282
1441
  coverage: number;
@@ -1512,6 +1671,159 @@ interface PerceiveSequenceOptions extends Omit<LuminanceGridOptions, 't'> {
1512
1671
  */
1513
1672
  declare function perceiveSequenceFrame(seq: IdleSequence, T: number, opts?: PerceiveSequenceOptions): SequenceFramePerception;
1514
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;
1826
+
1515
1827
  interface ResolvedSegment {
1516
1828
  index: number;
1517
1829
  localT: number;
@@ -1582,6 +1894,36 @@ declare function segmentRenderSeed(seq: IdleSequence, index: number): number | u
1582
1894
  * `SpecInstance`. `undefined` when there's no bed or no derivable seed.
1583
1895
  */
1584
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;
1585
1927
 
1586
1928
  /**
1587
1929
  * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
@@ -1761,6 +2103,17 @@ declare const PROCESSION_SPEC: SaverSpec;
1761
2103
  */
1762
2104
  declare const WARP_TUNNEL_SPEC: SaverSpec;
1763
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
+
1764
2117
  /** Catalog entry for a bundled schema example. */
1765
2118
  interface SchemaExample {
1766
2119
  id: string;
@@ -1774,4 +2127,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
1774
2127
  /** Lookup by saver id. */
1775
2128
  declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
1776
2129
 
1777
- 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, 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 SequenceFramePerception, type SequenceMountContext, type SequenceSegment, type SequenceTransition, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, 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, bedRenderSeed, breakTextBlock, buildEntities, canMorph, cohesionOf, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, emitEnvelope, emitWindow, graphemeClusters, headingAt, layerCohesion, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, normalizeSeed, perceiveScene, perceiveSequenceFrame, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rotationAt, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textBlockAnchorOffset, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, 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 };