@idle-screens/schema 3.13.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/FORMAT.md CHANGED
@@ -48,7 +48,11 @@ scene's own clock), paint-level layer `opacity` and `transform`,
48
48
  `text: 'dip'` on `morph` (2026-09-25 — timed scenes); layer `groups` and
49
49
  `transform.origin` (2026-09-26 — groups and origin); path drawings on
50
50
  `polygon` — `path`, `fillRule`, `fill`, `gradient`, `stroke`, `parts`,
51
- `span`, `origin` (2026-10-17 — vector drawings).
51
+ `span`, `origin` (2026-10-17 — vector drawings); `draw` on `stroke` sprites
52
+ and path strokes, the `in` / `out` / `spring` glide eases (timeline keys and
53
+ live steers), `rock`, `life.stagger`, `reveal.rise`, `textBlock.number`, the
54
+ `wipe` transition, `scenePeriod` and the `loop-seam` advisory
55
+ (2026-10-25 — motion vocabulary).
52
56
 
53
57
  ## Safety invariants
54
58
 
@@ -90,6 +94,12 @@ Two validators exist with deliberately different strictness:
90
94
 
91
95
  Author against the JSON Schema; ship through the runtime validator.
92
96
 
97
+ An unknown field that is another motion tool's vocabulary gets a pointer in
98
+ its `unknown-property` warning: `delay` on a layer names `life.enter`,
99
+ `duration` on a timeline key names `dur`, `fps` or `bpm` at the top level
100
+ says a scene is a pure function of time in ms. The code and path are the
101
+ same as any unknown field's; only the message says what to write instead.
102
+
93
103
  Rules the JSON Schema cannot fully express (the runtime enforces them):
94
104
  total entities across all layers ≤ 800; every `[min, max]` range must satisfy
