@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 +111 -59
- package/dist/index.d.ts +152 -19
- package/dist/index.js +513 -134
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +11 -7
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
|
|
217
|
-
|
|
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
|
|
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
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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)
|
|
311
|
-
| `group` | string | none | the name of a `groups` entry this layer paints through — see **Groups**. **Native:**
|
|
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.
|
|
404
|
-
|
|
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.
|
|
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:**
|
|
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
|
-
|
|
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
|
|
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
|
|
511
|
-
|
|
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.
|
|
554
|
-
|
|
555
|
-
|
|
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
|
|
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. `
|
|
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:**
|
|
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
|
|
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
|
|
970
|
-
base as the piece's rest state** (its
|
|
971
|
-
|
|
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:**
|
|
999
|
-
|
|
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).
|
|
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).
|
|
1115
|
-
|
|
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
|
|
1216
|
-
|
|
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
|
|
1249
|
-
|
|
1250
|
-
|
|
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:**
|
|
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:**
|
|
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
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|