@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/FORMAT.md +417 -42
- package/dist/index.d.ts +632 -13
- package/dist/index.js +1563 -110
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +192 -4
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:
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
839
|
-
|
|
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
|
-
|
|
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 };
|