Deliberately a target rather than a description of where the library sits. Of the scenes measured since the region block went in, only Droste Feedback at 0.324 is comfortably over; Plasma Bloom is 0.082, Voronoi Shatter and Apollonian Gasket 0.062, Moiré Grid 0.047. Most of the library fails this, which is the point — a bar set where the work already is measures nothing. Third value in two days, and both previous moves were instrument corrections rather than changes of mind: the motion block was reading zero for the entire library, and adding the region block made the total a mean over six blocks instead of five. Both raised every score, so 0.04 had quietly drifted down relative to the thing it was judging. Documented as something to re-read off a fresh gallery whenever the descriptor changes, rather than a number to carry forward. The marker now sits at the middle of each meter and the grades land where they should: 0.047, 0.062 and 0.082 all red, 0.140 amber, 0.324 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
272 lines
12 KiB
Markdown
272 lines
12 KiB
Markdown
# How to build a visualizer that varies
|
||
|
||
`HOWTO-visualizers.md` covers making a scene that *works*. This one covers making
|
||
a scene that looks different from one song to the next — a separate skill, and
|
||
the one the library is currently worst at.
|
||
|
||
Everything here is a measurement rather than an opinion, and the numbers are
|
||
quoted so a future change can contradict them. Several already contradict things
|
||
believed earlier in the same week. Living document: add what you learn, and
|
||
delete what stops being true.
|
||
|
||
---
|
||
|
||
## The one-line version
|
||
|
||
> A scene varies when the SONG can change what is on screen. It does not vary
|
||
> because its parameters moved.
|
||
|
||
Parameters mostly change the same picture. Content changes the picture.
|
||
|
||
---
|
||
|
||
## How to know if you succeeded
|
||
|
||
```
|
||
gallery.html your scene, six times, on six songs — with a score
|
||
checks.html?scene=X the per-scene gate, including `consumes:` lines
|
||
```
|
||
|
||
The gallery score is the mean structural distance between a scene's own six
|
||
frames, on the same descriptor the variety harness uses. **Minimum bar: 0.1**
|
||
(`MIN_VARIETY` in `checks/gallery.js`), drawn as a red line. Below it a scene is
|
||
the same picture wherever it appears, and because it is cast into many songs
|
||
that sameness leaks between them — the viewer recognises the shot rather than
|
||
the song.
|
||
|
||
Most of the library does not clear it. That is deliberate: a bar set where the
|
||
work already is measures nothing.
|
||
|
||
Some reference points, measured:
|
||
|
||
```
|
||
0.324 Droste Feedback varies a lot
|
||
0.082 Plasma Bloom
|
||
0.062 Voronoi Shatter after a focus
|
||
0.062 Apollonian Gasket
|
||
0.047 Moiré Grid after a subject; 0.031 before
|
||
```
|
||
|
||
---
|
||
|
||
## What the descriptor can and cannot see
|
||
|
||
Design against this, because half of "why is my score low" is here.
|
||
|
||
**It is blind to:**
|
||
|
||
- **Brightness and contrast.** Frames are standardised to zero mean and unit
|
||
variance before anything is measured. A scene that only gets brighter has not
|
||
changed.
|
||
- **Colour.** Measured, reported, and excluded from the score. Six palettes
|
||
cannot disguise one image, which was the entire point.
|
||
- **Rotation**, on the scale, orientation and texture blocks.
|
||
- **Quality.** It measures change between songs, nothing else. Apollonian Gasket
|
||
scores 0.039 and looks great. Both facts are true and neither implies the
|
||
other.
|
||
|
||
**It sees:**
|
||
|
||
| block | what it measures | how to move it |
|
||
|---|---|---|
|
||
| `scale` | feature size — fine grain vs large forms | let the song set element size (`stageScale()`) |
|
||
| `orient` | grid vs radial vs stripes, rotation-blind | break or vary a regular lattice |
|
||
| `layout` | where in the frame structure sits | move the subject, change what is empty |
|
||
| `texture` | element count, sparsity, mirror and radial symmetry | vary how many things there are |
|
||
| `region` | how differently the parts of the frame behave from each other | give the frame a subject, so one region is unlike its neighbours |
|
||
| `motion` | what moves and where, not how much | vary the KIND of movement, not the rate |
|
||
|
||
**`region` is the block that rewards having something to look at.** Layout says
|
||
where the energy is, normalised, so a uniform field and a field with a subject in
|
||
it can normalise to nearly the same answer. Region characterises each part of the
|
||
frame in its own right — detail, direction, element count — and reports how far
|
||
each deviates from the frame's average. A pattern spread evenly deviates by
|
||
nothing everywhere, which is exactly why there is nothing to watch.
|
||
|
||
Measured when it was added: Apollonian Gasket 0.039 to 0.062 and Plasma Bloom to
|
||
0.082, both on region alone, because both have a subject the old blocks were not
|
||
crediting. It is the strongest single block in the descriptor.
|
||
|
||
**A known blind spot.** `layout` is nearly useless for full-frame fields. Voronoi
|
||
Shatter measures 0.004 there no matter what changes, because edge-to-edge cells
|
||
occupy the same frame however they fall. That is honest — there genuinely is no
|
||
arrangement — but it means a whole family is judged on four blocks instead of
|
||
five. If your scene fills the frame corner to corner, expect to earn your score
|
||
on `orient` and `texture`.
|
||
|
||
---
|
||
|
||
## What has actually worked
|
||
|
||
Ordered by measured effect.
|
||
|
||
### 1. Draw the song's content instead of your own
|
||
|
||
The largest single lever. Take `consumes: ['cast', 'ink', 'staging']` and draw
|
||
`castMain`/`castChorus` where you would have drawn your own primitive. Recipe in
|
||
`MIGRATION.md`.
|
||
|
||
Measured: identity is worth 117% of what the container is worth
|
||
(`checks.html?decompose=1`) — swapping the song's cast under a fixed scene moves
|
||
the picture more than swapping the scene under a fixed cast. A migrated library
|
||
scene also carries the identity better than a stage written from scratch to
|
||
carry it, which was a surprise and is why the whole library was migrated rather
|
||
than replaced.
|
||
|
||
### 2. Let the song decide element SIZE
|
||
|
||
`stageScale()`. Not the size of your features relative to each other — the size
|
||
of the whole vocabulary. A song of six huge forms and a song of four hundred
|
||
tiny ones are different videos before anything else is decided.
|
||
|
||
Measured: adding `elementScale` to the identity moved stages from
|
||
+0.0111 ±0.0043 to +0.0143 ±0.0008, and the `scale` block from 0.033 to 0.041.
|
||
|
||
### 3. Displace a regular lattice
|
||
|
||
If your scene is a grid, let the song push things off it. Bounded — past about
|
||
half a cell the structure the grid was providing stops reading.
|
||
|
||
Measured: Isometric Blocks 0.034 → 0.049, with `orient` nearly doubling from
|
||
0.093 to 0.164 as the rigid lattice softened.
|
||
|
||
### 4. Give a full-frame field somewhere to be about
|
||
|
||
`focusWarp(p, amount)` and `focusField(p)`. The song picks one to three focal
|
||
points; tile in the warped coordinate so cells crowd toward them or pull away.
|
||
|
||
Measured: Voronoi Shatter 0.0463 → 0.0588. **Note what moved:** `orient` 0.140 →
|
||
0.194, while `layout` went 0.004 → 0.007, i.e. nowhere. Warping where cells sit
|
||
changes what they look like without changing where the frame's energy is. The
|
||
gain is real; the stated reason for it was wrong.
|
||
|
||
---
|
||
|
||
## What has measurably NOT worked
|
||
|
||
Recorded because the failures were more informative than the wins, and because
|
||
each of these looked obviously right beforehand.
|
||
|
||
### Effects as parameters
|
||
|
||
Forty-nine scenes had their own `glow`; every one had its own grain. An effect
|
||
knob sampled per song makes a scene look varied across parameter draws while its
|
||
structure never moves — it inflates the score without changing the picture, and
|
||
it fights the grade, which already does bloom, grain, chroma and vignette with
|
||
an envelope the scene cannot see.
|
||
|
||
**Do not add glow, bloom, grain, haze, chroma or trails to a scene.** The post
|
||
chain owns them. `npm run lint:scenes` does not catch this yet; reviewers should.
|
||
|
||
### Concentrating disturbance instead of adding it
|
||
|
||
Isometric Blocks, twice. Spending the scatter budget near the focal points and
|
||
calming the rest measured 0.049 → 0.0374 — most of the field went back onto the
|
||
rigid lattice and took the orientation variety with it. Making the focus
|
||
additive instead recovered nothing: 0.0365.
|
||
|
||
Two plausible diagnoses, both wrong. The lesson is narrow and worth keeping:
|
||
**a focal point has to be something the field gains, not something the rest of it
|
||
pays for** — and even that framing did not rescue it, so the real cause is still
|
||
unknown.
|
||
|
||
### Making the roster smaller
|
||
|
||
A whole afternoon went into the theory that a track drawing on fewer scenes
|
||
would look more like itself. Swept directly across pool sizes 4, 8, 16 and 32
|
||
over twelve songs with three draws each: differences of 0.003 to 0.007 against a
|
||
run-to-run noise of ±0.003 to ±0.005. **Nothing.** The theory came from an
|
||
experiment whose arms differed in two ways at once.
|
||
|
||
### Reading a form as a metric rather than drawing it
|
||
|
||
Spectrum Sculpture used `sigShape` as a radial distance rather than as a subject.
|
||
Migrating it to `castMain` rendered pure black, because the cast carries notches
|
||
and a hollow and an annulus used as a radius turns a sculpture inside out. If
|
||
your scene consults a form's geometry rather than drawing it, it wants
|
||
`sigShape`, and the cast is not for you.
|
||
|
||
---
|
||
|
||
## Scene shapes, and how hard each is to vary
|
||
|
||
- **Element-placing** (`metaballs`, `floating-geometry`, `firefly-drift`) —
|
||
easiest. Take the cast, place on `stageNode`, keep your motion. Nearly all the
|
||
wins above are this shape.
|
||
- **Single figure** (`slow-orb`, `eclipse-field`) — take the cast and
|
||
`stageScale()`. Composition is yours to vary; most such scenes centre their
|
||
subject and never move it, which is free `layout` left on the table.
|
||
- **Full-frame field** (`voronoi-shatter`, `plasma-bloom`, `curl-flow`) — hard.
|
||
`layout` is unavailable to you. Earn it on `orient` and `texture`, and consider
|
||
a focus.
|
||
- **Fixed geometry** (`truchet-fold`, `quasicrystal`, `apollonian-gasket`) —
|
||
hardest, and currently unsolved. The image *is* the maths, so the cast cannot
|
||
be pasted on top of it. The open idea is to make the cast the repeating UNIT
|
||
the tiling is built from, which is a real rewrite per scene rather than a
|
||
recipe step. Nobody has tried it yet.
|
||
- **Treatment** (`analog-wow`, `scan-tear`, `halftone-misprint`) — these are
|
||
effects wearing a scene's clothes. They score 0.031–0.040 and their whole
|
||
migration was one posterisation. They probably belong in the identity's
|
||
effects register rather than competing for screen time as subjects.
|
||
|
||
---
|
||
|
||
## Trusting the numbers
|
||
|
||
This harness has two classes of measurement and only one is safe to steer by.
|
||
|
||
**Direct render comparisons** — the gallery score, `checks.html?decompose=1`, the
|
||
per-scene gate — compare rendered frames to each other. Noise floor of exactly
|
||
zero. Trust these.
|
||
|
||
**Aggregate ratios** — chiefly `separation`, which divides one small difference
|
||
by another — swing wildly. Three runs of the same post-migration measurement gave
|
||
0.31, 0.56 and 0.07. **Four conclusions were drawn and withdrawn during this epic
|
||
for exactly that reason.** Anything under about 0.01 of spread needs
|
||
`&repeats=3` before it is believed, and a difference that changes sign with the
|
||
sample size is not a difference.
|
||
|
||
Two more traps, both of which have bitten:
|
||
|
||
- **A score of exactly 0.000 is a dead shader, not a boring scene.** The gallery
|
||
now says so outright — the row goes purple and carries the luminance and
|
||
variance — but the underlying trap is general: a broken render produces the
|
||
most boring possible numbers rather than an error.
|
||
- **A permanently-zero block looks like a property of your scene.** The gallery
|
||
built four of the descriptor's five blocks for weeks; `motion` came back 0.000
|
||
for every scene and depressed every score by a fifth. If a block is identical
|
||
across many unrelated scenes, suspect the instrument.
|
||
- **Arms that differ in more than one way cannot be read as if they differed in
|
||
one.** This produced the roster-size result above, and it was believed for a
|
||
day.
|
||
|
||
---
|
||
|
||
## Checklist for a new or reworked scene
|
||
|
||
1. `consumes` declared, and the `consumes:` lines pass in
|
||
`checks.html?scene=<name>`. A declared artifact that is ignored is worse than
|
||
one not declared, because the casting code believes it.
|
||
2. No glow, bloom, grain, haze, chroma or trail parameters.
|
||
3. Element size goes through `stageScale()`.
|
||
4. Placement goes through `stageNode()` unless the composition IS the scene.
|
||
5. Gallery score above 0.1, checked before and after.
|
||
6. The rest of the per-scene battery still passes — especially `distinct`. The
|
||
more scenes share a cast, the easier it is to become one of your neighbours.
|
||
|
||
---
|
||
|
||
## Open questions
|
||
|
||
- **Coupling is zero and has never moved.** Whether a song *looks* different in
|
||
proportion to how it *sounds* different measures -0.02 ±0.03. Every change in
|
||
this epic left it there. Nobody has attacked it directly.
|
||
- **Fixed-geometry scenes** have no known route to variety.
|
||
- **The `layout` blindness** for full-frame fields would need a new descriptor
|
||
block measuring cell statistics rather than where structure sits.
|
||
- **The bar is a target, not a description.** Most of the library does not clear
|
||
0.1 — that is the point of it. It has moved from 0.04 to 0.05 to 0.1
|
||
as the descriptor gained a block and lost a bug, both of which raised every
|
||
score. Expect to move it again whenever the descriptor changes, and re-read it
|
||
off a fresh gallery rather than carrying the old number forward.
|