@idle-screens/schema 3.5.0 → 3.6.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
@@ -32,7 +32,11 @@ sprites, `colorWeights`, `pulse.wave`, `layout` (grid), `life`,
32
32
  `links.mode/falloff/closed`, `blend: screen|multiply`, orbit layer-parents
33
33
  (2026-07-21 — "the v1 ceiling");
34
34
  `textBlock` sprite — deterministic multi-line text with viewport-unit sizing;
35
- `textBlock.reveal` — animated typing/deleting via one steerable paint param.
35
+ `textBlock.reveal` — animated typing/deleting via one steerable paint param;
36
+ `emit` (sparse events), `clock` (phase-lock), `motion.ease` (settle / buoyant)
37
+ (2026-09-05 — time structure); `polygon` / `stroke` sprites and `rect.feather`
38
+ (2026-09-05 — shape glyphs); `layout: list | table` and the `bar` sprite
39
+ (2026-09-05 — data layout).
36
40
 
37
41
  ## Safety invariants
38
42
 
@@ -48,7 +52,10 @@ These hold **by construction** — no spec can violate them:
48
52
  so a layer will generally not pulse in unison (edge case: if spawn positions
49
53
  alias the wavelength, phases may coincide). `ghosting` only *smooths* luminance
50
54
  changes (it composites frames over a faded copy of the last), never sharpens
51
- them.
55
+ them. `clock` deliberately removes the per-entity phase, so the validator
56
+ floors a clocked layer's periods at 1000 ms (1 Hz) instead — a layer in
57
+ unison breathes, it never flashes. `emit` events are floored at one per
58
+ second per entity and always ride a smooth envelope, never a cut.
52
59
  3. **Motion is bounded.** Speeds are capped (4000 px/sec; warp at 1.5
53
60
  depth-units/sec; orbit at 180 deg/sec; path laps at ≥ 2 s).
54
61
  4. **Work is bounded.** ≤ 36 layers, ≤ 400 entities per layer, ≤ 800 total,
@@ -72,7 +79,10 @@ Author against the JSON Schema; ship through the runtime validator.
72
79
 
73
80
  Rules the JSON Schema cannot fully express (the runtime enforces them):
74
81
  total entities across all layers ≤ 800; every `[min, max]` range must satisfy
75
- `min ≤ max`; `colorWeights` length must match `colors`; orbit layer-parents
82
+ `min ≤ max`; `colorWeights` length must match `colors`; `emit.life ≤
83
+ emit.every`; a `clock`ed layer's `pulse`/`grow`/`cycle` period must satisfy
84
+ `period / rate ≥ 1000`; a `polygon` takes `sides` or `points`, not both (the
85
+ JSON Schema enforces this one too); orbit layer-parents
76
86
  must exist, have `count: 1`, and not themselves orbit a layer.
77
87
 
78
88
  ## Structure
@@ -84,6 +94,7 @@ must exist, have `count: 1`, and not themselves orbit a layer.
84
94
  "label": "Snowfall",
85
95
  "seed": 42, // optional; falls back to the host's seed
86
96
  "motionIntensity": "calm", // optional: calm | moderate | energetic
97
+ "density": "normal", // optional: sparse | normal | dense — declared intent, see below
87
98
  "units": "viewport", // optional: viewport (default) | px
88
99
  "referenceViewport": 1080, // optional; design resolution for density scaling
89
100
  "ghosting": 0.9, // optional 0..0.95; frame-persistence smear
@@ -94,14 +105,26 @@ must exist, have `count: 1`, and not themselves orbit a layer.
94
105
 
95
106
  With `units: "viewport"` (the default) every dimensional value — sizes, radii,
96
107
  speeds, distances, wavelengths, stroke widths — is a fraction of
