@idle-screens/schema 3.12.0 → 3.12.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
@@ -192,8 +192,11 @@ exceeds 2 %, or a `dense` one that would have tripped `sparse-scene`, gets a
192
192
 
193
193
  **Steering.** Every numeric knob and every band is a paint path —
194
194
  `background.scale`, `background.warp`, `background.quantize`,
195
- `background.bands.2`, `background.drift.amount` — and glides (a fractional
196
- `quantize` or `octaves` mid-glide rounds). `background.seed` is not
195
+ `background.bands.2`, `background.drift.amount` — and glides. `quantize`
196
+ and `octaves` are level counts, so a glide (`setParam`, a `timeline` key, a
197
+ `morph`) steps through **whole** levels: the in-between value rounds, and a
198
+ `quantize` under 1.5 reads `0` (smooth) — so `quantize` 0 → 6 eases smooth,
199
+ then 2, 3 … 6 bands. Authored values must still be whole. `background.seed` is not
197
200
  steerable. The background is paint, not structure, so a `morph` between
198
201
  two field segments glides the field; a gradient → field morph steps
199
202
  (different shape).
@@ -269,7 +272,7 @@ own `finish` is not steerable; steer a segment's, or republish.
269
272
 
270
273
  | Field | Type | Default | Meaning |
271
274
  |---|---|---|---|
272
- | `count` | int 1..400 | — | entities in this layer |
275
+ | `count` | int 1..400 | — | entities in this layer. **At most 800 entities across all layers** (and ≤ 36 layers): the validator rejects a spec whose counts sum past 800, and density scaling never takes a screen past it |
273
276
  | `sprite` | SpriteSpec | — | what each entity looks like |
274
277
  | `motion` | MotionSpec | — | how entities move |
275
278
  | `size` | `[min,max]` > 0 | sprite-dependent | font size for emoji/text; ignored for shaped sprites |
@@ -278,13 +281,13 @@ own `finish` is not steerable; steer a segment's, or republish.
278
281
  | `alpha` | `[min,max]` 0..1 | `[1,1]` | per-entity opacity range |
279
282
  | `blend` | `lighter` \| `screen` \| `multiply` | source-over | additive glow / gentle additive / darkening |
280
283
  | `region` | `{x?, y?}` ranges 0..1 | full viewport | fractional spawn window (placement only, not travel) |
281
- | `pulse` | `{amp ≤ 0.5, period ≥ 500, wave?}` | none | opacity breathing; `wave: {wavelength, angle?}` turns it into a traveling wave across the field |
282
- | `spin` | number \| `[min,max]` ±360 deg/sec | none | per-entity rotation (seeded start angle); 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` |
284
+ | `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 |
285
+ | `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` |
283
286
  | `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 |
284
287
  | `grow` | `{amp ≤ 0.8, period ≥ 500}` | none | size breathing (seeded phase) |
285
- | `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) |
288
+ | `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 |
286
289
  | `links` | see below | none | inter-entity lines |
287
- | `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); text/emoji take `strings[i]` / `glyphs[i]` and palettes `colors[i]` **in order**, so N labels are one layer |
290
+ | `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 |
288
291
  | `life` | `{enter?, exit?, fade?}` ms | always on | act structure: fade the layer in at `enter`, out at `exit` |
289
292
  | `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 |
290
293
  | `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` |
@@ -308,20 +311,24 @@ with distance, removing pop-in at the cutoff.
308
311
  - `{ "kind": "text", "strings": ["HELLO"], "color": "#e6e8ef", "font": "bold monospace", "align": "center", "baseline": "middle", "maxWidth": 300, "cycle": ..., "role": "read" }`
309
312
  — a `font` **with** a px size keeps that shorthand, its number rescaled to the
310
313
  viewport (verbatim only under `units: "px"`); a family/weight only
311
- (`"bold monospace"`) composes with the seeded per-entity `size`; `role` is
314
+ (`"bold monospace"`) composes with the seeded per-entity `size` (faces
315
+ installed on the viewer only — see `font` under `textBlock`); `role` is
312
316
  the legibility opt-in described under `textBlock` below (same field, same
313
317
  meaning on both text sprites)
