@idle-screens/schema 3.15.0 → 3.16.1

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
@@ -52,7 +52,9 @@ scene's own clock), paint-level layer `opacity` and `transform`,
52
52
  and path strokes, the `in` / `out` / `spring` glide eases (timeline keys and
53
53
  live steers), `rock`, `life.stagger`, `reveal.rise`, `textBlock.number`, the
54
54
  `wipe` transition, `scenePeriod` and the `loop-seam` advisory
55
- (2026-10-25 — motion vocabulary).
55
+ (2026-10-25 — motion vocabulary); path paint on `points` polygons,
56
+ `textBlock.number.minDigits`, `stroke.draw.mode` and `transform.scaleY`
57
+ (2026-10-27 — small format adds).
56
58
 
57
59
  ## Safety invariants
58
60
 
@@ -213,10 +215,8 @@ exceeds 2 %, or a `dense` one that would have tripped `sparse-scene`, gets a
213
215
  two field segments glides the field; a gradient → field morph steps
214
216
  (different shape).
215
217
 
216
- **Native:** tvOS ignores the field and paints a **vertical gradient through
217
- `bands` in order** (first band at the top, last at the bottom), so a field
218
- scene still carries its palette on the Apple TV until the native player
219
- rasters the same sampler.
218
+ **Native:** tvOS rasters the same sampler (bit-identical hash), at 96 px on
219
+ the short side and 48 px on its sprite tier — the `basic` tier's raster.
220
220
 
221
221
  ### `finish`
222
222
 
@@ -278,7 +278,9 @@ steps. Declare the block (`"finish": { "grain": 0 }`) to make the paths
278
278
  exist — a steer cannot add a `finish` to a spec without one. A sequence's
279
279
  own `finish` is not steerable; steer a segment's, or republish.
280
280
 
281
- **Native:** tvOS **ignores `finish`** — the frame is presented as drawn.
281
+ **Native:** tvOS screens the same seeded grain and Bayer tiles with `overlay`
282
+ over the composed frame; `animate` steps on the Canvas tier and holds static
283
+ on the sprite tier (the `basic` tier rule).
282
284
 
283
285
  ### `layers[]`
284
286
 
@@ -293,22 +295,22 @@ own `finish` is not steerable; steer a segment's, or republish.
293
295
  | `alpha` | `[min,max]` 0..1 | `[1,1]` | per-entity opacity range |
294
296
  | `blend` | `lighter` \| `screen` \| `multiply` | source-over | additive glow / gentle additive / darkening |
295
297
  | `region` | `{x?, y?}` ranges 0..1 | full viewport | fractional spawn window (placement only, not travel) |
296
- | `pulse` | `{amp ≤ 0.5, period ≥ 500, wave?}` | none | opacity breathing: `amp` is **added** to the entity's `alpha` (alpha ± amp, clamped to 0..1), not a fraction of it — a dim layer (`alpha` 0.2, `amp` 0.3) swings through 0 and sits fully dark for part of every period; keep `amp` ≤ the alpha floor for a breath that never blacks out. `wave: {wavelength, angle?}` turns it into a traveling wave across the field |
298
+ | `pulse` | `{amp 0..0.5, period ≥ 500, wave?}` | none | opacity breathing (`amp: 0` is no pulse — one layer shape can serve a lit and an unlit variant): `amp` is **added** to the entity's `alpha` (alpha ± amp, clamped to 0..1), not a fraction of it — a dim layer (`alpha` 0.2, `amp` 0.3) swings through 0 and sits fully dark for part of every period; keep `amp` ≤ the alpha floor for a breath that never blacks out. `wave: {wavelength, angle?}` turns it into a traveling wave across the field |
297
299
  | `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` |
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 |
300
+ | `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:** rendered |
299
301
  | `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 |
302
+ | `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:** rendered |
301
303
  | `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 |
302
304
  | `links` | see below | none | inter-entity lines |