97
- `min(width, height)`, so specs scale to any display. Exception: text sprites
98
- with an explicit `px` size in their `font` string (e.g. `"bold 14px monospace"`)
99
- use that CSS font verbatim and do not scale with the viewport.
108
+ `min(width, height)`, so specs scale to any display. Text sprites with an
109
+ explicit `px` size in their `font` string (e.g. `"bold 14px monospace"`) are no
110
+ exception: the compiler rescales that number by
111
+ `min(width, height) / referenceViewport` so the text holds its apparent size,
112
+ and only `units: "px"` specs get it verbatim. See **Scale** below.
100
113
 
101
114
  `ghosting` paints each frame over a faded copy of the previous one instead of
102
115
  clearing: moving entities leave decaying after-images (Mystify smears, Matrix
103
116
  trails, long-exposure light). 0.85–0.95 is the useful range; 0 (default) is off.
104
117
 
118
+ `density` declares what the emptiness or the crowding means. `sparse` says
119
+ the scene is *meant* to be almost empty — one faint mark on a dark ground,
120
+ long silences — so `adviseSpec` withholds `sparse-scene` and a scorer that
121
+ gates on coverage should read the declaration before calling a faithful
122
+ scene broken. `dense` withholds `dense-scene` the same way. The declaration
123
+ is checked, not trusted: a `sparse` scene whose alpha-weighted coverage
124
+ exceeds 2 %, or a `dense` one that would have tripped `sparse-scene`, gets a
125
+ `density-mismatch` advisory instead. Omitted means `normal`. Like
126
+ `motionIntensity`, it is a hint — it changes no pixel.
127
+
105
128
  ### `background`
106
129
 
107
130
  - `{ "type": "solid", "color": "#06060c" }`
@@ -129,10 +152,12 @@ trails, long-exposure light). 0.85–0.95 is the useful range; 0 (default) is of
129
152
  | `grow` | `{amp ≤ 0.8, period ≥ 500}` | none | size breathing (seeded phase) |
130
153
  | `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) |
131
154
  | `links` | see below | none | inter-entity lines |
132
- | `layout` | `{type: "grid", columns?, jitter?}` | scatter | grid placement; `jitter` scalar or `{x?, y?}` 0..1 per axis |
155
+ | `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 |
133
156
  | `life` | `{enter?, exit?, fade?}` ms | always on | act structure: fade the layer in at `enter`, out at `exit` |