314
318
  - `{ "kind": "circle", "radius": [0.001, 0.003], "color": "#ffffff", "soft": true }` —
315
319
  `soft` renders a radial-falloff glow orb instead of a hard disc
316
320
  - `{ "kind": "ring", "radius": [0.002, 0.005], "color": "#d8f6ff", "width": 0.001 }` —
317
- unfilled circle (bubbles, portals, sonar)
321
+ unfilled circle (bubbles, portals, sonar). The line is **centred on the
322
+ radius**: half of `width` falls inside, half outside, so the outer edge is
323
+ `radius + width / 2`
318
324
  - `{ "kind": "streak", "length": [0.01, 0.03], "color": "#cfd8ff", "width": 0.002 }` —
319
325
  a line oriented along the entity's analytic heading with a faded tail
320
326
  (rain that reads as rain, shooting stars, warp stars)
321
327
  - `{ "kind": "rect", "width": [0.012, 0.02], "aspect": [1.3, 1.7], "color": "#ffb347", "feather": 0.6 }` —
322
328
  rectangle; `aspect` is the height/width ratio range (rotates with `spin`);
323
329
  `feather` 0..1 softens the edges — that fraction of the half-size fades out
324
- toward the border (Rothko's block at 0.5–0.8)
330
+ toward the border in one smooth linear falloff on each axis (Rothko's block
331
+ at 0.5–0.8)
325
332
  - `{ "kind": "bar", "values": [82, 64, 91], "max": 100, "length": 0.5, "thickness": 0.02, "color": "#17e8c8", "direction": "right" }` —
326
333
  a data bar: entity *i* draws `length × values[i] / max` (viewport units),
327
334
  `thickness` thick, growing from its position toward `direction` (`right`
@@ -334,14 +341,22 @@ with distance, removing pop-in at the cutoff.
334
341
  6); or `"points": [[-1, 0.9], [0.2, -1], [1, 0.4]]` — 3..24 unit
335
342
  coordinates in −1..1 scaled by the radius — for any facet (a Picasso shard,
336
343
  a Kandinsky triangle, a Mondrian plane). `soft` feathers the fill from the
337
- centre. Rotates with `spin`. `points` are paint, so two polygons with the
338
- same point count `morph` into each other
344
+ centre. Rotates with `spin`. The outline fills with the **nonzero** winding
345
+ rule: a self-crossing outline paints both lobes, and a hole needs its inner
346
+ loop traced in the **opposite** direction (a keyhole: outer ring clockwise,
347
+ then back round the hole counter-clockwise, joined by a zero-width slit). `points` are paint, so two polygons with the
348
+ same point count `morph` into each other — and a `setParam`, `timeline` key
349
+ or `morph` target for `sprite.points` is in the **same unit coordinates**,
350
+ scaled by each entity's seeded radius (not by the keyed shape's own extent):
351
+ keep every keyed point inside −1..1 (the validator rejects one outside), and
352
+ shrink a target shape by scaling its points, not by exceeding the box
339
353
  - `{ "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" }` —
340
354
  a freehand mark: a path through 2..24 unit-coordinate points, scaled so the
341
355
  unit box spans the seeded `length`, stroked `width` wide with round joins.
342
356
  `curve: "smooth"` (default) is a Catmull-Rom spline, `"linear"` a polyline;
343
357
  `taper` thins the mark to almost nothing at both ends (a brush stroke rather
344
- than a line); `orient: true` turns the path's +x along the entity's heading,
358
+ than a line; it paints as one filled mark, so a translucent stroke has no
359
+ overlap beads); `orient: true` turns the path's +x along the entity's heading,
345
360
  like `streak`. Rotates with `spin`. Van Gogh's stroke, Hokusai's contour,
346
361
  O'Keeffe's petal, Basquiat's scrawl
347
362
  - `{ "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 }` —
@@ -384,6 +399,12 @@ with distance, removing pop-in at the cutoff.
384
399
  advance so mono wraps land where the real face puts them; every other