95
105
  `min ≤ max`; `colorWeights` length must match `colors`; `emit.life ≤
@@ -287,16 +297,17 @@ own `finish` is not steerable; steer a segment's, or republish.
287
297
  | `spin` | number \| `[min,max]` ±360 deg/sec | none | per-entity rotation (seeded start angle — drawn from the scene's one shared seeded stream, so it is reproducible for a given spec, seed **and viewport** but not a fixed per-entity value: density scaling changes how many entities draw before it, and declaring a `rotate` range adds a draw per entity, so either one changes the angles; use `rotate` for an angle you need to pin); a range gives each entity a seeded speed (confetti, tumbling debris). `[0, 0]` does **not** hold the seeded angle — a zero speed renders at 0; use `rotate` |
288
298
  | `rotate` | number \| `[min,max]` ±360 deg | none | **static** per-entity rotation: a scalar turns the whole layer, a range gives each entity a seeded angle (thrown blades, scattered glyphs, a tilted grid). Adds to `spin`'s start angle. Structural. **Native:** tvOS reads `spin` only ⇒ 0 |
289
299
  | `grow` | `{amp ≤ 0.8, period ≥ 500}` | none | size breathing (seeded phase) |
300
+ | `rock` | `{amp ≤ 45, period ≥ 1000}` (degrees, ms) | none | a **swing** about each entity's resting angle (`rotate` plus any `spin`): `amp` degrees either side, once per `period` there and back, on a seeded per-entity phase — a boat on a swell, a hanging sign, a lantern. A `clock` replaces the seeded phase (the layer swings in step) and then needs `period / rate ≥ 1000`. Structural, like `pulse`; its phases are drawn in a pass after the layer's entities, only when declared, so declaring it re-seats nothing else in the layer. Rotation is not in the luminance grid, so perception does not see it. **Native:** ignored ⇒ no swing |
290
301
  | `trail` | `{length ≤ 5000, fade?}` | none | analytic afterglow trail; `length` is **milliseconds** of history (max 5000), `fade` a number 0..1 (not a boolean — `links.falloff` in the next row is the boolean). Echoes of a shaped sprite (`polygon`, `stroke`, `rect`, `ring`, `streak`, `bar`) are the sprite's own shape as it was then, shrinking toward the tail; `circle`, `emoji` and text trail discs. A hard `circle`'s trail is one continuous ribbon tapering behind it, so a quick path draws a line rather than a string of dots (a layer whose ribbons would cover more than a tenth of the screen draws its echo discs instead, to bound the cost); a `soft` circle's echoes are filled in where they would part, within a small per-layer budget |
291
302
  | `links` | see below | none | inter-entity lines |
292
- | `layout` | `{type: "grid", columns?, jitter?}` \| `{type: "list", gap?}` \| `{type: "table", columns, gap?}` | scatter | `grid`: cells fill `region`, `jitter` scalar or `{x?, y?}` 0..1 per axis. **`list` / `table`: data layouts** — entities in reading order from `position` (any count) or centred in `region`, `gap` apart (viewport units of `min(w,h)`, default 0.06; a number or `{x?, y?}`). **`gap` is the pitch between cell anchors, not the space between cells** — a label wider than the column pitch runs into its neighbour (the `layout-text-overlap` advisory names the gap that clears it), so size `gap.x` to the widest label. Text/emoji take `strings[i]` / `glyphs[i]` **in order**, so N labels are one layer; a shaped sprite's palette (`colors[i]` on `bar`, `rect`, `circle`…) is read in order too. **Text and emoji have no `colors`**: a `text` layer paints every label in its one `color` (a `colors` on it is an unknown property, ignored with a warning) — use one layer per label colour |
293
- | `life` | `{enter?, exit?, fade?}` ms | always on | act structure: fade the layer in at `enter`, out at `exit` |
303
+ | `layout` | `{type: "grid", columns?, jitter?}` \| `{type: "list", gap?}` \| `{type: "table", columns, gap?}` (`gap`: a number or `{x?, y?}`) | scatter | `grid`: cells fill `region`, `jitter` scalar or `{x?, y?}` 0..1 per axis. **`list` / `table`: data layouts** — entities in reading order from `position` (any count) or centred in `region`, `gap` apart (viewport units of `min(w,h)`, default 0.06; a number or `{x?, y?}` — a list is one column, so only its `y`, the row pitch, applies). **`gap` is the pitch between cell anchors, not the space between cells** — a label wider than the column pitch runs into its neighbour (the `layout-text-overlap` advisory names the gap that clears it), so size `gap.x` to the widest label. Text/emoji take `strings[i]` / `glyphs[i]` **in order**, so N labels are one layer; a shaped sprite's palette (`colors[i]` on `bar`, `rect`, `circle`…) is read in order too. **Text and emoji have no `colors`**: a `text` layer paints every label in its one `color` (a `colors` on it is an unknown property, ignored with a warning) — use one layer per label colour |
304
+ | `life` | `{enter?, exit?, fade?, stagger?}` ms | always on | act structure: fade the layer in at `enter`, out at `exit`. `stagger` shifts entity *i*'s whole envelope (enter and exit) by *i* × `stagger` in entity order (reading order for `list` / `table` / `grid`), so a row of labels builds one after another; paint, no RNG draws. Density scaling changes the count, so the build takes (count − 1) × `stagger`. **Native:** ignored ⇒ all together |
294
305
  | `emit` | `{every ≥ 1000, life ≥ 500, jitter?, grow?}` (`life ≤ every`) | always lit | **sparse events**: each entity is dark except a `life`-ms window every `every` ms, fading in fast and out slow; `jitter` 0 staggers entities evenly (one event at a time while `life ≤ every / count`), 1 scatters the offsets (a fixed sequence, not seeded — declaring `emit` disturbs no other draw); `grow: [from, to]` scales size across the window — expansion rather than travel |
295
306
  | `clock` | `{phase?, rate?}` | seeded phases | **phase-lock**: `pulse`, `grow` and `cycle` share one phase (turns, 0..1) and run at `rate` × time; two layers with the same clock breathe in step. Clocked periods must satisfy `period / rate ≥ 1000` |
296
307
  | `key` | string | none | addressable name → `setParam("key.count", …)` for a layer field, `setParam("key.sprite.color", …)` for a sprite field (the path mirrors the JSON: sprite fields sit under `sprite`) |
297
308
  | `position` | `{x, y, dx?, dy?}` — `x`/`y` 0..1, `dx`/`dy` ±2 (±17280 px under `units: "px"`) | none | exact placement; **requires `count: 1`** — except with a `list`/`table` layout, where it anchors the block's top-left; overrides `region`. `dx`/`dy` offset the point in **`min(w, h)` units** (px under `units: "px"`): `x`/`y` are fractions of width and height while every size is a fraction of `min(w, h)`, so parts placed at computed fractions only register at the aspect they were computed for — anchor a compound form's parts on one shared fraction (the centre is the only aspect-safe one) and offset each with `dx`/`dy`. Placement (structural). **Native:** tvOS ignores the offset |
298
309
  | `opacity` | number 0..1 | `1` | **paint-level** layer opacity: multiplies every entity's `alpha` and the `life` envelope at draw time. Unlike `alpha` (a range baked into each entity), it is outside the structural signature, so `setParam`, a `timeline` key or a `morph` **glides** it instead of re-seeding the layer. `0` skips the layer. **Native:** ignored ⇒ 1 |
299
- | `transform` | `{x?, y?, scale?, scaleX?, rotate?, origin?}` | identity | **paint-level** transform of the whole layer, about the viewport centre — or, with `origin: "anchor"`, about the layer's own anchor (its `position` point with `dx`/`dy`; a `list`/`table` block's anchor; without `position`, the centre of its `region`), so a word scales **in place** on every aspect (`origin` is an enum: not a steer or key target, and it steps across a morph — keep one origin per layer): translate `x`/`y` (±2 in `min(w, h)` units; px under `units: "px"`, ±17280), `rotate` (degrees, ±3600), `scale` (0..8) and `scaleX` (±8, × `scale` horizontally — toward 0 reads as a turn about the vertical axis, negative mirrors). Outside the structural signature: a camera for a timed piece, a slow drift for an ambient one, and it glides under steering. Sprites are not re-rasterised, so a large `scale` enlarges pixels of soft sprites, not detail. **Native:** ignored ⇒ identity |
310
+ | `transform` | `{x?, y?, scale?, scaleX?, rotate?, origin?}` | identity | **paint-level** transform of the whole layer, about the viewport centre — or, with `origin: "anchor"`, about the layer's own anchor (its `position` point with `dx`/`dy`; a `list`/`table` block's anchor; without `position`, the centre of its `region`), so a word scales **in place** on every aspect (`origin` is an enum: not a steer or key target, and it steps across a morph — keep one origin per layer): translate `x`/`y` (±2 in `min(w, h)` units; px under `units: "px"`, ±17280), `rotate` (degrees, ±3600), `scale` (0..8) and `scaleX` (±8, × `scale` horizontally — toward 0 reads as a turn about the vertical axis, negative mirrors). **Order:** `scale`/`scaleX` (along the layer's own horizontal) and `rotate` apply about the origin first, then `x`/`y` translate in **screen space** — an offset is never turned or stretched by the layer's own `rotate`/`scale`, so `{ "rotate": 90, "x": 0.2 }` moves the layer right, not down. A group's transform wraps the result (*group(layer(p))*), so a group's `scale` and `rotate` do act on its members' offsets. Outside the structural signature: a camera for a timed piece, a slow drift for an ambient one, and it glides under steering. Sprites are not re-rasterised, so a large `scale` enlarges pixels of soft sprites, not detail. **Native:** ignored ⇒ identity |
300
311
  | `group` | string | none | the name of a `groups` entry this layer paints through — see **Groups**. **Native:** ignored |
301
312
 
302
313
  `links`: `{ k: 1..8, maxDist, color?, alpha?, width?, mode?, falloff?, closed? }`.
@@ -364,7 +375,9 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
364
375
  `parts`).
365
376
  - **Holes and pieces.** Several subpaths in one `path` are one drawing.
366
377
  `fillRule: "auto"` (default) decides by containment: a subpath lying
367
- entirely inside another is a hole; one inside a hole is solid again (a
378
+ entirely inside another is a hole — touching its container's edge counts
379
+ as inside, so a notch cut from the rim or a mouth meeting the face's
380
+ outline is a hole too (a vertex within 0.001 of the edge is on it); one inside a hole is solid again (a
368
381
  pupil in an eye-spot ring); pieces that merely overlap merge (branches,
369
382
  veins, leaflets on a stem), and a hole cuts through every overlapping
370
383
  piece of the layer (a window across two walls). `"nonzero"` /
@@ -383,6 +396,12 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
383
396
  — an outline along every subpath **as drawn** (holes included; open
384
397
  subpaths too). `width` is in unit-box units (0.01 = 1% of the drawing's
385
398
  half-size). Overlapping pieces show their inner edges, as in SVG.
399
+ `draw: {start?, end?}` on a stroke paints only that stretch of it, the
400
+ part's subpaths counted as one pen run in the order the path draws them
401
+ (a closed ring includes its closing edge, and comes back open when cut) —
402
+ `fill: false` line art plus a glide of `stroke.draw.end` is a drawing
403
+ that draws itself. Paint; `…stroke.draw.end` is a key target like the
404
+ stroke's width. **Native:** ignored ⇒ the whole outline.
386
405
  - **`fill: false`** draws the stroke only — line art. Then open paths
387
406
  (no `Z`) are fine: veins, hatching, antennae, rigging.
388
407
  - **`parts`** — a multi-colour drawing in one layer:
@@ -411,18 +430,26 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
411
430
  - **Steering and morphs.** Part colours, gradient stops, stroke widths
412
431
  and opacities glide like any paint. Two paths with the **same commands**
413
432
  (same letters, same count of numbers — draw both from one template)
414
- morph number by number; any other path change steps.
433
+ morph number by number; any other path change steps. The full list of
434
+ what a `setParam` or `timeline` key can address is under **Timeline**
435
+ ("Path drawings: what a key can address").
415
436
  - Drawings are rendered once and reused while they look the same (moving
416
437
  copies are resampled), so a detailed still drawing costs about what a
417
438
  flat one does per frame.
418
439
  - `{ "kind": "stroke", "length": [0.08, 0.18], "points": [[-1, 0.3], [-0.5, -0.6], [0.3, -0.7], [1, 0.1]], "width": 0.004, "taper": true, "color": "#f2e8c9" }` —
419
440
  a freehand mark: a path through 2..24 unit-coordinate points, scaled so the
420
441
  unit box spans the seeded `length`, stroked `width` wide with round joins.
421
- `curve: "smooth"` (default) is a Catmull-Rom spline, `"linear"` a polyline;
442
+ `curve: "smooth"` (default) is a Catmull-Rom spline, `"linear"` a polyline
443
+ through every point (its corners stay sharp);
422
444
  `taper` thins the mark to almost nothing at both ends (a brush stroke rather
423
445
  than a line; it paints as one filled mark, so a translucent stroke has no
424
446
  overlap beads); `orient: true` turns the path's +x along the entity's heading,
425
- like `streak`. Rotates with `spin`. Van Gogh's stroke, Hokusai's contour,
447
+ like `streak`. Rotates with `spin`. **`draw: {start?, end?}`** paints only
448
+ the stretch of the mark between those fractions of its length (defaults 0
449
+ and 1) — glide `end` 0 → 1 and the mark draws itself on; glide `start` after
450
+ it and it erases from the tail. Paint (it glides, it is outside the
451
+ structural signature), and perception scales the ink with it.
452
+ **Native:** ignored ⇒ the whole mark. Van Gogh's stroke, Hokusai's contour,
426
453
  O'Keeffe's petal, Basquiat's scrawl
427
454
  - `{ "kind": "textBlock", "text": "Multi-line text with wrapping.", "maxWidth": 0.8, "fontSize": 0.04, "lineHeight": 1.4, "align": "left", "color": "#e6e8ef", "anchor": "top-left", "font": "system-ui, sans-serif", "opacity": 1 }` —
428
455
  multi-line text block with deterministic line-breaking. All dimensions are
@@ -437,6 +464,9 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
437
464
  wrapping is identical across platforms; note that the table is approximate
438
465
  — a painted line may slightly exceed `maxWidth` when the real font is wider
439
466
  than the table estimates, so `maxWidth` is a layout target, not a hard clip.
467
+ Full-width glyphs (CJK ideographs, kana, Hangul, fullwidth forms) count a
468
+ whole em in every table; lines break only at whitespace, so an unspaced CJK
469
+ run stays on one line — break it with spaces or `\n`.
440
470
  Use with `count: 1`, `motion: { type: "static" }`, and `position`.
441
471
 
442
472
  `anchor` (one of nine compass points: `top-left`, `top`, `top-right`,
@@ -461,7 +491,7 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
461
491
  rejected** (`fontSize` owns size, so the block keeps scaling with the
462
492
  viewport). A monospace family (`monospace`, `ui-monospace`, Menlo, Courier,
463
493
  SF Mono, Fira Code, …) switches the line-breaker to a uniform 0.6 em
464
- advance so mono wraps land where the real face puts them; every other
494
+ advance (1 em for a full-width glyph) so mono wraps land where the real face puts them; every other
465
495
  family uses the proportional table. Default `system-ui, sans-serif`.
466
496
  **Native clients fall back to the system face** until a face table exists.
467
497
  `font` names faces **installed on the viewing device** — nothing is
@@ -490,7 +520,9 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
490
520
  across the room, and opts that layer into two advisories:
491
521
  `text-legibility` fires when the text colour's WCAG ratio falls below
492
522
  **4.5:1** against the background sampled at the box centre (gradient stops
493
- interpolated at rest, `band` honoured) **or** against the brightest
523
+ interpolated at rest, `band` honoured) — or, where an earlier static,
524
+ hard-edged, unblended layer covers the whole box (a backing chip), against
525
+ that layer composited over the page — **or** against the brightest
494
526
  additive layer (`blend: lighter` / `screen`) whose entities can reach the
495
527
  box and are at least a glyph tall, taken at its peak alpha (base + pulse)
496
528
  composited over that background — a glow parked under a caption is
@@ -520,7 +552,11 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
520
552
  progress — small reads as a soft typewriter, large as a wave of
521
553
  overlapping fades. Still a pure function of `progress`, so it glides and
522
554
  deletes exactly like the other modes. Native clients render `glyphFade`
523
- as `typewriter` until they gain per-glyph alpha. `caret` draws a
555
+ as `typewriter` until they gain per-glyph alpha. With `glyphFade`, `rise`
556
+ (0..1 em, default 0) lifts each glyph into its line as it fades in, from
557
+ `rise` em below — still a pure function of progress, and layout never
558
+ moves (it warns and is ignored on the other modes; native ignores it).
559
+ `caret` draws a
524
560
  blinking caret at the frontier (`true`, or `{ "blink": 1.2, "color": "#fff" }`;
525
561
  blink is in full cycles/sec, capped at 3 for flash safety, and is a square
526
562
  wave of `t` like every other animation). With `align: "center"`/`"right"`
@@ -533,6 +569,20 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
533
569
  sequence, a morph declared `text: 'crossfade'` cross-fades the old words
534
570
  under the new ones instead (see **Transitions** below).
535
571
 
572
+ textBlock also accepts **`number`** — a counter:
573
+ `{ "number": { "value": 1200, "decimals": 0, "prefix": "", "suffix": " visitors", "grouping": true } }`.
574
+ The block paints `prefix + value + suffix` instead of `text`, `value`
575
+ rounded to `decimals` (0..6, default 0) with its thousands grouped by
576
+ commas unless `grouping: false` — formatted by the engine, never by the
577
+ viewer's locale, so every screen shows the same digits. `value` is
578
+ **paint**: `setParam("stats.sprite.number.value", 48210, { dur: 4000 })`
579
+ rolls the digits up over the glide, and a timeline key does the same.
580
+ Line-breaking runs on the formatted string, so a counter that gains a
581
+ digit can re-wrap: give it width to spare and an `anchor` on the edge that
582
+ should stay put. Perception and the text advisories read the formatted
583
+ number. `text` is what clients that do not read `number` show — author it
584
+ as the counter's rest value. **Native:** ignored ⇒ `text`.
585
+
536
586
  `circle`, `ring`, `streak`, `rect`, `bar`, `polygon` and `stroke` all accept
537
587
  `colors: [...]` (seeded per-entity palette pick) and `colorWeights: [...]`
538
588
  (relative weights, same length — "mostly cool tones, occasional ember").
@@ -550,10 +600,29 @@ and configured the second.
550
600
 
551
601
  ### `motion` (one of)
552
602
 
603
+ Every field each motion type takes — **bold** is required; anything else
604
+ on a motion is an unknown property, ignored with a warning. Lengths are
605
+ fractions of `min(w, h)` like every size (px under `units: "px"`).
606
+
607
+ | `type` | fields | units and ranges |
608
+ | --- | --- | --- |
609
+ | `drift` | **`speed`**, `angle`, `bidirectional`, `bob`, `ease` | `speed` [min,max] per second; `angle` degrees (0 = right, 90 = down); `bob` vertical wobble amplitude (typ. 0.002–0.02) |
610
+ | `rise` | **`speed`**, `sway`, `ease` | `speed` [min,max] per second, upward; `sway` horizontal amplitude (typ. 0.002–0.02) |
611
+ | `bounce` | **`speed`** | `speed` [min,max] per second, diagonal |
612
+ | `static` | — | none |
613
+ | `orbit` | **`speed`**, **`radius`**, `center` | `speed` [min,max] **degrees** per second (negative turns the other way, ≤ 180); `radius` [min,max]; `center` `{x, y}` 0..1 or `{layer}` |
614
+ | `wander` | **`speed`**, `angle`, `meander`, `coherence`, `ease` | `speed` [min,max] per second; `angle` degrees (omit for a seeded heading); `meander` amplitude ≤ 0.463; `coherence` 0..1 |
615
+ | `warp` | **`speed`**, `center` | `speed` [min,max] **depth-units** per second (≤ 1.5); `center` `{x, y}` 0..1 |
616
+ | `path` | **`points`**, **`duration`**, `curve`, `closed`, `scatter` | `points` 2..24 `{x, y}` 0..1; `duration` ms per lap (≥ 2000); `curve` `"smooth"` \| `"linear"`; `closed` boolean; `scatter` > 0 |
617
+
618
+ `ease` is `{ "type": "settle" | "buoyant", "tau": ms }` (below). `bob` and
619
+ `sway` warn `motion-amplitude` above 0.25: `bob: 4` swings the layer four
620
+ screen-heights each way, a px value written into a viewport-unit spec.
621
+
553
622
  - `{ "type": "drift", "speed": [0.02, 0.06], "angle": 0, "bidirectional": true, "bob": 0.004 }`
554
623
  — heading in degrees (0 = right, 90 = down); `bidirectional` flips horizontal
555
- direction per entity (fish); `bob` adds vertical wobble
556
- - `{ "type": "rise", "speed": [0.01, 0.03], "sway": 0.006 }` — upward, with horizontal sway (bubbles)
624
+ direction per entity (fish); `bob` adds vertical wobble (0.004 ≈ 4 px at 1080p)
625
+ - `{ "type": "rise", "speed": [0.01, 0.03], "sway": 0.006 }` — upward, with horizontal sway (bubbles; 0.006 ≈ 6 px at 1080p)
557
626
  - `{ "type": "bounce", "speed": [0.12, 0.24] }` — diagonal, reflecting off edges
558
627
  - `{ "type": "static" }` — stays exactly where placed (use with `position` for HUD text)
559
628
  - `{ "type": "orbit", "speed": [10, 40], "radius": [0.05, 0.2], "center": { "x": 0.5, "y": 0.5 } }`
@@ -773,6 +842,12 @@ Paths that are **paint** (colours, `alpha` inside a sprite's paint, layer
773
842
  pop. Prefer `opacity` over `alpha` and `transform` over `position` for
774
843
  anything that should move smoothly.
775
844
 
845
+ A live steer glides `smooth` unless a delta names `ease: "in"`, `"out"` or
846
+ `"spring"` (the longest delta's ease shapes the glide). `step`, `linear` and
847
+ `smooth` on a steer still mean smooth: tracks carried those names before
848
+ steers read an ease at all, and honouring them now would change how every
849
+ stored track replays.
850
+
776
851
  ## Timeline — keyframes on the scene's clock
777
852
 
778
853
  A SaverSpec can carry authored change on its **own** clock — the same clock
@@ -794,7 +869,12 @@ its drift, orbit and pulse run on (from mount; a sequence segment's `localT`):
794
869
  (default 1000, the live-steer default; `0` is a cut; at most 30000 — a
795
870
  longer `dur` fails validation for the whole spec, so split a slower glide
796
871
  into consecutive keys of ≤ 30 s each), eased `smooth`
797
- (default), `linear` or `step` (holds, then switches at the glide's end).
872
+ (default), `linear`, `step` (holds, then switches at the glide's end),
873
+ `in` (accelerates from rest — a departure), `out` (leaves at speed and
874
+ decelerates into place — an arrival) or `spring` (a critically damped
875
+ spring: a quick start that settles with no overshoot). Every ease is
876
+ monotone, so a glide never leaves the range its two values bound.
877
+ **Native:** an ease it does not know reads as `smooth`.
798
878
  Keys address the same dot-paths as `setParam` — index or `key` form — and a
799
879
  path must already exist on the spec (a key cannot add a field). The spec must
800
880
  still validate with each key's value applied.
@@ -809,7 +889,38 @@ its drift, orbit and pulse run on (from mount; a sequence segment's `localT`):
809
889
  Keys are also checked **together**: two keys that are each valid can
810
890
  combine past a floor (a `clock.rate` and a `pulse.period` share one), so
811
891
  the validator resolves the scene at every key's start, end and glide
812
- quarter-points and rejects it there.
892
+ quarter-points and rejects it there. Paint keys on different paths can
893
+ also combine into a faster flicker than any one path allows: a top
894
+ layer's `opacity` switching between its own colour and the colour
895
+ beneath, each keyed at the floor and out of step, flickers the whole
896
+ frame 5 times a second. So where any one second holds 7 or more key
897
+ boundaries on paint paths (opacity, alpha, colours, stops, gradients,
898
+ strokes), publishing resolves the frame there and refuses more than 3
899
+ flashes a second over a quarter of the screen (WCAG 2.3.1,
900
+ `timelineFlashReport`). Keep big brightness changes at least ~170 ms
901
+ apart across the whole frame, not just per path. The count is also taken
902
+ per cell of the check's 16 × 9 grid, from the keys on the layers that can
903
+ paint that cell: a staggered wave of small static dots is busy across the
904
+ frame but sparse in every cell, so it passes without a frame resolved. A
905
+ layer that moves, emits, links or has its geometry keyed counts as
906
+ reaching every cell. Past 512 frames to resolve, publishing refuses and
907
+ says the timeline is too dense to verify: put fewer paint keys in that
908
+ second, or align them to shared times.
909
+ - **Path drawings: what a key can address.** Every field of a path
910
+ polygon is paint, so all of these glide (`N` is a part's index):
911
+ `sprite.path` and `sprite.parts.N.path` (a morph number by number when the
912
+ commands match, else a step); `sprite.color` / `sprite.parts.N.color`;
913
+ `sprite.parts.N.opacity`; `…stroke.width`, `…stroke.color`,
914
+ `…stroke.opacity`; `…gradient.stops.M.color` / `.opacity` / `.at`; and a
915
+ gradient's `from` / `to` / `center` / `radius` / `focus`. A key can only
916
+ address a field the spec **declares** — key `parts.N.opacity` or a
917
+ stroke's `opacity` only after writing it on the part (`"opacity": 1`).
918
+ A **gradient paints over the flat colour**: a `color` key on a part (or
919
+ path sprite) that has a `gradient` changes nothing, and the validator
920
+ warns `timeline-key-no-effect` — key its stops instead. A keyed path may
921
+ **collapse to nothing** (an eye blinking shut, a mouth closing to a line):
922
+ at those moments that part draws nothing, rather than the scene being
923
+ refused. The base spec's own paths must still draw something.
813
924
  - A key on a **structural** path rebuilds the scene at that key — a pop, and
814
925
  the validator warns `timeline-structural-key`. Animate paint: colours,
815
926
  `opacity`, `transform`, polygon `points` (a whole equal-length array
@@ -840,9 +951,15 @@ Each steer is applied once per server stamp, so a host re-sending the whole
840
951
  track (idle-server does, on every steer) never re-arms a hold the timeline has
841
952
  taken back. A viewer that mounts later replays the stored track at its own
842
953
  mount: a steer on an animated path shows there until that path's next key
843
- after the mount, while screens that saw it live have already moved on. Until
844
- viewers share a clock (`sync: 'epoch'`, still unwired on the site), screens
845
- mounted at different times can briefly disagree on such a path.
954
+ after the mount, while screens that saw it live have already moved on. Inside
955
+ a `sync: 'epoch'` sequence every viewer shares the channel's clock (the
956
+ idlescreens.com viewer passes `sequenceBaseT` from idle-server PR 247 on), so
957
+ playback lines up. That aligns playback, not steers: a steer is still armed at
958
+ the receiving screen's current time rather than the server stamp, so a stored
959
+ track replayed at a later mount can still hold a path until a different key
960
+ than a screen that saw it live. A timed scene on its own, or a `mount`
961
+ sequence, runs on each screen's own clock, so screens mounted at different
962
+ times can briefly disagree on such a path.
846
963
 
847
964
  **One format for both use cases.** An ambient scene is a SaverSpec without a
848
965
  timeline — nothing about it changes. A timed piece (an ident, a film, a
@@ -962,14 +1079,40 @@ moves and scales its positions and sizes (sprite rotation is not modelled).
962
1079
  will look wrong on the wall; one that passes may still sit a few px off.
963
1080
  Two more are **opt-in** through the text sprite's `role: "read"` (see the
964
1081
  `textBlock` sprite): `text-legibility` (WCAG ratio below 4.5:1 against the
965
- background at the box centre or under the brightest additive layer that can
966
- sit beneath the line) and `text-safe-area` (the box within 5 % of an edge,
1082
+ background at the box centre, or the solid chip painted under the whole box,
1083
+ or under the brightest additive layer that can sit beneath the line) and `text-safe-area` (the box within 5 % of an edge,
967
1084
  with its box in `boxes`). Undeclared text is never measured for either.
968
1085
  `overlap-seams` catches the trap the other channels are blind to: a layer
969
1086
  whose entities bury each other (so it was drawn as one shape) but whose paint
970
1087
  settings make every overlap visible. It is gated to stay off deliberate
971
1088
  washes — it needs at least 6 entities and a mean base alpha of 0.45, and
972
- never fires under `blend: lighter` / `screen`. No shipped example trips it.
1089
+ never fires under `blend: lighter` / `screen`. Nor does it fire when the
1090
+ only seam is a multi-colour palette and the shapes bury their neighbours'
1091
+ outlines but under 15 % of their area — rows or tiles butted edge to edge
1092
+ (overlapping by a hairline to hide the gap) are banding, not a pile. No
1093
+ shipped example trips it.
1094
+ `motion-intensity-mismatch` fires when a scene declares
1095
+ `motionIntensity: "calm"` but a layer of shapes, glyphs or text spins
1096
+ faster than 90°/s on average (shipped calm spins stay at or under 32°/s).
1097
+ `flash-near-limit` fires when a timeline's luminance keys, resolved over
1098
+ their densest one-second window, flicker at least 2 times a second over a
1099
+ quarter of the frame — a full-frame step at the 200 ms key floor is 2.5, which
1100
+ the publish-time WCAG gate (more than 3) lets through.
1101
+ `loop-seam` fires when a scene declares a looping timeline and the rest
1102
+ of the scene repeats too, but on a period the lap does not divide — so
1103
+ one lap can never come round to its own first frame (it names the period
1104
+ to make the lap a multiple of). A scene whose other motion never repeats
1105
+ (drifting stars, wandering motes) is not judged: that is ambient by
1106
+ design. `scenePeriod(spec)` is the analysis behind it — the least common
1107
+ multiple of every periodic part (pulse, grow, rock, spin, cycle, orbit,
1108
+ path laps, emit, caret, background drift, a looping timeline), `0` for a
1109
+ static scene, or `null` with the reasons when something never repeats
1110
+ (seeded speed ranges, wrapping travel, a `life` build, a non-looping
1111
+ timeline, a self-typing reveal, animated grain) — and likewise when a part
1112
+ repeats between whole milliseconds (`spin: 7` laps every 51428.57 ms, which
1113
+ no whole-ms period reproduces) or a timeline key moves a period, rate or
1114
+ speed (the cycle itself then varies). An exporter reads it to cut an exact
1115
+ loop.
973
1116
  The two density advisories read the spec's declared `density` first:
974
1117
  `sparse-scene` is withheld under `density: "sparse"` and `dense-scene`
975
1118
  under `"dense"`, and `density-mismatch` fires instead when the measured
@@ -1107,6 +1250,20 @@ cross-fades on every segment change using the transition's `dur`, so `fade`
1107
1250
  matches the native player rather than diverging from it; native reads
1108
1251
  `fade.dur` where it reads the morph `dur` today.
1109
1252
 
1253
+ `{ type: 'wipe', dur, shape?, origin?, angle?, feather? }` reveals the
1254
+ incoming segment through a moving edge instead of a dissolve: `shape:
1255
+ 'circle'` (default) is an iris opening from `origin` (`{x, y}` 0..1,
1256
+ default the centre) until it clears the farthest corner; `'line'` sweeps a
1257
+ straight edge along `angle` degrees (0 = left to right, 90 = top to
1258
+ bottom). `feather` (0..1, default 0.08) is the edge's softness, as a
1259
+ fraction of the distance it travels. It runs on `fade`'s machinery — both
1260
+ segments live on canvases of their own for `dur`, each composited through
1261
+ the edge's mask — with the same `dur` bounds, the same tier gate (`basic` /
1262
+ `minimal` hosts cut, and `fade-degrades-on-low-tier` counts wipes), and the
1263
+ same answer to `boundary-luminance-jump`: every pixel changes once, a
1264
+ spatial ramp, never a flash. `text` is a morph option and is rejected on a
1265
+ wipe. **Native:** tvOS cross-fades over `dur`.
1266
+
1110
1267
  **Wrap morph:** under `loop: true` a morph declared on the **last** segment
1111
1268
  is ignored at the wrap — the lap cuts back to segment 0, and every stored
1112
1269
  sequence expects that. `wrapMorph: true` on the envelope honours it: the lap
@@ -1287,5 +1444,9 @@ layers); and `lobby-talk` (one screen of a quarter-in-review, every text
1287
1444
  layer `role: "read"`, zero advisories — readable copy over additive
1288
1445
  atmosphere); and `thermal-field` (a `field` background in six riso inks,
1289
1446
  `quantize: 6`, warped and drifting, under a handful of screened motes, with
1290
- a grain-and-dither `finish` — the ground is the piece and the paper shows). See [`src/examples/`](./src/examples/). The dashboard exercises the
1447
+ a grain-and-dither `finish` — the ground is the piece and the paper shows); and
1448
+ `ink-tide` (the motion vocabulary in one scene: a pen line that draws itself
1449
+ on with `stroke.draw` eased `out` and erases eased `in`, a boat that `rock`s on
1450
+ a clock, and a caption that lifts in with `reveal.rise` — its rock period
1451
+ divides the 30 s lap, so `loop-seam` stays quiet). See [`src/examples/`](./src/examples/). The dashboard exercises the
1291
1452
  static/HUD subset at scale (34 layers of keyed, positioned text).