157
+ | `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 |
158
+ | `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` |
134
159
  | `key` | string | none | addressable name → `setParam("key.field", …)` |
135
- | `position` | `{x, y}` 0..1 | none | exact placement; **requires `count: 1`**; overrides `region`/`layout` |
160
+ | `position` | `{x, y}` 0..1 | none | exact placement; **requires `count: 1`** — except with a `list`/`table` layout, where it anchors the block's top-left; overrides `region` |
136
161
 
137
162
  `links`: `{ k: 1..8, maxDist, color?, alpha?, width?, mode?, falloff?, closed? }`.
138
163
  `mode: "nearest"` (default) wires each entity to its k nearest neighbors within
@@ -146,7 +171,8 @@ with distance, removing pop-in at the cutoff.
146
171
  - `{ "kind": "emoji", "glyphs": ["🐟", "🐠"], "cycle": { "period": 800 } }` —
147
172
  glyph picked per entity (seeded); optional `cycle` rotates variants over time
148
173
  - `{ "kind": "text", "strings": ["HELLO"], "color": "#e6e8ef", "font": "bold monospace", "align": "center", "baseline": "middle", "maxWidth": 300, "cycle": ... }`
149
- — a `font` **with** a px size is used verbatim; a family/weight only
174
+ — a `font` **with** a px size keeps that shorthand, its number rescaled to the
175
+ viewport (verbatim only under `units: "px"`); a family/weight only
150
176
  (`"bold monospace"`) composes with the seeded per-entity `size`
151
177
  - `{ "kind": "circle", "radius": [0.001, 0.003], "color": "#ffffff", "soft": true }` —
152
178
  `soft` renders a radial-falloff glow orb instead of a hard disc
@@ -155,8 +181,31 @@ with distance, removing pop-in at the cutoff.
155
181
  - `{ "kind": "streak", "length": [0.01, 0.03], "color": "#cfd8ff", "width": 0.002 }` —
156
182
  a line oriented along the entity's analytic heading with a faded tail
157
183
  (rain that reads as rain, shooting stars, warp stars)
158
- - `{ "kind": "rect", "width": [0.012, 0.02], "aspect": [1.3, 1.7], "color": "#ffb347" }` —
159
- rectangle; `aspect` is the height/width ratio range (rotates with `spin`)
184
+ - `{ "kind": "rect", "width": [0.012, 0.02], "aspect": [1.3, 1.7], "color": "#ffb347", "feather": 0.6 }` —
185
+ rectangle; `aspect` is the height/width ratio range (rotates with `spin`);
186
+ `feather` 0..1 softens the edges — that fraction of the half-size fades out
187
+ toward the border (Rothko's block at 0.5–0.8)
188
+ - `{ "kind": "bar", "values": [82, 64, 91], "max": 100, "length": 0.5, "thickness": 0.02, "color": "#17e8c8", "direction": "right" }` —
189
+ a data bar: entity *i* draws `length × values[i] / max` (viewport units),
190
+ `thickness` thick, growing from its position toward `direction` (`right`
191
+ default, `left`, `up`, `down`). `values` are **paint**: `setParam("bars.values", […])`
192
+ glides every bar. With `layout: { type: "list" }` a chart is one layer and
193
+ its labels another — the dashboard genre stops needing one layer per number
194
+ - `{ "kind": "polygon", "radius": [0.02, 0.05], "sides": 3, "color": "#f2e8c9", "soft": false }` —
195
+ regular n-gon of the seeded circumradius, point up (`sides` 3..12, default
196
+ 6); or `"points": [[-1, 0.9], [0.2, -1], [1, 0.4]]` — 3..24 unit
197
+ coordinates in −1..1 scaled by the radius — for any facet (a Picasso shard,
198
+ a Kandinsky triangle, a Mondrian plane). `soft` feathers the fill from the
199
+ centre. Rotates with `spin`. `points` are paint, so two polygons with the
200
+ same point count `morph` into each other
201
+ - `{ "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" }` —
202
+ a freehand mark: a path through 2..24 unit-coordinate points, scaled so the
203
+ unit box spans the seeded `length`, stroked `width` wide with round joins.
204
+ `curve: "smooth"` (default) is a Catmull-Rom spline, `"linear"` a polyline;
205
+ `taper` thins the mark to almost nothing at both ends (a brush stroke rather
206
+ than a line); `orient: true` turns the path's +x along the entity's heading,
207
+ like `streak`. Rotates with `spin`. Van Gogh's stroke, Hokusai's contour,
208
+ O'Keeffe's petal, Basquiat's scrawl
160
209
  - `{ "kind": "textBlock", "text": "Multi-line text with wrapping.", "maxWidth": 0.8, "fontSize": 0.04, "lineHeight": 1.4, "align": "left", "color": "#e6e8ef" }` —
161
210
  multi-line text block with deterministic line-breaking. All dimensions are
162
211
  viewport fractions (of `min(w,h)`), not px — `units: "px"` specs reject
@@ -196,9 +245,20 @@ with distance, removing pop-in at the cutoff.
196
245
  glide `reveal.progress` to 0, swap `text` while nothing is visible, glide
197
246
  back to 1.
198
247
 
199
- `circle`, `ring`, `streak`, and `rect` all accept `colors: [...]` (seeded
200
- per-entity palette pick) and `colorWeights: [...]` (relative weights, same
201
- length — "mostly cool tones, occasional ember").
248
+ `circle`, `ring`, `streak`, `rect`, `bar`, `polygon` and `stroke` all accept
249
+ `colors: [...]` (seeded per-entity palette pick) and `colorWeights: [...]`
250
+ (relative weights, same length — "mostly cool tones, occasional ember").
251
+
252
+ **Drawing a shape out of sprites.** Overlapping sprites merge into one seamless
253
+ silhouette under exactly one set of conditions: a single flat colour, `alpha`
254
+ of `[1, 1]`, no `blend`, no `pulse`, and hard edges (no `soft`, no
255
+ `rect.feather`). Anything else leaves every overlap visible as an internal edge,
256
+ so the layer reads as a pile of sprites rather than a form — `soft` in
257
+ particular reads as smoke, not mass. Depth then comes from *layer order* and a
258
+ different flat colour per layer, painting back to front, and a later layer can
259
+ be used to cut a clean edge across an earlier one. `layerCohesion` measures
260
+ which of the two you got; `overlap-seams` warns when you asked for the first
261
+ and configured the second.
202
262
 
203
263
  ### `motion` (one of)
204
264
 
@@ -228,6 +288,154 @@ length — "mostly cool tones, occasional ember").
228
288
 
229
289
  All speeds are ranges; each entity draws its own value (seeded).
230
290
 
291
+ `drift`, `rise` and `wander` also accept **`ease`** — closed-form velocity
292
+ shaping, no integration state:
293
+ `{ "ease": { "type": "settle", "tau": 2500 } }` starts at the entity's speed
294
+ and decelerates to rest with time constant `tau` ms (it travels `speed × tau`
295
+ and stops — a mark flung and coming to rest); `"buoyant"` starts at rest and
296
+ approaches the speed over `tau` (a bubble reaching terminal velocity). With
297
+ `emit`, the eased travel restarts from the spawn point on every event. For
298
+ `wander` only the base velocity eases — the harmonic meander keeps breathing,
299
+ so a settled wanderer hovers rather than freezes.
300
+
301
+ **Time structure, composed.** `emit` is how a scene does *almost nothing,
302
+ almost never*: one ring every twelve seconds that expands and fades
303
+ (`emit: { every: 12000, life: 5000, jitter: 0, grow: [0.15, 2.4] }`) reads as
304
+ an event, where the same ring pulsing forever reads as wallpaper. Long
305
+ `every`, low `count`, `jitter: 0` for a metronome, `1` for weather; pair with
306
+ `ease: settle` so a mark is flung and comes to rest before it fades ("expand,
307
+ drift a little, settle, fade" is one layer). `clock` is the other half: two
308
+ layers on the same clock are a duet, not a coincidence. The shipped `pings`
309
+ example is the whole idea in two layers.
310
+ ## Scale — what the numbers actually look like
311
+
312
+ Every dimensional value is a fraction of `min(width, height)`. On a 1920×1080
313
+ display that divisor is **1080**, so `0.01` is 10.8 px there and the same
314
+ fraction of the short side on a phone or a 4K wall. Nothing below is a rule —
315
+ they are measured landmarks, because the most common authoring failure is not
316
+ a bad idea, it is an idea rendered two orders of magnitude too small or too
317
+ large to see. The px conversions and coverage figures are measured
318
+ (`perceiveScene` at 1920×1080, 40 opaque white entities on black, unless a row
319
+ says otherwise); the "reads as" readings and the recipe advice are judgment.
320
+
321
+ ### `circle.radius` (a **radius**, so a dot is twice this wide)
322
+
323
+ | radius | px radius @1080p | px across | coverage (40) | reads as |
324
+ |---|---|---|---|---|
325
+ | `0.0005` | 0.5 | 1 | 0.9 % | star grain — texture, never the subject |
326
+ | `0.002` | 2.2 | 4 | 1.0 % | dust, embers, distant snow |
327
+ | `0.005` | 5.4 | 11 | 1.6 % | a distinct dot; graph node, firefly |
328
+ | `0.02` | 21.6 | 43 | 6.2 % | a clear disc — bokeh, a planet |
329
+ | `0.05` | 54 | 108 | 21 % | a focal object, or a soft glow field |
330
+ | `0.1` | 108 | 216 | 52 % | dominant form — sun, moon, one per scene |
331
+ | `0.2` | 216 | 432 | 88 % | 40 % of the short side; only as a soft glow plate |
332
+
333
+ **Read the coverage column with its floor in mind.** `perceiveScene` samples an
334
+ 80 × 48 grid, so a cell is ~24 × 22 px at 1080p and anything smaller registers as
335
+ one cell per entity. That is why the top two rungs both land near 1 % despite a
336
+ 16 × difference in area — below `radius ≈ 0.01` the column counts entities, not
337
+ pixels. It becomes an area measurement from `0.02` up.
338
+
339
+ The shipped examples put entity *fields* almost entirely in `0.0003 – 0.007`
340
+ and large forms in `0.03 – 0.185` (the biggest is a `ring` gauge in
341
+ `dev-dashboard`). The gap between is thinly populated
342
+ but not empty, and the exception is instructive: `aurora` runs 60–100-entity
343
+ layers at `radius [0.045, 0.11]`, because a soft additive orb that large is
344
+ atmosphere rather than an object. Within one scene, `lanterns`' three parallax
345
+ layers step by ~2.2 × and ~2.4 × per layer; steps much smaller than that read
346
+ as one layer with a wide size range rather than as depth.
347
+
348
+ The bottom two rungs are texture, not content, and `adviseSpec` says so: a
349
+ `0.002` field stops raising `sparse-scene` at ~70 entities, while a `0.0005`
350
+ field would need over a thousand — far past `LIMITS.maxPerLayer` (400) — so it
351
+ raises `sparse-scene` at every legal count and always needs a brighter layer
352
+ above it.
353
+
354
+ ### `size` (emoji), `fontSize` (textBlock), `size` (text sprites)
355
+
356
+ `0.02` ≈ 22 px, `0.035` ≈ 38 px, `0.05` ≈ 54 px, `0.1` ≈ 108 px at 1080p.
357
+ Emoji fields in the examples sit at `0.011 – 0.078`; `haiku`, the one textBlock
358
+ example, sets `fontSize: 0.05`.
359
+
360
+ An explicit px size inside a `font` string (`"bold 14px monospace"`) is **not**
361
+ absolute: under the default `viewport` units the compiler rescales it by
362
+ `min(w, h) / referenceViewport`, so it holds its apparent size across displays
363
+ (14 px at 1080p becomes 28 px at 4K and 5 px on a phone). Only `units: "px"`
364
+ specs get the number verbatim. Two consequences: a font pinned for a 4K wall
365
+ becomes illegibly small on a phone-sized viewport, and a spec that genuinely
366
+ wants fixed pixels must say `units: "px"`.
367
+
368
+ ### `speed` (viewport fractions per second)
369
+
370
+ | speed | px/s @1080p | crosses the short side | crosses a 16:9 width |
371
+ |---|---|---|---|
372
+ | `0.001` | 1.1 | 17 min | 30 min |
373
+ | `0.005` | 5.4 | 3.3 min | 5.9 min |
374
+ | `0.02` | 21.6 | 50 s | 89 s |
375
+ | `0.06` | 65 | 17 s | 30 s |
376
+ | `0.15` | 162 | 7 s | 12 s |
377
+
378
+ An ambient scene meant to live on a wall for hours wants its background layers
379
+ under `0.005` — anything faster resolves as traffic rather than atmosphere.
380
+ Above `0.06` a layer reads as an event (rain, warp, a passing comet). Speeds
381
+ are ranges and each entity draws its own, so a `[0.002, 0.008]` layer already
382
+ carries internal parallax.
383
+
384
+ ### `links.maxDist`
385
+
386
+ Link density is governed by `maxDist` relative to the mean spacing between
387
+ entities, `sqrt(w·h / count) / min(w, h)` — for 40 entities at 1920×1080 that
388
+ is `0.21` (228 px), for 100 it is `0.13` (144 px). Measured at `k: 3`, count 40,
389
+ averaged over 15 seeds:
390
+
391
+ | `maxDist` | edges drawn (of `k·count`) | graph |
392
+ |---|---|---|
393
+ | 0.5 × spacing | ~12 % | scattered pairs; ~45 % of nodes isolated |
394
+ | 1.0 × spacing | ~45 % | a constellation with a few loners |
395
+ | 1.5 × spacing | ~60 % | usually one connected component, rarely an isolate |
396
+ | 2.0 × spacing | ~62 % | saturated — `k` is now the binding cap |
397
+
398
+ Past ~2 × spacing, raising `maxDist` costs distance tests and buys nothing;
399
+ raise `k` instead. `describeScene` reports `linksDrawn` / `linksExpected` and
400
+ `isolatedNodes`, so connectivity is checkable per spec rather than guessed —
401
+ worth doing, since the seed moves these by several points either way.
402
+
403
+ ### Recipes
404
+
405
+ **Parallax depth.** Three or four layers where size and speed rise together —
406
+ scaling size alone reads as a size distribution, not as distance. `lanterns`'
407
+ ladder, which the numbers above are drawn from: `radius [0.0005, 0.0013]` /
408
+ `speed [0.0005, 0.002]` / `alpha [0.35, 1]` far, `radius [0.0014, 0.0028]` /
409
+ `speed [0.006, 0.013]` / `alpha [0.5, 0.9]` mid, `radius [0.0032, 0.006]` /
410
+ `speed [0.015, 0.026]` / `alpha [0.6, 1]` near. Note that alpha *narrows*
411
+ toward the front rather than simply rising — the far layer spans the whole
412
+ range because distance is what varies it; the near layer is uniformly present.
413
+
414
+ **Glow stacking.** `soft: true` + `blend: "lighter"` + `alpha` around 0.5–0.9,
415
+ with `pulse: { amp: 0.2, period: 3000 }` for breathing. Glow reads only against
416
+ a dark plate — over a pale background use `"screen"`. Do not assume a soft orb
417
+ covers what a hard disc of the same radius would: `perceiveScene` models the
418
+ halo as reaching 2.4 × the radius, so a large soft additive layer measures far
419
+ *more* coverage than its hard equivalent — ~2.5 × at `radius 0.05` — while a
420
+ small one measures *less*, ~0.5 × at `radius 0.005`, because the faint outer
421
+ halo falls under the per-cell visibility threshold. The crossover sits near
422
+ `radius 0.01`. Re-measure after switching `soft` or `blend` rather than
423
+ compensating by a rule of thumb.
424
+
425
+ **Graph web.** `count 40–100`, `radius [0.002, 0.005]`, a slow `drift`
426
+ (`speed [0.002, 0.007]`) so the topology keeps re-forming, and `links` with
427
+ `k: 3`, `maxDist` at 1.5 × mean spacing (`0.3` for 40 entities, `0.2` for 100),
428
+ `alpha: 0.15`, `width: 0.0005`. Link alpha wants to be much lower than the
429
+ nodes' — at 0.15 the web is a suggestion, at 0.5 it is a diagram. Add
430
+ `falloff: true` to fade edges toward the cutoff instead of popping them.
431
+
432
+ **Focal pin.** One `count: 1` layer with `motion: { type: "static" }` and an
433
+ explicit `position`, `radius` in the `0.05 – 0.15` band, over a field of
434
+ `0.002`-scale entities. The size ratio between the two is what makes it read as
435
+ a subject rather than the largest member of the crowd — an order of magnitude
436
+ or more, which is the gap the ladder above shows between the field rungs and
437
+ the focal ones.
438
+
231
439
  ## Determinism contract
232
440
 
233
441
  Entity construction consumes the seeded RNG in a **fixed draw order** per layer.
@@ -293,6 +501,17 @@ trade-offs for a zero-dependency, renderer-free analysis tool.
293
501
  line-salience boost so they aren't crushed by filled discs.
294
502
  - `motionStats(spec, opts)` — per-layer mean/max on-screen speed from analytic
295
503
  displacement — choreography as numbers.
504
+ - `layerCohesion(spec, opts)` — **does a layer read as one form or as N marks?**
505
+ The luminance maps measure ink, not edges, so a layer whose sprites merged
506
+ into a silhouette and one that stayed a pile of discs are identical in every
507
+ other channel. Two independent facts, both reported (also on
508
+ `perceiveScene().form`): `overlap` is geometry — the mean fraction of an
509
+ entity's outline buried inside a sibling (shipped particle fields measure
510
+ ≤ 0.154; a packed silhouette measures 0.79–0.85; `null` for lines, glyphs and
511
+ text, where a merged silhouette is not a meaningful idea). `seamless` is
512
+ paint — whether those overlaps vanish or draw an internal edge, with
513
+ `seamCause` naming the field to change. `reads` combines them into `mass`,
514
+ `seamed` or `marks`.
296
515
  - `textSprites(spec, opts)` — the literal strings and rendered sizes of every
297
516
  text layer. Glyphs are invisible in the luminance maps, so this is the only
298
517
  way to confirm *what words* are on screen and how big. (Also on
@@ -305,6 +524,16 @@ trade-offs for a zero-dependency, renderer-free analysis tool.
305
524
  textBlock line-breaker uses, so they are estimates of the renderer's own
306
525
  layout — a caption that fails these will look wrong on the wall; one that
307
526
  passes may still sit a few px off.
527
+ `overlap-seams` catches the trap the other channels are blind to: a layer
528
+ whose entities bury each other (so it was drawn as one shape) but whose paint
529
+ settings make every overlap visible. It is gated to stay off deliberate
530
+ washes — it needs at least 6 entities and a mean base alpha of 0.45, and
531
+ never fires under `blend: lighter` / `screen`. No shipped example trips it.
532
+ The two density advisories read the spec's declared `density` first:
533
+ `sparse-scene` is withheld under `density: "sparse"` and `dense-scene`
534
+ under `"dense"`, and `density-mismatch` fires instead when the measured
535
+ coverage contradicts the declaration (a "sparse" scene covering more than
536
+ 2 % of the frame, a "dense" one that would have read as empty).
308
537
  - `diffScenes(a, b, opts)` — **relative sight**: coverage/luminance deltas,
309
538
  visual-balance shift, 3×3 region deltas, dominance-rank movement, and
310
539
  advisory codes added/removed. Agents judge "is B better than A" far more
@@ -400,6 +629,9 @@ Shipped working specs (also exposed as `EXAMPLE_SPECS` /
400
629
  showcases — `aurora` (wander + coherence + ghosting + pulse.wave),
401
630
  `warp-tunnel` (warp + streaks), `polygons` (chain links + heavy ghosting),
402
631
  `matrix-rain` (grid layout + glyph cycle + ghosting), and `procession`
403
- (path + layer-parented orbit + life staging + ring/rect sprites). See
404
- [`src/examples/`](./src/examples/). The dashboard exercises the static/HUD
405
- subset at scale (34 layers of keyed, positioned text).
632
+ (path + layer-parented orbit + life staging + ring/rect sprites);
633
+ `nostalghia-candle` and `haiku` (restraint and text); and the 2026-09 trio —
634
+ `pings` (emit + grow + ease: one event at a time), `facets` (polygon, stroke
635
+ and feathered rect) and `relay-board` (list layout + bar: a chart in five
636
+ layers). See [`src/examples/`](./src/examples/). The dashboard exercises the
637
+ static/HUD subset at scale (34 layers of keyed, positioned text).