303
305
  | `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 |
306
+ | `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:** rendered |
305
307
  | `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 |
306
308
  | `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` |
307
309
  | `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`) |
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 |
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 |
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 |
311
- | `group` | string | none | the name of a `groups` entry this layer paints through — see **Groups**. **Native:** ignored |
310
+ | `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:** rendered |
311
+ | `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:** rendered |
312
+ | `transform` | `{x?, y?, scale?, scaleX?, scaleY?, 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), `scaleX` (±8, × `scale` horizontally — toward 0 reads as a turn about the vertical axis, negative mirrors) and `scaleY` (±8, × `scale` vertically — key it against `scaleX` for squash-and-stretch: wide-and-short on a landing, narrow-and-tall on take-off; negative flips). **Order:** `scale`/`scaleX`/`scaleY` (along the layer's own axes) 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:** rendered (the sprite tier applies the summed rotation and per-axis scale, exact unless a rotated layer is scaled unevenly) |
313
+ | `group` | string | none | the name of a `groups` entry this layer paints through — see **Groups**. **Native:** rendered |
312
314
 
313
315
  `links`: `{ k: 1..8, maxDist, color?, alpha?, width?, mode?, falloff?, closed? }`.
314
316
  `mode: "nearest"` (default) wires each entity to its k nearest neighbors within
@@ -365,6 +367,14 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
365
367
  scaled by each entity's seeded radius (not by the keyed shape's own extent):
366
368
  keep every keyed point inside −1..1 (the validator rejects one outside), and
367
369
  shrink a target shape by scaling its points, not by exceeding the box.
370
+ A `points` polygon also takes the path paint below — **`stroke`**,
371
+ **`fill: false`** and **`gradient`** — and then draws as the closed path
372
+ through its points (`M p0 L p1 … Z`, the closing edge included), so a
373
+ keyed `points` array is cheap line art: a stick figure or a wire rig whose
374
+ joints lerp, with `fill: false` and a `stroke`. An open figure traced as
375
+ an out-and-back tour (`[[0,0], [0.3,0.6], [0,0], [-0.3,0.6], [0,0], [0,-0.6]]`)
376
+ strokes each limb twice, which reads as one line. `stroke.draw` works
377
+ too. `fillRule`, `span` and `origin` stay path-only.
368
378
  For anything more detailed than a 24-point facet, give the polygon a
369
379
  **path** instead (next entry)
370
380
  - **Path drawings** — `{ "kind": "polygon", "radius": [0.3, 0.3], "color": "#2f6a3e", "path": "M 0 -0.9 C 0.5 -0.9 0.7 -0.3 0.5 0.1 … Z" }`:
@@ -384,7 +394,8 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
384
394
  `"evenodd"` are SVG's own rules — use them for pasted SVG, for overlap
385
395
  patterns (a rosette of overlapping circles wants `evenodd`), and when a
386
396
  same-colour shape sits inside another on purpose (`auto` would cut it).
387
- - **`gradient`** replaces the flat colour of the fill:
397
+ - **`gradient`** replaces the flat colour of the fill (the sprite still
398
+ needs a `color` or `colors`; a gradient does not stand in for it):
388
399
  `{ "type": "linear", "from": [0, -1], "to": [0, 1], "stops": [{ "at": 0, "color": "#2c6a3a" }, { "at": 1, "color": "#7ab070", "opacity": 0.6 }] }`
389
400
  or `{ "type": "radial", "center": [0, 0], "radius": 1, "focus": [-0.3, -0.4], "stops": [...] }`
390
401
  (2..8 stops, `at` 0..1 non-decreasing, optional per-stop `opacity`).
@@ -400,8 +411,13 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
400
411
  part's subpaths counted as one pen run in the order the path draws them
401
412
  (a closed ring includes its closing edge, and comes back open when cut) —