385
400
  family uses the proportional table. Default `system-ui, sans-serif`.
386
401
  **Native clients fall back to the system face** until a face table exists.
402
+ `font` names faces **installed on the viewing device** — nothing is
403
+ bundled and no web font is fetched, so `'Inter'` only appears where Inter
404
+ is installed and every other screen falls through the stack. End every
405
+ stack in a generic `sans-serif`, `serif` or `monospace`; avoid ending on
406
+ `cursive` or `fantasy`, which resolve to a different (sometimes absurd)
407
+ face on every platform.
387
408
 
388
409
  `opacity` (0–1, default 1) multiplies the block's paint alpha — the whole
389
410
  block, on top of the layer's per-entity `alpha`. It is **paint**, not
@@ -479,7 +500,10 @@ and configured the second.
479
500
  - `{ "type": "wander", "speed": [0.004, 0.012], "angle": 0, "meander": 0.1, "coherence": 0.7 }`
480
501
  — organic harmonic drift: base velocity plus 3 seeded sine octaves per axis.
481
502
  Flowing curved paths with zero simulation state. `meander` scales the
482
- curvature; `coherence` 0..1 blends every entity toward a shared harmonic set
503
+ curvature (it is the harmonics' amplitude, so it also sets how fast an
504
+ entity sways — keep it ≤ ~0.015 for a slow drift); it is capped at
505
+ **0.463** (500 px ÷ `referenceViewport`, 1080 by default — 500 under
506
+ `units: "px"`), and the validator rejects anything above; `coherence` 0..1 blends every entity toward a shared harmonic set
483
507
  (1 = the layer undulates as one body — fake flocking)
484
508
  - `{ "type": "warp", "speed": [0.1, 0.5], "center": { "x": 0.5, "y": 0.5 } }`
485
509
  — perspective starfield: entities stream toward the viewer on a depth axis,
@@ -860,11 +884,14 @@ moves and scales its positions and sizes (sprite rotation is not modelled).
860
884
  its own × its group's — is 0 in the base scene and at every key boundary of
861
885
  its `timeline` never shows, so it is not judged and its entities do not count
862
886
  toward `dense-scene`; coverage is weighed by paint opacity) (also on
863
- `perceiveScene().advisories`). Two of them are **spatial**, for static text:
887
+ `perceiveScene().advisories`). Three of them are **spatial**, for static text:
864
888
  `text-off-screen` (a text/textBlock box crosses the viewport edge by more
865
- than 1%) and `text-overlap` (two static text layers share more than 10% of
889
+ than 1%), `text-overlap` (two static text layers share more than 10% of
866
890
  the smaller box; it carries both boxes as `boxes: [{x, y, w, h}]` in
867
- viewport fractions so the fix needs no re-derivation). Boxes come from the
891
+ viewport fractions so the fix needs no re-derivation) and
892
+ `layout-text-overlap` (two cells of one `list`/`table` layer do — its
893
+ `gap` pitch is narrower than its labels; the message names the `gap` that
894
+ clears them, and it carries the two boxes too). Boxes come from the
868
895
  same character-class width table the textBlock line-breaker uses, so they
869
896
  are estimates of the renderer's own layout — a caption that fails these
870
897
  will look wrong on the wall; one that passes may still sit a few px off.
package/dist/index.d.ts CHANGED
@@ -1379,7 +1379,8 @@ declare function applyDeltasToSpec(spec: SaverSpec, deltas: SteerDelta[]): Saver
1379
1379
  /**
1380
1380
  * Interpolate between two structurally-similar specs at progress k (0..1).
1381
1381
  * Numbers lerp, hex colours lerp per channel, everything else steps to the
1382
- * target immediately (k > 0). Layer counts round to integers.
1382
+ * target immediately (k > 0). Layer counts, and a field background's
1383
+ * `octaves` / `quantize`, round to whole values.
1383
1384
  */
1384
1385
  declare function lerpSpec(from: SaverSpec, to: SaverSpec, k: number): SaverSpec;
1385
1386
  /**