@idle-screens/schema 3.14.0 → 3.15.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
@@ -194,11 +194,22 @@ interface TimelineKey {
194
194
  path: string;
195
195
  /** The target value; the key must still validate when applied. */
196
196
  value: unknown;
197
- /** Default `smooth`. */
198
- ease?: 'step' | 'linear' | 'smooth';
197
+ /** Default `smooth`. See `GlideEase`. */
198
+ ease?: GlideEase;
199
199
  /** Glide length in ms, starting at `t`. Default 1000 (as live steering); 0 = a cut. */
200
200
  dur?: number;
201
201
  }
202
+ /**
203
+ * How a glide moves between its two values. `smooth` (default) starts and
204
+ * ends at rest; `linear` is constant speed; `step` holds, then switches at
205
+ * the glide's end. `in` accelerates from rest and arrives at speed (a
206
+ * departure); `out` leaves at speed and decelerates into place (an
207
+ * arrival); `spring` is a critically damped spring — a fast start that
208
+ * settles with no overshoot, so every value stays between its two ends.
209
+ * Every ease is monotone, so a glide never leaves the range its endpoints
210
+ * bound. **Native:** an ease it does not know reads as `smooth`.
211
+ */
212
+ type GlideEase = 'step' | 'linear' | 'smooth' | 'in' | 'out' | 'spring';
202
213
  /**
203
214
  * Authored change on the scene's own clock — keyframes. A scene without a
204
215
  * timeline is unchanged (ambient, today's behaviour); with one, each key
@@ -373,7 +384,8 @@ interface LayerSpec {
373
384
  }
374
385
  /**
375
386
  * Data layouts — entities in READING ORDER, no scatter. `list` stacks them
376
- * in one column `gap` apart (viewport units of min(w,h), default 0.06);
387
+ * in one column `gap` apart (viewport units of min(w,h), default 0.06;
388
+ * `{y}` alone also works — a list has no columns, so `x` does nothing);
377
389
  * `table` fills `columns` row-major with `gap` `{x, y}` (one number = both).
378
390
  * Text and emoji sprites take `strings[i]` / `glyphs[i]` in order instead of
379
391
  * a seeded pick, and palette `colors[i]` likewise, so N labels or N bars are
@@ -382,7 +394,10 @@ interface LayerSpec {
382
394
  */
383
395
  | {
384
396
  type: 'list';
385
- gap?: number;
397
+ gap?: number | {
398
+ x?: number;
399
+ y?: number;
400
+ };
386
401
  } | {
387
402
  type: 'table';
388
403
  columns: number;
@@ -395,11 +410,33 @@ interface LayerSpec {
395
410
  * Layer lifecycle for act structure — a pure function of t, no state. Alpha is 0
396
411
  * before `enter` (ms), ramps up over `fade` ms (default 1000), holds at 1, then
397
412
  * ramps down starting at `exit`. Entities are skipped entirely while at alpha 0.
413
+ * `stagger` (ms, default 0) shifts entity i's whole envelope — its enter and
414
+ * its exit — by i × stagger, in the layer's entity order (reading order for
415
+ * `list` / `table` / `grid`), so a row of labels builds one after another
416
+ * instead of together. Paint, like the rest of `life`: it glides, draws
417
+ * nothing from the RNG, and on screens where density scaling changes the
418
+ * count the whole build takes (count − 1) × stagger. **Native:** ignored ⇒
419
+ * every entity enters together.
398
420
  */
399
421
  life?: {
400
422
  enter?: number;
401
423
  exit?: number;
402
424
  fade?: number;
425
+ stagger?: number;
426
+ };
427
+ /**
428
+ * A swing back and forth: each entity rotates `amp` degrees either side of
429
+ * its resting angle (`rotate` plus any `spin`), once per `period` ms, on a
430
+ * seeded per-entity phase — a boat on a swell, a hanging sign, a lantern,
431
+ * a jellyfish's lean. A `clock` replaces the seeded phase like `pulse`'s,
432
+ * so a clocked layer swings in step. Structural (baked per entity like
433
+ * `pulse`). Its phase is one seeded draw per entity, made last and only
434
+ * when declared, so existing specs keep identical streams. **Native:**
435
+ * ignored ⇒ no swing.
436
+ */
437
+ rock?: {
438
+ amp: number;
439
+ period: number;
403
440
  };
404
441
  /**
405
442
  * Sparse events — marks that appear, expand and fade in place, one at a time,
@@ -579,6 +616,8 @@ type SpriteSpec = {
579
616
  curve?: 'smooth' | 'linear';
580
617
  taper?: boolean;
581
618
  orient?: boolean;
619
+ /** Draw-on: the stretch of the mark that is painted — see `StrokeDraw`. Absent ⇒ the whole mark. */
620
+ draw?: StrokeDraw;
582
621
  colors?: string[];
583
622
  colorWeights?: number[];
584
623
  }