402
413
  `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.
414
+ that draws itself. `draw.mode: "each"` draws every subpath at once
415
+ instead: each is cut to the same `start`..`end` of its **own** length,
416
+ so 12 spokes of a burst, the ticks of a dial or a field of hatching all
417
+ grow together from one key (`"run"`, the default, is the one pen run).
418
+ `mode` is an enum, so a steer or key on it steps. Paint;
419
+ `…stroke.draw.end` is a key target like the stroke's width.
420
+ **Native:** rendered.
405
421
  - **`fill: false`** draws the stroke only — line art. Then open paths
406
422
  (no `Z`) are fine: veins, hatching, antennae, rigging.
407
423
  - **`parts`** — a multi-colour drawing in one layer:
@@ -409,7 +425,9 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
409
425
  (1..256), drawn back to front, each with its own `path`, `color`
410
426
  (default: the entity's colour, so `colors` palettes still pick per
411
427
  entity), `fillRule`, `fill`, `gradient`, `stroke` and `opacity`. With
412
- `parts`, put that paint on the parts, never on the sprite. One layer
428
+ `parts`, put that paint on the parts, never on the sprite. The sprite
429
+ still needs its own `color` (or a `colors` palette): parts without a
430
+ colour inherit it, and it is required even when every part sets one. One layer
413
431
  holds a whole illustration, and a counted layer reuses it: a balloon with
414
432
  gores, shading, ropes and basket × 7 drifting entities, all parts moving
415
433
  as one.
@@ -449,7 +467,7 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
449
467
  and 1) — glide `end` 0 → 1 and the mark draws itself on; glide `start` after
450
468
  it and it erases from the tail. Paint (it glides, it is outside the
451
469
  structural signature), and perception scales the ink with it.
452
- **Native:** ignored ⇒ the whole mark. Van Gogh's stroke, Hokusai's contour,
470
+ **Native:** rendered (the sprite tier in 1/128 steps of the length). Van Gogh's stroke, Hokusai's contour,
453
471
  O'Keeffe's petal, Basquiat's scrawl
454
472
  - `{ "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 }` —
455
473
  multi-line text block with deterministic line-breaking. All dimensions are
@@ -482,8 +500,8 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
482
500
  it no longer moves the ink. Anchor is placement, so it is part of the
483
501
  structural signature (a change rebuilds); rotation stays about the anchor
484
502
  point. The perception boxes (`text-overlap`, `text-off-screen`) follow the
485
- same rule. Absent ⇒ exactly the pre-anchor behaviour above. **Native
486
- clients that do not read `anchor` render as absent (layout-box top-left).**
503
+ same rule. Absent ⇒ exactly the pre-anchor behaviour above. **Native:**
504
+ rendered.
487
505
 
488
506
  `font` is a CSS family and/or weight/style — `"bold monospace"`,
489
507
  `"300 'Inter', sans-serif"` — composed with the scaled `fontSize` the way
@@ -493,7 +511,9 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
493
511
  SF Mono, Fira Code, …) switches the line-breaker to a uniform 0.6 em
494
512
  advance (1 em for a full-width glyph) so mono wraps land where the real face puts them; every other
495
513
  family uses the proportional table. Default `system-ui, sans-serif`.
496
- **Native clients fall back to the system face** until a face table exists.
514
+ **Native:** tvOS uses the first family in the stack installed on the
515
+ device (as the web does) and the same metrics tables, else the system face
516
+ in the stack's class (serif, monospace, rounded) at its weight and slant.
497
517
  `font` names faces **installed on the viewing device** — nothing is
498
518
  bundled and no web font is fetched, so `'Inter'` only appears where Inter
499
519
  is installed and every other screen falls through the stack. End every
@@ -507,8 +527,8 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
507
527
  `setParam("h.sprite.opacity", 0, { dur: 1500 })` glides the block out
508
528
  without a rebuild (and back in with `1`). Declare `"opacity": 1` on the
509
529
  block to make the path steerable (a `setParam` cannot create a field that
510
- is not there). Scales perceived ink in the luminance grid. **Native clients
511
- without `opacity` render at 1.**
530
+ is not there). Scales perceived ink in the luminance grid. **Native:**
531
+ rendered.
512
532
 