@@ -624,7 +663,28 @@ type SpriteSpec = {
624
663
  opacity?: number;
625
664
  /** See `TextRole`. Absent ⇒ no legibility advisories, no pixel change. */
626
665
  role?: TextRole;
666
+ /** A number the block shows instead of `text` — see `TextNumber`. */
667
+ number?: TextNumber;
627
668
  };
669
+ /**
670
+ * A counter: the block paints `prefix + value + suffix` instead of `text`,
671
+ * with `value` rounded to `decimals` places (default 0) and, unless
672
+ * `grouping: false`, its thousands separated by commas — formatted the same
673
+ * way on every device (never by locale). `value` is **paint**: steer
674
+ * `sprite.number.value` (or key it on the timeline) and the digits roll
675
+ * there over the glide. Line-breaking runs on the formatted string, so a
676
+ * counter that gains a digit can re-wrap — give it room (`maxWidth`) or an
677
+ * `anchor` that pins the edge you want still. `text` stays what clients
678
+ * that do not read `number` show — author it as the counter's rest value.
679
+ * **Native:** ignored ⇒ `text`.
680
+ */
681
+ interface TextNumber {
682
+ value: number;
683
+ decimals?: number;
684
+ prefix?: string;
685
+ suffix?: string;
686
+ grouping?: boolean;
687
+ }
628
688
  /**
629
689
  * What a text layer is *for* — a declaration, not a pixel. `read` says the
630
690
  * words must be readable from across the room and opts the layer into
@@ -679,6 +739,14 @@ interface TextRevealSpec {
679
739
  * wave of overlapping fades. Ignored by the other modes.
680
740
  */
681
741
  fade?: number;
742
+ /**
743
+ * `glyphFade` only: each glyph rises into place as it fades in, from
744
+ * `rise` em below its line (0..1, default 0) — the caption that lifts as
745
+ * it appears. A pure function of the glyph's fade, so it glides, deletes
746
+ * and seeks like the rest of reveal; layout never moves. **Native:**
747
+ * ignored ⇒ no rise.
748
+ */
749
+ rise?: number;
682
750
  }
683
751
  /** Rotate through sprite variants over time. Each entity offsets by its seeded phase. */