513
533
  `role` (`"read"` | `"atmosphere"`, on `textBlock` **and** `text`) declares
514
534
  what the words are *for*, and is the only thing that turns legibility
@@ -550,12 +570,22 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
550
570
  popping it whole (the caption look); `fade` (0–1 exclusive of 0, default
551
571
  0.15, `glyphFade` only) is each glyph's fade window as a fraction of
552
572
  progress — small reads as a soft typewriter, large as a wave of
553
- overlapping fades. Still a pure function of `progress`, so it glides and
554
- deletes exactly like the other modes. Native clients render `glyphFade`
555
- as `typewriter` until they gain per-glyph alpha. With `glyphFade`, `rise`
573
+ overlapping fades. The mapping is exact, so a key can land a beat: with
574
+ `N` glyphs and fade `f`, glyph `g` (0-based) starts fading in at progress
575
+ `(g/N)·(1−f)` and is **fully shown at `f + (g/N)·(1−f)`**. `N` counts the
576
+ graphemes of the **broken** lines — line breaks (authored `\n` or wrap)
577
+ are not glyphs, and spaces are. So the last glyph of line 1 (index
578
+ `n₁ − 1`, where `n₁` is line 1's grapheme count) is fully shown at
579
+ `f + ((n₁ − 1)/N)·(1−f)`, not at `n₁/N`. In `mode: "line"` whole lines
580
+ pop: `round(progress · L)` of the `L` broken lines show, so line `k`
581
+ (1-based) appears at progress `(k − ½)/L` — key `k/L` to show exactly `k`
582
+ lines, and remember a wrapped line counts as its own line.
583
+ Still a pure function of `progress`, so it glides and
584
+ deletes exactly like the other modes (tvOS renders it glyph by glyph; its
585
+ sprite tier re-bakes the block per 1/16 of a glyph's alpha). With `glyphFade`, `rise`
556
586
  (0..1 em, default 0) lifts each glyph into its line as it fades in, from
557
587
  `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).
588
+ moves (it warns and is ignored on the other modes).
559
589
  `caret` draws a
560
590
  blinking caret at the frontier (`true`, or `{ "blink": 1.2, "color": "#fff" }`;
561
591
  blink is in full cycles/sec, capped at 3 for flash safety, and is a square
@@ -574,14 +604,22 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
574
604
  The block paints `prefix + value + suffix` instead of `text`, `value`
575
605
  rounded to `decimals` (0..6, default 0) with its thousands grouped by
576
606
  commas unless `grouping: false` — formatted by the engine, never by the
577
- viewer's locale, so every screen shows the same digits. `value` is
607
+ viewer's locale, so every screen shows the same digits. `minDigits`
608
+ (1..12, default 1) zero-pads the integer part for a fixed-width counter:
609
+ `{ "value": 42, "minDigits": 4, "grouping": false, "prefix": "FRAME " }`
610
+ paints `FRAME 0042`. Padding comes before grouping (as `Intl`'s
611
+ `minimumIntegerDigits` does), so with the default grouping 42 reads
612
+ `0,042` — set `grouping: false` for a counter. The sign stays outside the
613
+ padding (`-0042`). With a monospace `font` the counter keeps its width as
614
+ it rolls. `value` is
578
615
  **paint**: `setParam("stats.sprite.number.value", 48210, { dur: 4000 })`
579
616
  rolls the digits up over the glide, and a timeline key does the same.
580
617
  Line-breaking runs on the formatted string, so a counter that gains a
581
618
  digit can re-wrap: give it width to spare and an `anchor` on the edge that
582
619
  should stay put. Perception and the text advisories read the formatted
583
620
  number. `text` is what clients that do not read `number` show — author it
584
- as the counter's rest value. **Native:** ignored ⇒ `text`.
621
+ as the counter's rest value. **Native:** rendered (the same locale-free
622
+ formatting); clients that do not read `number` show `text`.
585
623
 
586
624
  `circle`, `ring`, `streak`, `rect`, `bar`, `polygon` and `stroke` all accept
587
625
  `colors: [...]` (seeded per-entity palette pick) and `colorWeights: [...]`
@@ -874,7 +912,7 @@ its drift, orbit and pulse run on (from mount; a sequence segment's `localT`):
874
912
  decelerates into place — an arrival) or `spring` (a critically damped
875
913
  spring: a quick start that settles with no overshoot). Every ease is
876
914
  monotone, so a glide never leaves the range its two values bound.
877
- **Native:** an ease it does not know reads as `smooth`.
915
+ **Native:** tvOS honours all six; an ease it does not know reads as `smooth`.
878
916
  Keys address the same dot-paths as `setParam` — index or `key` form — and a
879
917
  path must already exist on the spec (a key cannot add a field). The spec must
880
918
  still validate with each key's value applied.
@@ -903,9 +941,14 @@ its drift, orbit and pulse run on (from mount; a sequence segment's `localT`):
903
941
  paint that cell: a staggered wave of small static dots is busy across the
904
942
  frame but sparse in every cell, so it passes without a frame resolved. A
905
943
  layer that moves, emits, links or has its geometry keyed counts as
906
- reaching every cell. Past 512 frames to resolve, publishing refuses and
944
+ reaching every cell, except for keys on its own `transform` or its
945
+ group's: those reach everywhere the transform can carry the drawing while
946
+ `x`, `y`, `scale`, `scaleX` and `rotate` range over their base and keyed
947
+ values (a tile turning about its anchor stays in its own cells; an `x`
948
+ glide covers the cells it sweeps). Past 512 frames to resolve, publishing refuses and
907
949
  says the timeline is too dense to verify: put fewer paint keys in that
908
- second, or align them to shared times.
950
+ second, or align them to shared times. That case is known without
951
+ resolving a frame (`flashSamplingForecast`).
909
952
  - **Path drawings: what a key can address.** Every field of a path
910
953
  polygon is paint, so all of these glide (`N` is a part's index):
911
954
  `sprite.path` and `sprite.parts.N.path` (a morph number by number when the
@@ -966,9 +1009,10 @@ timeline — nothing about it changes. A timed piece (an ident, a film, a
966
1009
  pre-roll) is a scene whose evolution is a timeline, and a sequence only where
967
1010
  it genuinely cuts to a different set of layers.
968
1011
 
969
- **Native:** tvOS ignores `timeline` and shows the base spec — so **author the
970
- base as the piece's rest state** (its end card), and a timed piece degrades
971
- to a valid ambient scene.
1012
+ **Native:** tvOS plays the timeline (every frame on its Canvas tier, at 12 Hz
1013
+ on its sprite tier). Still **author the base as the piece's rest state** (its
1014
+ end card): a client that does not read `timeline` shows the base, and a timed
1015
+ piece then degrades to a valid ambient scene.
972
1016
 
973
1017
  ## Groups — one handle for a multi-layer subject
974
1018
 
@@ -995,8 +1039,9 @@ record's grooves and label) can be moved, scaled and faded as one thing:
995
1039
  end without the group paints it at the identity); moving a layer from one
996
1040
  group to another steps on the morph's first frame.
997
1041
 
998
- **Native:** ignored ⇒ identity transform, opacity 1 — author the base so the
999
- subject reads without the group's paint.
1042
+ **Native:** rendered. A client that does not read `groups` paints the
1043
+ identity (no transform, opacity 1), so author the base to read without the
1044
+ group's paint.
1000
1045
 
1001
1046
  ## Seeing without eyes — the perception API
1002
1047
 
@@ -1101,7 +1146,13 @@ moves and scales its positions and sizes (sprite rotation is not modelled).
1101
1146
  `loop-seam` fires when a scene declares a looping timeline and the rest
1102
1147
  of the scene repeats too, but on a period the lap does not divide — so
1103
1148
  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
1149
+ to make the lap a multiple of). `spin`, `rock`, `pulse` and `grow` are left
1150
+ out of that period: they run on scene time and change smoothly, so nothing
1151
+ jumps at the wrap, and a lap off their period still loops seamlessly on
1152
+ screen (only an exact one-lap export would differ). Keyed paths are
1153
+ continuous at the wrap by construction — each lap enters from the last
1154
+ key's value — unless a key cuts there on purpose (`t: 0, dur: 0`) or a
1155
+ glide overruns the lap (`timeline-glide-overruns-lap` warns). A scene whose other motion never repeats
1105
1156
  (drifting stars, wandering motes) is not judged: that is ambient by
1106
1157
  design. `scenePeriod(spec)` is the analysis behind it — the least common
1107
1158
  multiple of every periodic part (pulse, grow, rock, spin, cycle, orbit,
@@ -1111,8 +1162,14 @@ moves and scales its positions and sizes (sprite rotation is not modelled).
1111
1162
  timeline, a self-typing reveal, animated grain) — and likewise when a part
1112
1163
  repeats between whole milliseconds (`spin: 7` laps every 51428.57 ms, which
1113
1164
  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.
1165
+ speed (the cycle itself then varies). A spin counts at its drawing's
1166
+ rotational symmetry: a circle or ring never shows its spin, a regular
1167
+ `sides` n-gon repeats every 360°/n, and a `points` or path drawing whose
1168
+ every paint (fill colour, stroke, opacity) maps onto itself under a turn
1169
+ of 360°/n (n up to 64) does too — 18 identical spokes at `spin: 16`
1170
+ repeat every 1250 ms, not 22.5 s. A gradient, a part-drawn stroke
1171
+ (`stroke.draw`), a stretched `span` or a soft fill off its `origin` is
1172
+ taken as asymmetric. An exporter reads it to cut an exact loop.
1116
1173
  The two density advisories read the spec's declared `density` first:
1117
1174
  `sparse-scene` is withheld under `density: "sparse"` and `dense-scene`
1118
1175
  under `"dense"`, and `density-mismatch` fires instead when the measured
@@ -1212,10 +1269,8 @@ place. `text` is a morph option only (a `fade` already cross-fades whole
1212
1269
  frames; a `cut` has no window) and the validator rejects it elsewhere.
1213
1270
  **Default `step` is today's behaviour byte for byte** — the sequence
1214
1271
  baseline pins it — and a flip to `crossfade` by default would be a major
1215
- change, with the baseline regenerated. **Native:** tvOS **steps** the words (it
1216
- ignores `text`) but already cross-fades whole frames on every segment
1217
- change using the transition's `dur`, so the result on the Apple TV is
1218
- close to the web's. Morph requires structurally
1272
+ change, with the baseline regenerated. **Native:** tvOS lerps the two
1273
+ segments frame by frame on the chain root's seed and honours `text`. Morph requires structurally
1219
1274
  identical adjacent segments (same `structuralSignature`); if they differ, the
1220
1275
  engine falls back to cut and the validator emits a `morph-structural-mismatch`
1221
1276
  warning. When the segments are structural twins whose only differences are
@@ -1245,10 +1300,9 @@ lowest tiers, so a host on the `basic` (canvas2d only) or `minimal` capability
1245
1300
  tier — passed as `capabilityTier` on the `SequenceMountContext`, the tier
1246
1301
  `computeTier` from `@idle-screens/capabilities` reports — renders `fade` as
1247
1302
  `cut`; absent ⇒ fade enabled. `adviseSequence` says so once per sequence with
1248
- the informational `fade-degrades-on-low-tier`. **Native:** tvOS already
1249
- cross-fades on every segment change using the transition's `dur`, so `fade`
1250
- matches the native player rather than diverging from it; native reads
1251
- `fade.dur` where it reads the morph `dur` today.
1303
+ the informational `fade-degrades-on-low-tier`. **Native:** tvOS cross-fades
1304
+ over `dur` on every tier (capped at 0.4 s on the pre-4K boxes); the
1305
+ transition is the outgoing segment's, as here.
1252
1306
 
1253
1307
  `{ type: 'wipe', dur, shape?, origin?, angle?, feather? }` reveals the
1254
1308
  incoming segment through a moving edge instead of a dissolve: `shape:
@@ -1262,7 +1316,7 @@ the edge's mask — with the same `dur` bounds, the same tier gate (`basic` /
1262
1316
  `minimal` hosts cut, and `fade-degrades-on-low-tier` counts wipes), and the
1263
1317
  same answer to `boundary-luminance-jump`: every pixel changes once, a
1264
1318
  spatial ramp, never a flash. `text` is a morph option and is rejected on a
1265
- wipe. **Native:** tvOS cross-fades over `dur`.
1319
+ wipe. **Native:** rendered (the same edge, eased the same way).
1266
1320
 
1267
1321
  **Wrap morph:** under `loop: true` a morph declared on the **last** segment
1268
1322
  is ignored at the wrap — the lap cuts back to segment 0, and every stored
@@ -1275,7 +1329,7 @@ entity placement; otherwise it is still a cut and the validator warns
1275
1329
  `wrap-morph-inactive` with the reason. With it, a looping ident is a closed
1276
1330
  cycle of poses. A clicker jump to segment 0 (a `sequence.segment` steer)
1277
1331
  still cuts — it arrives from wherever the presenter was, not from the last
1278
- segment. **Native:** tvOS cross-fades the wrap like any segment change.
1332
+ segment. **Native:** rendered.
1279
1333
 
1280
1334
  **Timelines in segments:** a segment's scene may carry a `timeline`; it runs
1281
1335
  on the segment's `localT`. A morph resolves each side on its own clock — the
@@ -1306,9 +1360,9 @@ are the same under both — `advance: 'input'` is armed for every viewer, so a
1306
1360
  late joiner whose seeded clock is already past an unreleased hold lands **on**
1307
1361
  the held segment (still animating), not past it, and the clicker releases it
1308
1362
  from there. `epoch` without a `sequenceBaseT` starts at 0 (a host that does
1309
- not pass the hint loses nothing). **Native clients that anchor at their own
1310
- mount behave as `mount`** until they read the field and seed from the channel
1311
- epoch. The default is never flipped: pre-roll depends on `mount`.
1363
+ not pass the hint loses nothing). **Native:** tvOS seeds the clock from the
1364
+ channel epoch, as the site does; a client that anchors at its own mount
1365
+ behaves as `mount`. The default is never flipped: pre-roll depends on `mount`.
1312
1366
 
1313
1367
  **Bed:** `bed` is one SaverSpec drawn **under every segment on the
1314
1368
  sequence's global clock** — the ground that does not reset. Every segment still
@@ -1346,11 +1400,9 @@ slide content in the segments. Rules:
1346
1400
  - **Perception:** `perceiveSequenceFrame(seq, T)` composes bed + segment (see
1347
1401
  the perception API above). Bed and segments are assumed to share `units` /
1348
1402
  `referenceViewport`.
1349
- - **Native:** tvOS **ignores `bed` initially** and renders segments with their
1350
- own backgrounds, exactly as it does today — so a sequence authored with a bed
1351
- should still carry sensible segment backgrounds until the native player
1352
- draws the bed (a second compiled scene drawn first, which its layer model
1353
- already supports). Web viewers hide those backgrounds; native shows them.
1403
+ - **Native:** tvOS draws the bed under every segment on the sequence clock,
1404
+ as the web does. A client without bed support shows each segment's own
1405
+ background, so a sequence with a bed should still carry sensible ones.
1354
1406
  - **Not a default.** The "cheap form" — rendering a morph chain's root child at
1355
1407
  `T − segmentStart(chainRoot)` so twins keep one continuous clock — is **not**
1356
1408
  implemented and never will be as a default: stored morph-chained sequences