684
752
  interface CycleSpec {
@@ -686,11 +754,12 @@ interface CycleSpec {
686
754
  }
687
755
  type MotionSpec =
688
756
  /**
689
- * Drift at a per-entity speed picked from `speed` (px/sec) along `angle` degrees
757
+ * Drift at a per-entity speed picked from `speed` (min(w,h) per second; px/sec under `units: 'px'`) along `angle` degrees
690
758
  * (0 = right, 90 = down; default 0). `bidirectional` randomly flips horizontal
691
759
  * direction per entity (e.g. fish swimming both ways); `bob` adds a small vertical
692
- * wobble amplitude (px). Covers horizontal fields, diagonals (toasters) and rain
693
- * (angle 90).
760
+ * wobble amplitude, in the spec's dimensional unit like `speed` — a fraction of
761
+ * min(w,h) (0.004 ≈ 4 px at 1080p), px only under `units: 'px'`. Covers
762
+ * horizontal fields, diagonals (toasters) and rain (angle 90).
694
763
  */
695
764
  {
696
765
  type: 'drift';
@@ -700,14 +769,18 @@ type MotionSpec =
700
769
  bob?: number;
701
770
  ease?: MotionEase;
702
771
  }
703
- /** Rise upward (px/sec) with an optional horizontal sway amplitude (px) — bubbles. */
772
+ /**
773
+ * Rise upward with an optional horizontal `sway` amplitude — bubbles. Speed
774
+ * is min(w,h) per second and `sway` a fraction of min(w,h) (px/sec and px
775
+ * only under `units: 'px'`).
776
+ */
704
777
  | {
705
778
  type: 'rise';
706
779
  speed: [number, number];
707
780
  sway?: number;
708
781
  ease?: MotionEase;
709
782
  }
710
- /** Bounce diagonally at a per-entity speed, reflecting off the edges (px/sec). */
783
+ /** Bounce diagonally at a per-entity speed, reflecting off the edges (min(w,h) per second; px/sec under `units: 'px'`). */
711
784
  | {
712
785
  type: 'bounce';
713
786
  speed: [number, number];
@@ -878,11 +951,18 @@ declare const LIMITS: {
878
951
  readonly minClockedPeriod: 1000;
879
952
  readonly minClockRate: 0.1;
880
953
  readonly maxClockRate: 4;
954
+ readonly maxWobble: 0.25;
881
955
  readonly defaultListGap: 0.06;
882
956
  readonly defaultListGapPx: 40;
883
957
  readonly minPolygonSides: 3;
884
958
  readonly maxPolygonSides: 12;
885
959
  readonly maxShapePoints: 24;
960
+ readonly maxTextNumberDecimals: 6;
961
+ readonly maxTextAffix: 64;
962
+ readonly maxRevealRise: 1;
963
+ readonly maxRockAmp: 45;
964
+ readonly minRockPeriod: 1000;
965
+ readonly maxLifeStagger: 60000;
886
966
  readonly minEaseTau: 100;
887
967
  readonly maxEaseTau: 120000;
888
968
  };
@@ -915,7 +995,32 @@ type SequenceTransition = {
915
995
  | {
916
996
  type: 'fade';
917
997
  dur: number;
918
- };
998
+ }
999
+ /**
1000
+ * Wipe into the next segment over `dur` ms: the incoming segment is
1001
+ * revealed through a moving edge instead of a dissolve — `shape: 'circle'`
1002
+ * (default) is an iris opening from `origin` ({x, y} 0..1, default the
1003
+ * centre) until it clears the farthest corner; `'line'` sweeps a straight
1004
+ * edge across the frame along `angle` degrees (0 = left to right, 90 = top
1005
+ * to bottom). `feather` (0..1, default 0.08) softens the edge, as a
1006
+ * fraction of the distance it travels. Works between unlike segments,
1007
+ * like `fade`, on the same two-live-segment machinery and the same tier
1008
+ * gate (`basic` / `minimal` hosts cut). Every pixel changes once, so it is
1009
+ * a spatial ramp, never a flash. **Native:** cross-fades over `dur`.
1010
+ */
1011
+ | WipeTransition;
1012
+ /** See the `wipe` member of `SequenceTransition`. */
1013
+ interface WipeTransition {
1014
+ type: 'wipe';
1015
+ dur: number;
1016
+ shape?: 'circle' | 'line';
1017
+ origin?: {
1018
+ x: number;
1019
+ y: number;
1020
+ };
1021
+ angle?: number;
1022
+ feather?: number;
1023
+ }
919
1024
  interface SequenceSegment {
920
1025
  key: string;
921
1026
  scene: SaverSpec;
@@ -995,13 +1100,31 @@ interface PathGradientStop {
995
1100
  color: string;
996
1101
  opacity?: number;
997
1102
  }
998
- /** An outline drawn along every subpath of a path polygon (open ones too). `width` is in unit-box units. */
1103
+ /**
1104
+ * An outline drawn along every subpath of a path polygon (open ones too).
1105
+ * `width` is in unit-box units. `draw` paints only a stretch of it — see
1106
+ * `StrokeDraw`: every subpath of the part counts as one pen line, in the
1107
+ * order the path draws them.
1108
+ */
999
1109
  interface PathStroke {
1000
1110
  color: string;
1001
1111
  width: number;
1002
1112
  opacity?: number;
1003
1113
  join?: 'round' | 'miter' | 'bevel';
1004
1114
  cap?: 'round' | 'butt' | 'square';
1115
+ draw?: StrokeDraw;
1116
+ }
1117
+ /**
1118
+ * Draw-on for a line: only the stretch from `start` to `end` (fractions of
1119
+ * the line's arc length, 0..1, defaults 0 and 1) is painted, so gliding
1120
+ * `end` from 0 to 1 draws the line on as if by a pen, and gliding `start`
1121
+ * after it erases it from the tail. Paint: both glide under a steer, a
1122
+ * `timeline` key or a `morph` without a rebuild. `start ≥ end` paints
1123
+ * nothing. **Native:** ignored ⇒ the whole line.
1124
+ */
1125
+ interface StrokeDraw {
1126
+ start?: number;
1127
+ end?: number;
1005
1128
  }
1006
1129
  /**
1007
1130
  * One piece of a multi-colour path drawing (`polygon.parts`), drawn back to
@@ -1020,6 +1143,21 @@ interface PathPart {
1020
1143
  opacity?: number;
1021
1144
  }
1022
1145
 
1146
+ /**
1147
+ * Every motion type's fields besides `type` — the validator's allowlist, and
1148
+ * the source FORMAT.md's motion table is checked against (F74), so the table
1149
+ * cannot drift from what the validator accepts.
1150
+ */
1151
+ declare const MOTION_FIELDS: {
1152
+ readonly drift: readonly ["speed", "angle", "bidirectional", "bob", "ease"];
1153
+ readonly rise: readonly ["speed", "sway", "ease"];
1154
+ readonly bounce: readonly ["speed"];
1155
+ readonly static: readonly [];
1156
+ readonly orbit: readonly ["speed", "radius", "center"];
1157
+ readonly wander: readonly ["speed", "angle", "meander", "coherence", "ease"];
1158
+ readonly warp: readonly ["speed", "center"];
1159
+ readonly path: readonly ["points", "duration", "curve", "closed", "scatter"];
1160
+ };
1023
1161
  declare function validateSpec(spec: unknown): ValidationResult;
1024
1162
  /**
1025
1163
  * Dot-paths (numeric layer indices, as `steerablePaths` spells them) of every
@@ -1038,8 +1176,11 @@ declare function ignoredPropertyPaths(spec: unknown): string[];
1038
1176
  * `validateSpec`. For the timeline's composed-state checks, which run at every
1039
1177
  * key boundary and on every live steer: the whole-spec check costs a
1040
1178
  * full validation per sample, this one a layer or two.
1179
+ *
1180
+ * `keyed`: the spec is a timeline state (a key's value, or mid-glide), where a
1181
+ * path that collapses to nothing draws nothing instead of failing (X583).
1041
1182
  */
1042
- declare function validateSpecPaths(spec: SaverSpec, paths: Iterable<string>): boolean;
1183
+ declare function validateSpecPaths(spec: SaverSpec, paths: Iterable<string>, keyed?: boolean): boolean;
1043
1184
  /** Narrowing helper: validate + cast. Throws with a joined message when invalid. */
1044
1185
  declare function assertValidSpec(spec: unknown): SaverSpec;
1045
1186
  /**
@@ -1156,6 +1297,12 @@ interface Entity {
1156
1297
  };
1157
1298
  /** Static rotation (radians), added to any spin. Only set when `layer.rotate` is declared. */
1158
1299
  rotate?: number;
1300
+ /** `rock`: amplitude (radians), period (ms) and phase (radians; a clock's shared phase when clocked). */
1301
+ rock?: {
1302
+ amp: number;
1303
+ period: number;
1304
+ phase: number;
1305
+ };
1159
1306
  /** Spline params (pts in px, shared per layer). Only set for path motion. */
1160
1307
  path?: {
1161
1308
  pts: Array<{
@@ -1180,6 +1327,16 @@ declare function spriteVariants(sprite: SpriteSpec): number;
1180
1327
  * `scale` multiplies every dimensional draw (default 1). Used for viewport units.
1181
1328
  * `countScale` multiplies entity count for density-aware scaling (default 1). */
1182
1329
  declare function buildEntities(layer: LayerSpec, rng: Rng, w: number, h: number, scale?: number, countScale?: number): Entity[];
1330
+ /**
1331
+ * The string a `textBlock` paints: its `text`, or with `number` the
1332
+ * counter's `prefix + value + suffix` — rounded to `decimals`, thousands
1333
+ * grouped with commas unless `grouping: false`. Locale-free, so every
1334
+ * device draws the same digits.
1335
+ */
1336
+ declare function textBlockText(s: {
1337
+ text: string;
1338
+ number?: TextNumber;
1339
+ }): string;
1183
1340
  /**
1184
1341
  * Where entity `e` sits in its current emit window at time `t`: 0..1 across
1185
1342
  * the visible `life` ms, or null while dark (or when the layer has no `emit`).
@@ -1200,12 +1357,29 @@ declare function sizeAt(e: Entity, t: number): number;
1200
1357
  * `rotate` is for).
1201
1358
  */
1202
1359
  declare function rotationAt(e: Entity, t: number): number;
1203
- /** Layer lifecycle alpha multiplier at time `t` (ms). Pure. 1 when `life` is unset. */
1360
+ /**
1361
+ * Layer lifecycle alpha multiplier at time `t` (ms). Pure. 1 when `life` is
1362
+ * unset. With `stagger` this is the layer's GATE only — 0 before the first
1363
+ * entity enters, else 1 — and each entity's own envelope is
1364
+ * `lifeStaggerAt(life, t, i)`.
1365
+ */
1204
1366
  declare function lifeAlphaAt(life: {
1205
1367
  enter?: number;
1206
1368
  exit?: number;
1207
1369
  fade?: number;
1370
+ stagger?: number;
1208
1371
  } | undefined, t: number): number;
1372
+ /**
1373
+ * Entity i's share of a staggered `life`: its whole envelope shifted by
1374
+ * i × stagger. 1 without a stagger (the layer's `lifeAlphaAt` already
1375
+ * carries the envelope), so callers multiply it in unconditionally.
1376
+ */
1377
+ declare function lifeStaggerAt(life: {
1378
+ enter?: number;
1379
+ exit?: number;
1380
+ fade?: number;
1381
+ stagger?: number;
1382
+ } | undefined, t: number, i: number): number;
1209
1383
  /** Analytic position of an entity at logical time `t` (ms). Pure & deterministic. */
1210
1384
  declare function positionAt(e: Entity, t: number, w: number, h: number): Placed;
1211
1385
  /**
@@ -1266,11 +1440,23 @@ type TextMetricsClass = 'proportional' | 'mono';
1266
1440
  * family), else `proportional`. Absent font ⇒ proportional, as before.
1267
1441
  */
1268
1442
  declare function textMetricsClassFor(font?: string): TextMetricsClass;
1443
+ /** Combining (semi-)voiced sound marks: they ride on the preceding kana and advance nothing. */
1444
+ declare function isCombiningKana(cp: number): boolean;
1445
+ /**
1446
+ * Whether a code point is set full-width — a CJK ideograph, kana, Hangul
1447
+ * syllable, CJK punctuation or a fullwidth form — so it advances ~1 em, not
1448
+ * the 0.55 of a Latin letter (X614: a centred katakana line measured at half
1449
+ * its real width and spilled off the frame). Halfwidth kana and Hangul
1450
+ * (U+FF61–FFDC) stay narrow. A fixed table, for the same determinism reason
1451
+ * as the buckets below.
1452
+ */
1453
+ declare function isFullWidth(cp: number): boolean;
1269
1454
  /**
1270
1455
  * Em-width of a string under the same character-class table `breakTextBlock`
1271
1456
  * uses. `mono` counts grapheme clusters, not UTF-16 code units — an emoji or
1272
1457
  * combining/ZWJ sequence is one advance-width cell, matching what's painted,
1273
- * not one cell per surrogate half or combining mark.
1458
+ * not one cell per surrogate half or combining mark. Under either table a
1459
+ * full-width (CJK) glyph advances a full em.
1274
1460
  */
1275
1461
  declare function textWidthEm(str: string, metrics?: TextMetricsClass): number;
1276
1462
  interface TextBlockLine {
@@ -1356,6 +1542,14 @@ type CapabilityTier = 'minimal' | 'basic' | 'standard' | 'high';
1356
1542
  interface SpecMountContext extends SaverContext {
1357
1543
  capabilityTier?: CapabilityTier;
1358
1544
  }
1545
+ /**
1546
+ * A wipe's revealed region at progress `k` as a fill: opaque where the
1547
+ * incoming segment shows, clear where the outgoing still does, ramping over
1548
+ * the feathered edge — in the canvas's own pixels (`cw` × `ch`). The edge
1549
+ * starts just outside the frame and ends past its far side (feather
1550
+ * included), so k = 0 reveals nothing and k = 1 everything.
1551
+ */
1552
+ declare function wipeMask(g: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, w: WipeTransition, k: number, cw: number, ch: number): CanvasGradient;
1359
1553
  /**
1360
1554
  * Compile a declarative spec into a runnable SaverPlugin. Throws if the spec is invalid
1361
1555
  * (so an agent-authored spec is validated before it can run). The result is seeded,
@@ -1516,6 +1710,16 @@ declare function steerablePaths(spec: unknown): string[];
1516
1710
  declare function sequenceSteerablePaths(seq: IdleSequence): string[];
1517
1711
  /** Smooth (ease-in-out) progress curve used for glides. */
1518
1712
  declare function easeSmooth(k: number): number;
1713
+ /**
1714
+ * A glide's progress curve, by name (`GlideEase`), for progress `k` (0..1).
1715
+ * Every curve is monotone and runs 0 → 1, so an eased value never leaves the
1716
+ * range its two endpoints bound. `spring` is the critically damped step
1717
+ * response 1 − (1 + ωk)e^(−ωk), normalised to land exactly on 1 at k = 1.
1718
+ * Anything unknown is `smooth` (today's default).
1719
+ */
1720
+ declare function easeGlide(ease: string | undefined, k: number): number;
1721
+ /** The named eases a live steer honours (the ones that did not exist before steers carried an ease). */
1722
+ declare const STEER_EASES: ReadonlySet<string>;
1519
1723
  /**
1520
1724
  * Fields that require re-seeding entities when they change (placement/motion
1521
1725
  * are baked at build time). Colour/alpha/pulse-only changes redraw in place.
@@ -1746,6 +1950,15 @@ interface LuminanceGridOptions extends PerceiveOptions {
1746
1950
  * balance — and now smear.
1747
1951
  */
1748
1952
  declare function luminanceGrid(spec: SaverSpec, opts?: LuminanceGridOptions): LuminanceGrid;
1953
+ /**
1954
+ * The luminance-grid cells each layer can paint (same viewport, seed and
1955
+ * grid as `luminanceGrid` with these options), for the timeline flash gate's
1956
+ * shortcut: per layer its cell indices, or null when its footprint is not
1957
+ * fixed — it moves, emits, links, trails, orbits or cycles its text — and so may reach
1958
+ * any cell. Each entity is a disc about its position that holds its whole
1959
+ * outline at its largest size under any rotation: generous, never short.
1960
+ */
1961
+ declare function layerCellFootprints(spec: SaverSpec, opts?: LuminanceGridOptions): Array<number[] | null>;
1749
1962
  /**
1750
1963
  * Encode a luminance grid as a braille "image": each output character is a 2×4
1751
1964
  * pixel cell, ordered-dithered so mid-tones render as dot density. A grid of
@@ -2059,8 +2272,16 @@ interface TimelineFlashReport {
2059
2272
  flashingArea: number;
2060
2273
  /** Start (scene ms, within the lap) of the worst window checked, or null when none was checked. */
2061
2274
  at: number | null;
2062
- /** Set (with `fails`) when dense sections remained beyond the {@link MAX_TOTAL_SAMPLES} budget: unchecked is never reported as safe. */
2275
+ /**
2276
+ * Set (with `fails`) when dense sections remained beyond the
2277
+ * {@link MAX_TOTAL_SAMPLES} budget: unchecked is never reported as safe.
2278
+ * `flashesPerSecond` and `flashingArea` then describe only what was checked
2279
+ * (often nothing), so a message must say the timeline was too dense to
2280
+ * verify, not quote them as a strobe.
2281
+ */
2063
2282
  incomplete?: true;
2283
+ /** With `incomplete`: the paint-key boundaries in the busiest second of the window left unchecked. */
2284
+ boundariesPerSecond?: number;
2064
2285
  }
2065
2286
  declare function timelineFlashReport(spec: SaverSpec): TimelineFlashReport;
2066
2287
 
@@ -2363,7 +2584,7 @@ declare function sequenceSwapCompatible(prev: IdleSequence, next: IdleSequence):
2363
2584
  * exist on the spec (a key cannot create a field).
2364
2585
  * - A key glides from wherever its path is at `t` to `value` over `dur` ms
2365
2586
  * (default 1000, the live-steering default; 0 is a cut), eased `smooth`
2366
- * unless it says otherwise. Overlapping glides are fine: the later one
2587
+ * unless it names another `GlideEase`. Overlapping glides are fine: the later one
2367
2588
  * starts from the earlier one's in-flight value.
2368
2589
  * - Without `loop`, a path holds the base spec's value before its first key.
2369
2590
  * - With `loop`, time wraps every `duration` ms and a path's value before its
@@ -2416,6 +2637,42 @@ declare function timelineSampleTimes(spec: SaverSpec, dense?: boolean, cap?: num
2416
2637
  /** The absolute scene times at or after `now` where `timelineSampleTimes` fall — the next lap's occurrences under `loop`. */
2417
2638
  declare function timelineSampleTimesAfter(spec: SaverSpec, now: number, dense?: boolean, only?: (path: string) => boolean): number[];
2418
2639
 
2640
+ /**
2641
+ * The scene's own repeat period — how long until every frame comes round
2642
+ * again — read analytically from the spec, never by rendering.
2643
+ *
2644
+ * A scene is a pure function of time, so it repeats exactly when every
2645
+ * time-varying part does: each periodic part contributes its period, and the
2646
+ * scene's is their least common multiple (in whole ms). Anything that never
2647
+ * repeats — a seeded speed range, a drift that wraps at a per-entity speed,
2648
+ * a `life` build, a non-looping timeline, a self-typing reveal, animated
2649
+ * grain — makes the scene aperiodic, and the reason says which part. A scene
2650
+ * with nothing time-varying is static: every frame is the same (period 0).
2651
+ *
2652
+ * Who reads it: an export that wants an exact loop (#84), and the
2653
+ * `loop-seam` advisory, which fires only when a scene declares a looping
2654
+ * timeline whose lap the rest of the scene does not divide.
2655
+ */
2656
+
2657
+ /** Periods that share no cycle shorter than this are reported as aperiodic. */
2658
+ declare const MAX_SCENE_PERIOD = 3600000;
2659
+ interface ScenePeriod {
2660
+ /** ms until the scene repeats exactly; 0 for a static scene; null when it never does (within an hour). */
2661
+ period: number | null;
2662
+ /** One line per part that sets or breaks the period, e.g. `layers[2] pulse: 4000 ms`. */
2663
+ parts: string[];
2664
+ /** Why it is null (empty otherwise). */
2665
+ reasons: string[];
2666
+ }
2667
+ /** The repeat period of `spec` — see the module comment. */
2668
+ declare function scenePeriod(spec: SaverSpec): ScenePeriod;
2669
+ /**
2670
+ * The period of everything in the scene except its timeline — what a
2671
+ * looping timeline's lap has to be a multiple of for one lap to loop
2672
+ * seamlessly.
2673
+ */
2674
+ declare function periodWithoutTimeline(spec: SaverSpec): ScenePeriod;
2675
+
2419
2676
  /**
2420
2677
  * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
2421
2678
  * that the schema is actually expressive: two layers (fish drifting both ways at varied
@@ -2806,6 +3063,54 @@ declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
2806
3063
  type PolygonSprite = Extract<SpriteSpec, {
2807
3064
  kind: 'polygon';
2808
3065
  }>;
3066
+ type StrokeSprite = Extract<SpriteSpec, {
3067
+ kind: 'stroke';
3068
+ }>;
3069
+ /** A stroke sprite's sampled path as painted: `strokeSamples`, cut to its `draw` span when it has one. */
3070
+ declare function strokeDrawnSamples(s: StrokeSprite, halfSize: number): Array<{
3071
+ x: number;
3072
+ y: number;
3073
+ }>;
3074
+ /**
3075
+ * The [start, end] arc-length fractions a `draw` paints, clamped to 0..1 — or
3076
+ * null when it paints the whole line (absent, or 0..1), so the caller keeps
3077
+ * its untrimmed (cached) geometry.
3078
+ */
3079
+ declare function drawSpan(d: StrokeDraw | undefined): [number, number] | null;
3080
+ /** The stretch of a polyline between arc-length fractions `a` and `b` (a ≥ b ⇒ empty). Endpoints are interpolated. */
3081
+ declare function trimPolyline(pts: Array<{
3082
+ x: number;
3083
+ y: number;
3084
+ }>, a: number, b: number): Array<{
3085
+ x: number;
3086
+ y: number;
3087
+ }>;
3088
+ /**
3089
+ * A part's subpaths cut to a `draw` span: the lines are one pen run in draw
3090
+ * order (a closed ring counts its closing edge), so `end` sweeps through the
3091
+ * first subpath, then the next. A trimmed ring comes back open; untouched
3092
+ * lines are returned as they were. Memoised per (lines, span).
3093
+ */
3094
+ declare function trimLines(lines: Array<{
3095
+ points: Array<[number, number]>;
3096
+ closed: boolean;
3097
+ }>, a: number, b: number): Array<{
3098
+ points: Array<[number, number]>;
3099
+ closed: boolean;
3100
+ }>;
3101
+ /** A part's stroke lines as drawn: its subpaths, cut to its stroke's `draw` span when it has one. */
3102
+ declare function strokeLines(part: {
3103
+ flat: {
3104
+ lines: Array<{
3105
+ points: Array<[number, number]>;
3106
+ closed: boolean;
3107
+ }>;
3108
+ };
3109
+ stroke?: PathStroke;
3110
+ }): Array<{
3111
+ points: Array<[number, number]>;
3112
+ closed: boolean;
3113
+ }>;
2809
3114
  /** Most outline points one flattened path may produce. */
2810
3115
  declare const MAX_PATH_POINTS = 4096;
2811
3116
  interface FlattenedPath {
@@ -2876,5 +3181,5 @@ declare function pathFrame(s: PolygonSprite, r: number, w: number, h: number): {
2876
3181
  uniform: boolean;
2877
3182
  };
2878
3183
 
2879
- export { AQUARIUM_SPEC, AURORA_SPEC, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, DASHBOARD_SPEC, DEFAULT_KEY_DUR, DITHER_ALPHA, DITHER_TILE, EXAMPLE_BY_ID, EXAMPLE_SEQUENCES, EXAMPLE_SPECS, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, GRAIN_ALPHA, GRAIN_TILE, HAIKU_SPEC, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, MATRIX_RAIN_SPEC, MAX_PATH_POINTS, MURMUR_SPEC, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PHASE_DUET_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, RAIN_SPEC, RELAY_BOARD_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SHARD_FALL_SPEC, SIGNAL_BOARD_SPEC, SNOWFALL_SPEC, SPARSE_NIGHT_SPEC, THERMAL_FIELD_SPEC, THREE_MOVEMENTS_SEQUENCE, TYPED_CUE_SPEC, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, WEB_WORK_SPEC, adviseSequence, adviseSpec, alphaAt, applyDeltasShared, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, buriedAreaOf, canMorph, canWrapMorph, canonicalSpecPath, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, easeSmooth, emitEnvelope, emitWindow, entityScales, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, flattenPath, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, ignoredPropertyPaths, isPathPolygon, latticeHash, layerCohesion, lerpPathD, lerpSpec, lerpValue, lifeAlphaAt, linkEdges, linkMaxDistPx, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, nextKeyAfter, normalizeSeed, pathFrame, pathParts, perceiveScene, perceiveSequenceFrame, pickScreenOp, positionAt, presentWithFinish, readSpecPath, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, resolveTimelineAt, revealState, rgb255Luma, rotationAt, sceneColour, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sequenceWantsFinish, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textBlockAnchorOffset, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, timelineFlashReport, timelineLocalTime, timelineSampleTimes, timelineSampleTimesAfter, timelineTracks, validateSequence, validateSpec, validateSpecPaths, withoutTimeline };
2880
- export type { BackgroundDrift, BackgroundSpec, BandSpec, CapabilityTier, CohesionRead, CycleSpec, DominanceEntry, Entity, FieldBackground, FieldDrift, FinishPass, FinishSpec, FlattenedPath, GradientStop, IdleSequence, LayerCohesion, LayerGroup, LayerMotionStats, LayerSnapshot, LayerSpec, LayerTransform, LinkEdge, LuminanceGrid, LuminanceGridOptions, MotionEase, MotionSpec, PathGradient, PathGradientStop, PathPart, PathStroke, PerceiveOptions, PerceiveSequenceOptions, Placed, ResolveOptions, ResolvedPart, ResolvedSegment, RevealState, SaverSpec, SceneColour, SceneDescription, SceneDiff, ScenePerception, SceneSnapshot, SchemaExample, ScreenOp, SequenceExample, SequenceFramePerception, SequenceMountContext, SequenceSaverInstance, SequenceSaverPlugin, SequenceSegment, SequenceTransition, SpecError, SpecMountContext, SpecWarning, SpriteSpec, SteerDelta, TextBlockAnchor, TextBlockLine, TextMetricsClass, TextRevealInput, TextRevealSpec, TextRole, TextSpriteInfo, Timeline, TimelineFlashReport, TimelineKey, ValidationResult, WanderOsc, WarningBox };
3184
+ export { AQUARIUM_SPEC, AURORA_SPEC, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, DASHBOARD_SPEC, DEFAULT_KEY_DUR, DITHER_ALPHA, DITHER_TILE, EXAMPLE_BY_ID, EXAMPLE_SEQUENCES, EXAMPLE_SPECS, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, GRAIN_ALPHA, GRAIN_TILE, HAIKU_SPEC, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, MATRIX_RAIN_SPEC, MAX_PATH_POINTS, MAX_SCENE_PERIOD, MOTION_FIELDS, MURMUR_SPEC, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PHASE_DUET_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, RAIN_SPEC, RELAY_BOARD_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SHARD_FALL_SPEC, SIGNAL_BOARD_SPEC, SNOWFALL_SPEC, SPARSE_NIGHT_SPEC, STEER_EASES, THERMAL_FIELD_SPEC, THREE_MOVEMENTS_SEQUENCE, TYPED_CUE_SPEC, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, WEB_WORK_SPEC, adviseSequence, adviseSpec, alphaAt, applyDeltasShared, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, buriedAreaOf, canMorph, canWrapMorph, canonicalSpecPath, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, drawSpan, easeGlide, easeSmooth, emitEnvelope, emitWindow, entityScales, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, flattenPath, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, ignoredPropertyPaths, isCombiningKana, isFullWidth, isPathPolygon, latticeHash, layerCellFootprints, layerCohesion, lerpPathD, lerpSpec, lerpValue, lifeAlphaAt, lifeStaggerAt, linkEdges, linkMaxDistPx, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, nextKeyAfter, normalizeSeed, pathFrame, pathParts, perceiveScene, perceiveSequenceFrame, periodWithoutTimeline, pickScreenOp, positionAt, presentWithFinish, readSpecPath, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, resolveTimelineAt, revealState, rgb255Luma, rotationAt, sceneColour, scenePeriod, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sequenceWantsFinish, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, strokeDrawnSamples, strokeLines, structuralSignature, textBlockAnchorOffset, textBlockText, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, timelineFlashReport, timelineLocalTime, timelineSampleTimes, timelineSampleTimesAfter, timelineTracks, trimLines, trimPolyline, validateSequence, validateSpec, validateSpecPaths, wipeMask, withoutTimeline };
3185
+ export type { BackgroundDrift, BackgroundSpec, BandSpec, CapabilityTier, CohesionRead, CycleSpec, DominanceEntry, Entity, FieldBackground, FieldDrift, FinishPass, FinishSpec, FlattenedPath, GlideEase, GradientStop, IdleSequence, LayerCohesion, LayerGroup, LayerMotionStats, LayerSnapshot, LayerSpec, LayerTransform, LinkEdge, LuminanceGrid, LuminanceGridOptions, MotionEase, MotionSpec, PathGradient, PathGradientStop, PathPart, PathStroke, PerceiveOptions, PerceiveSequenceOptions, Placed, ResolveOptions, ResolvedPart, ResolvedSegment, RevealState, SaverSpec, SceneColour, SceneDescription, SceneDiff, ScenePerception, ScenePeriod, SceneSnapshot, SchemaExample, ScreenOp, SequenceExample, SequenceFramePerception, SequenceMountContext, SequenceSaverInstance, SequenceSaverPlugin, SequenceSegment, SequenceTransition, SpecError, SpecMountContext, SpecWarning, SpriteSpec, SteerDelta, StrokeDraw, TextBlockAnchor, TextBlockLine, TextMetricsClass, TextNumber, TextRevealInput, TextRevealSpec, TextRole, TextSpriteInfo, Timeline, TimelineFlashReport, TimelineKey, ValidationResult, WanderOsc, WarningBox, WipeTransition };