Commit Graph
114 Commits
Author SHA1 Message Date
Dejvino 3b65ea905c Chamber: procedural junk furniture instead of actor-part memories
Tables, chairs, cupboards, shelves with seeded book rows, potted plants
and box stacks built from primitives, dealt from a shuffled deck per
(seed, arrangement) so the first slots always land distinct types. Same
spiral + repulsion placement with per-item footprint radii; front clamp
pulled to z<=-1.5 for furniture scale so close cameras never wear a
table as a lens bar. Static (junk doesn't bob) with the existing
story-driven emissive breathe; re-hang on arrangement jumps moves
everything under the camera cut. popKey drops the actor dependence.

Verified: 24/24 tests, scene lint clean, same-frame re-render
bit-identical, rooms read as furnished across centre/drone/elegy.
Chamber-forced strips: elegy direction 0.26->0.61; centre stays
pre-existing-flat (0.11->0.09, dark song, same early arrangement);
drone agit unchanged (viz-driven).
2026-09-19 19:08:50 +02:00
Dejvino fd0cbe0553 Ride: plant the planted shots (passerby/billboard camera + planner fixes)
driveRideCamera checked the passerby/billboard plants after the generic
chase branch, which returns on every planned shot — both plants were dead
code (camera orbited, only the car drive-through worked). Check them first.

Planner fixes for the now-live plants:
- a planted section keeps the plant on its first cue only; later cues fall
  back to the screen/room rotation (a whole section planted is a stuck
  camera with the car looping through one frame)
- venueShotAt holds plant position too, not just the look
- no consecutive planted sections in either order; the section-level
  history never forms streaks for single-cue sections, so gate on the
  previous section directly (draw-first, rng-neutral elsewhere)
- passerby plant back/up, aimed between approach and pass point so the
  whoosh does not swallow the frame

Verified: billboard holds centre-frame with the viz live while the car
crosses mid-cue; passerby plant static +/- handheld; tests 24/24, scene
lint clean, standard gates unchanged vs HEAD (P9/P12 + census
centre/drone fail pre-existing).
2026-09-19 09:08:11 +02:00
Dejvino ce79ccf387 Sharp transitions cut color instantly and always reframe
Hard-cut cues now obey a single rule: the colour jumps with the image
(palette blend lerp skipped, even under blend-mode directors — the 2-frame
image ramp stays for flash safety) and both cameras must land somewhere
new, never hold static.

- Viz gaze relocates unconditionally on hard cuts (was +0.3 chance)
- Venue marks force fresh size (≠ previous) and flip look target
  screen↔room on hard-cut cues, landed instantly via venueShotAt
- Motive yields to grammar: a cutaway needn't face the action

Verified on lattice (2 hard cuts): gaze jumps, venue flips size+target
with 1.4/2.6-unit moves. Phases 8/11 fast gates 30/30.
2026-09-19 08:16:46 +02:00
Dejvino 822f4b12ef Ride: venue-owned camera (directCamera) + sidewalk passerby shot
Camera architecture: a module with directCamera:true drives its own rig
inside update() (analytic f(timeline, seed, framing, venueState)); the
shared rig is ignored for that layer only. ModelLayer prefers own camera,
Show skips shared injection. Ride migrates its full grammar over
(traverse, orbit, car tracking, jumps, clamp); Compositor ride branches
removed. Chamber/stage unchanged on the shared path.

Sidewalk shot: new ride-only 'passerby' plan target (breaths only, never
climax, never twice running) plants a static roadside camera; the module
drives the car +12 to -14 on driveT with the world frozen. Verified
far-pass-away through the Show path; lint + 24 tests green.
2026-09-19 07:34:48 +02:00
Dejvino 4b0f27da9a Ride: Fix hub caps 2026-09-18 19:19:57 +02:00
Dejvino 9c768f13d1 Venues: song sky, ride roadside, room camera, filmstrip venue pin
- Shared sky dome (src/venues/sky.js): gradient backdrop from palette /
  section light / style texture, wired into chamber/stage/ride with fog
  matched to the dome mid colour — replaces clear-to-black
- Ride roadside: houses, signs, trees, seeded viz billboard, all analytic
  scroll; popMul fade for crew, arrangement-gated roadside (no mid-section
  popping); parked idle creep + engine shake; analytic dash clock
- Room camera: traverse glide + progress look sweep + per-section dolly,
  single-side room visits, one re-hang max; venue framing per archetype
- Filmstrip venue selector (?venueSel=) with cache key; seed persisted to
  URL on reroll; gallery query plumbing
2026-09-18 18:57:04 +02:00
Dejvino 46fab512df Venues: persistent room, motivated camera, ride; drop Pylon Grid
- Venue as one continuous place: persistent per-song layer, interpolated
  venueState, story jumps (one re-hang max + recap) with camera cuts
- Room camera shot plan (venueShots.js): per-section size/target, monotonic
  look sweep, traverse glide body, room-side alternation
- New Ride venue: generated vehicle + actor passengers, radio RTT screen,
  chase/stationary modes, ride archetype across generator/camera/staging
- Flora cost trim (5x(12+6+6) -> 4x(8+4+4) SDF evals, 204ms -> 140ms worst)
- Delete Pylon Grid shader (2.5s/frame SwiftShader); drop its metadata row
- tools/venue-check.js headless gate harness + venue-gallery explorer
2026-09-17 19:35:26 +02:00
DejvinoandClaude b1b6431eeb OSD: stop appending "· llm-tuned" after applying LLM tweak
The corner title plate should stay clean — the tweak is already
confirmed via the Apply dialog and a toast. Appending to the fileName
made every LLM-tuned export carry an ugly suffix.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-15 18:56:52 +02:00
DejvinoandClaude 49914fd4ad LLM refine: 2dp prompt, mixed 64/32 frames, full proxy log, low reasoning
- spec.js: round all songSpec + extended summary/tempo/normalization to
  r2 (2dp) so high-precision 0.6607… stops wasting tokens; hoist r2/r3
  above specForPrompt to fix ReferenceError; FULL control wording kept.
- thumbs.js: border detection — first/last + 0.5s of any section start
  → 64×64 (borders), interior → 32×32 (between). Back-compat shim for
  old {size:N} callers. Returns size+role per frame for attachFrames.
- vite.config.js: raise sanitizeBodyForLog to 64k for __llm so 8×
  vision frames aren't truncated (was 16384, only 2 images logged).
  Also log reasoningEffort via providerOptions.
- llmTweak.js: set providerOptions reasoningEffort:'low' for both
  streamObject and generateObject fallback to cut 2459 → faster
  streaming progress, temperature 0.7 kept for variety.

Verification: build+lint:scenes green, tests 18/18.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 22:21:09 +02:00
DejvinoandClaude b0989169e2 LLM proxy: stream SSE through without buffering
The dev proxy buffered the entire gateway response via
arrayBuffer() before forwarding, which defeats streamObject —
no partials arrive until the full completion. Now event-stream
responses are streamed chunk-by-chunk via the upstream ReadableStream,
so the UI's "LLM streaming…" toast and console partials update live.
Non-streaming JSON is still buffered as before.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 21:55:51 +02:00
DejvinoandClaude e0e619af85 LLM: streaming + rich context + 32×32 frames + proxy log
- Streaming: requestSongTweak now uses streamObject when a progress
  callback is provided (via handleLlmTweak), emitting partial {rationale,
  edits} and updating toast/console as text arrives; falls back to
  generateObject if gateway doesn't support streaming. Models always
  called via provider.chat(modelId) / /v1/chat/completions, the correct
  path for meta/muse-spark-1.3-contributor via gateway.

- Rich context: spec.js gains FULL control wording (any amount/arrangement/
  bpm allowed), and detailed payload — trackSummary with bandBalance,
  tempo, normalization (per-feature lo/hi), sectionsDetailed (energy/
  flux/centroid/flatness/width/slope/endEnergy/duration), stats
  (kinds, avg/max energy, bars). LLM told You have FULL control.

- Low-res vision: new src/llm/thumbs.js — captureThumbnails(show, {8,32})
  samples section midpoints, warms up state, renders via Show, box-averages
  down to 32×32 JPEG data URLs, restores timeline. main.js now captures 8
  frames with two animation frames of pause, then passes them as {type:
  image, image: dataUrl} parts in the user message (llmTweak.js
  attachFramesToMessages).

- Proxy logging (no bearer): vite.config.js llmGatewayProxy now logs
  sanitized requests/responses to logs/llm-proxy.log (JSON lines, 1MB
  rotation, redacts apiKey/authorization from path/body/headers) so the
  coding agent can inspect prompts without re-calling. .gitignore adds
  logs/.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 21:54:07 +02:00
DejvinoandClaude e2f56e3ce4 LLM: verbose [llm] logs + visible tweak progress
Tune appeared to do nothing — the 400's cause was buried, in-flight was
near-invisible, and dialog only appeared on success. Make every stage
observable and cheap to diagnose.

- src/llm/llmTweak.js (+ spec.js): provider.chat(model) + providerOptions
  strictJsonSchema:false fix (correct placement), edits nullable so strict
  requires all 7 keys; clamped, budget-aware. Now logs [llm] fetch target,
  messages/spec, raw vs clamped edits, rationale/confidence/usage,
  elapsed time, and diff summary. NO_EDITS refunded with console.warn.
- src/main.js: immediate OK toast "Contacting LLM (model)…" on click,
  console.group for handleLlmTweak start, post-result toast with
  rationale snippet + editsSummary before dialog, preview synthesis
  logged, catch logs stack once with toast.
- vite.config.js: /__llm dev proxy already in place from prior fix;
  retained for CORS-free dev.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 20:59:30 +02:00
DejvinoandClaude 82ab88e6b0 LLM: fix strict schema 400 — correct providerOptions + chat model
Previous attempt set `strictJsonSchema:false` on createOpenAI()
(factory ignores unknown keys), so wire was still strict:true and
sparse edits {brightness:0.3} were rejected: Missing 'arrangement'.

- src/llm/llmTweak.js: use provider.chat(modelId) (/chat/completions,
  correct for Meta via gateway, not /responses) and pass
  `providerOptions:{ openai:{ strictJsonSchema:false } }` on
  generateObject where the SDK actually reads it.
- src/llm/spec.js: edits fields nullable instead of optional — strict
  requires every key in `required` when `additionalProperties:false`;
  nullable makes required = all 7 (model emits null for unchanged),
  clampEdits already strips nulls so NO_EDITS still works. Prompt now
  instructs "use null for unchanged, example { bpm:null,... }".
- test/llm.test.js: minimal valid result now all-null with one edit.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 20:48:14 +02:00
DejvinoandClaude e8c04e7798 LLM: allow sparse optional edits — disable strict JSON schema
The Responses API via Vercel AI Gateway requires `required` to
include every key when `strict:true`. Our TweakEditsSchema has 7
optional song knobs, so sparse edits (e.g. only {brightness: 0.3})
were rejected with 400: Missing 'arrangement'.

- src/llm/llmTweak.js: createOpenAI({ strictJsonSchema: false }) so
  the gateway accepts optional properties; keeps zod validation +
  clampEdits as the real guardrail.
- src/llm/spec.js: drop .strict() on TweakEditsSchema — extra keys
  are stripped rather than erroring, more robust against model drift.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 20:30:54 +02:00
DejvinoandClaude a0eaf2ef95 Fix AI Gateway CORS by proxying through Vite dev server
Direct browser fetch to https://ai-gateway.vercel.sh/v1 is blocked
(No Access-Control-Allow-Origin). In dev the AI SDK now calls
/__llm/v1 via same-origin Vite middleware (llmGatewayProxy), which
forwards server-side to the real gateway — no CORS. Production still
calls the gateway directly (deployments needing CORS-free prod should
add a serverless /api/llm proxy).

- vite.config.js: new llmGatewayProxy middleware for /__llm/* —
  handles OPTIONS preflight, forwards via Node fetch to
  x-llm-gateway (fallback Vercel), mirrors status/headers +
  access-control-allow-origin, preserves query/body.
- src/llm/llmTweak.js: in DEV use baseURL /__llm/v1 + proxyFetch
  that injects x-llm-gateway; prod keeps cfg.gatewayUrl.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 20:25:19 +02:00
DejvinoandClaude 904ae09486 LLM: keep API key in localStorage only, default to muse-spark-1.3
Secrets must never be baked into the static build. Stop reading
VITE_AI_API_KEY from env — key comes only from localStorage via the
⚙️ config dialog (flow-state:llm). Gateway URL / model may still come
from env as non-secret defaults.

- src/llm/config.js: DEFAULTS model → meta/muse-spark-1.3-contributor
  (cheap, per user request), readEnv no longer touches apiKey,
  getLlmConfig resolves key only from stored.apiKey.
- .env.example: drop VITE_AI_API_KEY=, document browser-only key flow.
- src/main.js: dialog copy now says key is browser-only, never baked.

Build + lint:scenes + tests still green. No VITE_AI_API_KEY string
remains in source.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 20:08:06 +02:00
DejvinoandClaude b7c3537fcb LLM song-tweak: AI Gateway via Vercel AI SDK, budgeted tune flow
Add a ✨ tune button that lets an LLM inspect the proposed
visualization and tweak the proc-song knobs so the next generation
looks better. Edits only the 7 songbank params (bpm, brightness,
noise, noiseColour, dynamics, transition, arrangement) — never the
Look — then re-synthesizes and re-generates locally for preview.

- ai + @ai-sdk/openai + zod (library per plan) against the Vercel AI
  Gateway (OpenAI-compatible, configurable via
  VITE_AI_GATEWAY_URL/MODEL/API_KEY, localStorage override,
  .env.example).
- Structured output (TweakResultSchema) + clampEdits enforces ranges;
  prompt ≤ ~2.5k tokens, max 2-3 knobs ±0.15 / bpm ±8.
- Budget caps: 3/load + 10/session (sessionStorage), checked before
  network, NO_EDITS refunded, offline/auth/rate-limit mapped.
- Prompt split into src/llm/spec.js (pure, node-testable) + llmTweak.js
  (gateway + digest) to keep `node --test` off the Vite JSON path.
- UI: ⚙️ gateway/model/key dialog (localStorage flow-state:llm),
  confirm(prompt) with hint, diff modal with Apply/Dismiss; Apply
  synthesizes → FeatureTrack → generateLook → Show.useTrack.
- Tests: 7 new LLM cases (clamp, schema, prompt, budget), 18/18 pass.
  Build + lint:scenes green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 20:03:27 +02:00
DejvinoandClaude 7736390f9c Comfort: agitation ceiling, composition validator and whole-screen caps
Whole-screen flashing/coherent motion is now a gated comfort signal
rather than a hunt-and-tweak. Three-arm agitation (flash/3,
tile-motion/0.32, structure/0.12) over a 1s floating window, worst
window wins — engine/agitation.js.

Policy centralised in look/comfort.js: director comfort appetite,
per-scene agitationOf proxy, agitationWeight lean and stackAgitation.
Canvas beat/flux capped in Layer.resolveParams (0.15/0.25, spike→smooth)
so one contract change tames every full-frame scene; lint warns above
the cap.

Selection now filters before casting (look/scene-filter.js, 12+ ranged
metrics from checks/scene-metrics.js, SCHEMA 7) and validates after
stacking (look/composition-validator.js) with a deterministic retune:
drop overlay → swap quiet composable → reselect filtered shot. Fix
validator attempt-3 ground-layer bug that crashed ?song=dusk
(LookGenerator now passes module, not layer).

Eight canvas shaders window their beat/flux (focusField + per-seed hash
mask) and report the scaled amount to Layer so the cap stays authoritative:
scan-tear, gate-corridor, analog-wow, block-mosh, metaballs,
pitch-shatter, scale-mosaic, tide-rings.

Checks: scene-gate comfort gate (agitation ≤ 0.70), filmstrip/gallery
meters + per-scene drawer, metadata SCHEMA 7 with ranged metrics and
fingerprint over agitation/comfort sources. main.js surfaces
COMPOSITION_REFUSED as a user-facing error.

Lint: 114 files clean, 71 scenes ok. All 17 songs generate looks.
Build passes. metadata.json refresh deferred to gallery → Refresh
metadata (SCHEMA 7 writer in vite.config).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-13 19:42:50 +02:00
DejvinoandClaude a9b028b0bf Export: fix VideoEncoder.configure TypeError on Chrome
probeExportCodec returned codec/muxerCodec but not config, so the
pre-probed pick used by the software-encoding warning path passed
undefined to VideoEncoder.configure, which Chrome rejects as "not of
type VideoEncoderConfig". Include config in the probe result and thread
it through exportSegment (test render was still reprobing and ignoring
the pick).

Also prefer the browser-negotiated config from isConfigSupported when
available — it is guaranteed valid for configure (covers new required
fields), whereas our minimal config had started failing validation on
Chrome despite isConfigSupported reporting "supported".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 20:09:49 +02:00
DejvinoandClaude e5484c9c38 Export: fall back to VP9/AV1 when H.264 unavailable and warn about software encoding
Firefox on Linux exposes VideoEncoder but reports every avc1.* config as
unsupported (OpenH264 is decode-only), so pickVideoConfig returned null and
export threw "no supported H.264 configuration found". Probe VP9/AV1 families
after H.264 and pick the first profile that isConfigSupported and does not
reorder (B-frame) — same 12-frame ordering test. Map the chosen family to
mp4-muxer's video codec (avc/vp9/av1).

Before any rendering, probe the codec for the chosen preset and show a
modal when we fell off H.264 onto a software VP9/AV1 path — that's what the
user felt as "super slow". The modal names the codec, explains render vs
encode cost, suggests Chromium/Chrome for hardware H.264 (or 720p/lower
preset), and offers Continue anyway / Cancel (Esc/backdrop also cancel) plus
"Don't warn again this session". Injected pick is threaded through
Exporter.export({ videoPick }) and exportSegment so we don't probe twice.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 18:17:53 +02:00
DejvinoandClaude b2c9497bae Epic 5 Phase 2.5 — model blend via alpha, not lumakey
Model stages render with alpha:0 where no mesh covers (transparent
clear) and carry dark material colours. Lumakey keys on luma
(dot(src,0.212…)), so a dim mesh keyed to ~0 and vanished — Pylon
Field 3D / Synthwave Corridor looked black. Use 'normal' for
kind:'model' so the alpha is the mask; fragment over dark ground
stays covered. Per-stage fog/light tuning deferred to a director
param (LookGenerator) per review — not hand-tuned per stage.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 22:08:32 +02:00
DejvinoandClaude f8e8d91c9b Epic 5 Phase 2.4 — Pylon Field 3D + Synthwave Corridor (model stages)
Two Stage-A model stages behind the same infra as Assembly:

  Pylon Field 3D (structural, kind:model, actor:structure) — chorus
    stacked legs + protagonist crowns on the pylon-grid perspective
    grid, transparent compositing via ModelLayer, analytic f(t,seed).

  Synthwave Corridor (structural, kind:model, actor:vehicle) — grid
    verges with streaming chorus passers + monolith hero on the road,
    verge math preserved from synthwave-run horizon/roadHalf calc.

Registered in scenes/registry.js; consumes:['form','ink','staging'],
traits:['shape','space','camera','style'] so trait/consumes gates
stay testable (reads personality in update()). Grid texture falls
back to DataTexture headless so lint/build stay green outside a
browser.

Gates: lint 110 clean / 70 literals / 71 scenes; vite build 301 modules.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 20:43:50 +02:00
DejvinoandClaude b98aa94dda Stage 3.6 — models visible: transparent model compositing + Assembly density
Model layers render with transparent clear so Compositor lumakey/normal
can show composition underneath where no mesh covers — Assembly is now
sparse (hero + satellites on ground, not opaque fill), satellites wired
to count/spread/density so the density slider visibly changes the stage
and a seek is still just f(t,seed). Shared camera plumbing already
drove framing->dolly/personality->drift for real parallax.

Gates: lint 108 clean / 70 literals / 69 scenes; vite built.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 18:58:32 +02:00
DejvinoandClaude 93a5dc8437 Epic 5 Phase 2.3 — instrument Assembly for measurement
Gallery, metadata, scene-gate and scene-sweep now handle kind:'model'
with a deterministic ActorSpec. Drawing uses a forked ActorGenerator
so the actor stream never shifts the identity/palette streams:
  gallery.renderScene → generateActor per context for model stages
  metadata.measureLibrary now includes model stages, SCHEMA 5,
    fingerprint covers actors/
  scene-gate.runSceneGate → alive/animates/deterministic/distinct/
    param-sweep/flash-rate all drive Engine.setLayerSpecs with
    actorSpec, and consumes: searches re-derive the actor for the
    swapped identity (so the consumes: gate is still testable).
  scene-sweep behaves the same per-cell.
  Engine.setLayerSpecs now forwards actorSpec/framing into createLayer
    so the checks don't need ArcDriver.

SCHEMA bump 4→5 forces metadata refresh on next gallery build,
which is intentional: model stages now participate. Gates remain
all green (69 scenes) and vite builds clean — no runtime change to
the render path for fragment stages.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 18:38:21 +02:00
DejvinoandClaude 16bb67e703 Epic 5 Phase 2.2 — actors visible in look description
describeLook now includes describeActorSet so HUD / check output
shows which actor archetypes and forms were cast. Look -> actor
plumbing is end-to-end: LookGenerator.actors on the look,
ArcDriver._actorFor + ModelLayer setActor, Show.renderFrame shared
camera injection, all deterministic.

Gates: lint 108 clean / 70 literals / 69 scenes green; vite built.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 18:27:10 +02:00
DejvinoandClaude fcbde602de Epic 5 Phase 2.1 — Assembly stage (model)
First kind:'model' stage: Assembly. The song's solid as a mesh.
Hero is the ActorSpec(monolith) assembly via actorToGeometry, built
from Identity.form with paletteMap recolour — same character as the
shader impostor, now with real occlusion/parallax/foreshortening.

Analytic motion only (spin/bob/orbit as f(t,seed,params)), ground
plane + standard-material lighting that matches castLit key/fill, fog
for depth haze, shared camera rig (framing→dolly, personality.camera→
drift/sway/spin). Palette arc visible live via per-frame material
recolour; opacity crossfade via material.opacity.

Registered in structural family so castingPool / director blends see
it like any structural shot.

Gates: lint 108 clean / 70 literals / 69 scenes green; vite 294
modules built.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 18:22:52 +02:00
DejvinoandClaude f60a29ea97 Epic 5 Phase 1 — form is a mesh, actors have parity with SDF
meshes.js now mirrors shader-contract.js verbatim:
- jsCastSDF rotates by -tilt (matching GLSL column-major mat2), hollow
  excluded during outer-radius search so annular SDFs sample correctly,
  radial search probes without hollow then punches hole separately.
- castShape samples 64 segments, inner hole scaled by hollow width.
- formToGeometry uses formDepth (u_formDepth = r.z*depth half-extent),
  capsule h correctly half-height, sphere as scaled unit sphere, prism
  extruded with proper halfDepth and centered; bevel thickness from
  part.round.
- actorToGeometry preserves per-part op/blend userData, uses YXZ Euler
  for yaw/pitch (matches formRot), mirrors radial/mirror/stack symmetry
  exactly as shader formFold does.

Shared camera wired live: Show.renderFrame drives
Compositor.updateSharedCamera from ArcDriver framing/personality/time
and injects sharedCamera into each active ModelLayer — pure f(frame,look)
so seek === playback. ArcDriver exposes framingForFrame helper.

Gates: lint 107 clean / 70 literals / 68 scenes green; vite build
294 modules; deterministic actor geometry smoke-tested (same seed same
vert count, hollow handling, fallback).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 17:01:44 +02:00
DejvinoandClaude 7aa60d7336 Epic 5 Phase 0 — actors have bodies; the stage has depth
ActorGenerator is the cast with bodies: generateActor{mpl} takes
{summary,rng,archetype,personality,identity} and returns a serialisable
ActorSpec — same audio-tilts-centre / seed-picks-within rule as
Personality/Identity, forked rng so adding an actor never shifts later
decisions. Five archetypes (monolith/swarm/walker/vehicle/structure),
per-track actor set on look.actors, HUD helper included. Stage C will
grow as a library on this without infra changes.

Mesh twin of Identity.form: actors/meshes.js builds BufferGeometry from
the same assembly (cast SDF → Shape → ExtrudeGeometry, box/capsule/
torus/sphere primitives, symmetry folding radial/mirror/stack). Shared
with the shader impostor path — one character, two projectors.

Renderer depth targets: createDepthTarget / createTarget{depthTexture}
for WebGL DepthTexture plumbing.

Compositor shared rig: one PerspectiveCamera + DepthTexture so a ground
mesh can occlude a subject mesh from another layer. 4/scale dolly,
Personality.camera drift/sway/spin, framing shift — matches particles.js
and shader epilogue behaviour. ModelLayer (kind:model) with
build/update(actorSpec) and sharedCamera injection; createLayer dispatches
on model. Shader contract gains MODEL_PREAMBLE.

LookGenerator now derives actors before scenes; ArcDriver._actorFor +
_layerFor wires ActorSpec into ModelLayer; schema validates kind:model
and actor archetype; lint determinism gate covers actors/.

Gate: lint 107 files clean, 70 shader literals, 68 scenes green; vite
build 294 modules; ActorGenerator determinism + mesh smoke tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 16:26:15 +02:00
Dejvinoandinternal-model ef1d24e692 Director cuts between palettes; the look panel shows which one is live
One palette for a whole song reads as one colour once five minutes
have passed. The track now keeps 2-4 audio-tilted palettes (forced
scheme/hue diversity so switches are legible) and the director owns
how they are traversed per cue — sequential/pong/kindLed/storyLed/
contrast — with cut or OKLCH blend timed to the cue crossfade. The
fine OKLCH paletteArc drift still rides on top of whichever base is
active.

The look tab renders the full set as a list; the row matching
ArcDriver.paletteIndexAt(frame) gets an active border/glow and
follows playback/scrub live, with a blend badge while two palettes
interpolate.

Co-Authored-By: internal-model
2026-08-20 12:45:42 +02:00
DejvinoandClaude df609424d5 Synthwave verges stay off the road; camera stays level unless the tail says otherwise
Synthwave Run: roadside passers stream on the verges, not on the road.
Each passer places at roadHalf + psize*0.92 + gap where roadHalf mirrors
the grid's abs(p.x*perspective) < 0.8, so even a large chorus form's
edge stays clear of the centre lane reserved for the protagonist.
Lateral wobble reduced to worst ~0.025 inward vs. minimum 0.07 gap so
it can never carry back onto the road. Four random chorus objects
rushing horizon->camera make each song's traffic its own; on-road
traffic the hero weaves around is left as a separate kind later.
Also carries the previous chorus passer loop, hero castSolid, and
texture 0.4 / form+staging declarations still pending in the working
tree from the last step.

Personality camera: uniform boxes put as many tracks at the wild edge
as near level, so the grid swung too often. Replace with a bell curve
(Box-Muller over the seeded Rng) — mode at level, rarity in the tails.
driftRate/sway/swayRate/spin/breathe now via gaussAround(centre,sigma)
centred near level (spin at 0, sign from the Gaussian itself) with
audio tilting the centre (fast→more drift/sway/spin, dynamic→breathe)
rather than displacing the whole box.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 11:25:42 +02:00
DejvinoandClaude c216e31413 Synthwave Run drives the song's protagonist instead of the same car
The grid, mountains and sun keep their palette-driven forms and are still
individually switchable so the arc driver can strip the scene back to
just the grid for a breakdown, but the car was the same silhouette in
every video — a hexagonal track and a round track drove the same stepped
rect with red discs. The thing on the road is now the song's own
protagonist: the same solid every other stage in the video draws, seen
here as one hero above the grid, turning slowly so its outline changes
across the song rather than being one silhouette from every angle.

One castSolid instance sized by stageScale() so a song of few huge forms
drives a huge hero and a song of many small ones drives a small one,
lit by castLit with an inkStroke rim and a soft ground ellipse for
contact. Gated by the existing u_car param so a breakdown still drops
to just the grid the same way it used to. Declares form, ink, staging
and shape/space/camera/style (texture 0.4); the lint's backtick and
consumes gates stay green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 10:21:26 +02:00
DejvinoandClaude Opus 5 3a0cf26e6c The filmstrip fills in as it renders
A full build is a couple of minutes of GPU work, and the page spent all of it
showing an empty screen and then everything at once. The interesting failure —
a strip whose frames could be shuffled without anyone noticing — is visible in
the first row, so waiting for the seventeenth to look at the first is a
needlessly slow way to find it out.

Rows are appended as they land, in bank order, and re-sorted once at the end
when there is finally something to sort by. One row renderer handles both raw
pixels, which is what exists mid-build, and the compressed blobs the cache
holds afterwards, so what you watch appear is what you keep.

The song is announced BEFORE its render rather than only after it, too:
synthesising and rendering one is tens of seconds, and a page that says nothing
until the first one lands looks broken for exactly as long as that takes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:02:10 +02:00
DejvinoandClaude Opus 5 89e05459c0 Every shot stands on something, and the frame has two ends
A section used to be one scene, and two thirds of the library is composable —
sparse by design, elements ON something. Cast as backgrounds anyway, they left
9 of 40 sampled frames under 20% painted, the darkest at 0.3%: a minute and a
half of a few bright things on black, invisible to every gate because every
gate on the stack was a limit rather than a floor.

Every section now stands on a GROUND: a canvas that fills the frame, cast per
section kind so a shot cut changes the shot and not the world. When the shot
fills the frame itself it IS the ground — two canvases stacked is two pictures
fighting. Above that, a coverage BUDGET: director appetite times the section's
energy times where the story is, capped at two frames' worth of material.

The measured facts move into the repo. scenes/metadata.json is generated from
the gallery — coverage as a shot, coverage as a bed, variety, the structural
profile — tracked in git, stamped with a fingerprint of the scenes and the
metric definitions, and refreshed from gallery.html. `surface` is derived from
it rather than declared; nine scenes claimed `canvas` while painting under a
third of the frame, and declaring it is now a lint error. The generator weights
every layering choice by measured structural distance, because family labels
and the render disagree: two `geometric` scenes can be 0.31 apart and a `flow`
and an `organic` scene 0.04.

The gallery's 0.1 red line is gone. It was right when a section was one scene
and wrong now — nineteen scenes were failing a bar for being consistent, which
is a virtue in an ingredient.

Chasing the numbers turned up four real faults:

  * A scene that reads prev() cannot be a ground. It returns the whole
    composited frame including the layers above it, so a datamosh under a shot
    is eating it: the render stopped reproducing from a seek and two WebGL
    contexts diverged by 91/255 against a tolerance of 4.
  * Screen was the wrong operator for a shot over a bed. It lightens, so a
    median quarter of every frame clipped to paper and whole sections rendered
    100% white. Replaced by a lumakey — the shot's brightness is its alpha.
  * Feedback was an accumulator: a still image settled at 2.3x its own
    brightness. Fine over black, fatal over a filled ground. Normalised at 0.6,
    plus a highlight shoulder so the top rolls off instead of clipping.
  * useTrack never prewarmed, so a fresh Show's first frame differed from every
    later render of it — the export-breaking hazard Compositor.prime documents.

Blazing is a decision now, not a side effect: directors declare an appetite for
it, a section must be loud and late in the story to earn one, and quiet kinds
never do. The ceiling gate matches that — a hard cap per section, and no more
than a fifth of them hot at all.

Rendered across twelve videos, middle of every section:

    painted        51% mean, darkest 0.3%  ->  87% mean, darkest 43%
    clipped white  24% median, worst 100%  ->   1% mean, worst 30%
    separation     0.10                    ->  0.44

Seven scenes can ground a section — five geometric, two organic — so every
quiet section of every video stands on one of two beds. That is the library's
largest hole and it is scene work: there is no minimal or flow canvas that
fills half the frame without reading prev().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:02:01 +02:00
DejvinoandClaude Opus 5 7d151be6e1 Pylon Grid stands on the chorus instead of on a strut
The legs were the last thing in this scene it had invented for itself: a
fixed-width vertical capsule, the same object in every video, with only the
palette separating one track's structure from another's. Against migrated crowns
they read as scaffolding left up around a finished building — and structure is
half this frame, so half of it was still saying what the SCENE is rather than
what the song is.

A leg is now a stack of the song's chorus. How many members make up a column is
the song's element size deciding it — a track of few huge forms stands on three,
a track of many small ones on a ladder of nine — and how wide the column is comes
from the chorus's own proportions. Each member turns at its own angle, so a
column is one form seen many ways rather than one form printed nine times. That
makes `staging` a third declared artifact, which the lint caught before the gate
had to.

Measured on the crop that contains the change, since the frame-level score
cannot see it: over six identities the bottom third of the frame moves 13.4/255
with the old strut and 22.2/255 with the chorus stacked. The whole-frame variety
score goes 0.1322 to 0.1348, and the header now explains why the two disagree
and why both are right — the crowns dominate a descriptor averaged over the
whole image.

Cost is unchanged in practice: 7.7ms/frame at 4K, 12.4ms with every param at the
top of its range, against a 60ms ceiling. The column's bounding box is two
comparisons for a pixel outside it, which with a hundred and sixty pylons is
most of the budget.

The crown is resolved before the leg now. Resolved the other way round, the
first-wins rule let the column paint over the object standing on it and every
subject was cut off at the waist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:49:29 +02:00
DejvinoandClaude Opus 5 61e0c66f61 Pylon Grid stands the song's object on its pylons
The scene already had the depth axis the solid wants — near rows large, far rows
small, converging on the track's horizon — which made the flat crown the one
element in it drawn without perspective: a sticker facing the viewer on top of a
receding structure. Each crown is now marched, and each pylon turns its own at
its own angle, so a single field shows the form from a dozen sides at once.

12/12 on the gate, 7ms/frame at 4K against a 60ms ceiling. A hundred and sixty
instances stay affordable on the two guards MIGRATION.md now documents, and here
first-wins is the correct occlusion as well as the cheap one, because the rows
are already walked near to far.

IT MEASURES SLIGHTLY WORSE and the scene's header says so: 0.141 to 0.134 over
8 songs x 8 seeds, nearly all of it layout (0.156 to 0.126). Two candidate
causes were tested and both came back flat — keeping the far rows stamped scored
0.132, and running the lit surface through inkPattern scored 0.132 here and
moved Effigy by 0.0009. It is the solid itself: a body always fills its own
silhouette, where a stamped cast form paints anywhere between an outline and a
disc depending on whether the identity is hollow, and that swing was worth three
hundredths of layout variety.

Kept because the score does not measure what was wrong. A crown that ignores the
perspective of the field it stands in is a defect no coverage variance reports,
and the header records the number and the two-line revert for whoever disagrees.

The inkPattern experiment is reverted rather than left in as a plausible-looking
no-op, with its measurements written where the next person will look for them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:24:28 +02:00
DejvinoandClaude Opus 5 0a89676cc8 One scene, wide: songs against seeds
Six thumbnails is enough to rank sixty-eight scenes and far too few to study
one. Worse, six songs at one seed each confounds the two inputs: a scene whose
frames are interchangeable might be ignoring the identity or ignoring the music,
and the library gallery cannot say which.

So a scene's name in the gallery now opens it on a grid — songs down, seeds
across, up to every song in the bank by ten draws. Buttons pick the size and
walk onto a fresh block of seeds, which is a much better answer to "is it flat
or was that ten unlucky rolls" than staring at the same ten. The state lives in
the URL, so every button is also a back button.

Three scores rather than one, and the two new ones are the diagnosis: ACROSS
SEEDS is the same song with a different draw, so low means the scene ignores the
identity; ACROSS SONGS is the same draw against different music, so low means it
ignores the song. They point at different fixes. The closest pair is outlined,
because at 170 cells no eye is finding it.

The first version hashed the song name into the seed so a column would not be
one roll repeated down the grid. It reads better and measures nothing: with the
seed varying on both axes the two scores are the same comparison, and they came
back within 0.001 of each other for every scene tried. A column holds its seed
fixed now, which is what makes it attributable to the music — and the scores
separate, with the seed moving every scene tried more than the song does.

170 cells in 9s. The songs are the slow part and are analysed one at a time so
the count moves, then cached for the session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:16:15 +02:00
DejvinoandClaude Opus 5 89277705c4 Nothing is held for two minutes, not even one visual
planShots skipped cut planning entirely when a section had a single visual to
show — "nothing to cut to" — and the exemption swallowed the ceiling with it.
Adding a scene shifted the casting enough to land a 150 BPM track in that case,
and it held one image for 151.98 seconds against a 22-second limit. Any new
scene could have exposed it; the shot-length gate had simply never met a
single-visual section before.

The premise was wrong rather than the arithmetic. A shot boundary changes the
FRAMING as well as the image, so the same visual filmed again at another size is
a shot, and the ceiling is about how long one image is held rather than about how
many images a section has. Single-visual sections now plan shots like any other.

What the single case still suppresses is the hard cut: cutting straight between
two framings of one image is a jump cut, so those boundaries always dissolve.

Two intermediate versions were wrong in ways worth not repeating: splitting the
section into equal spans put a 23.5s shot past the ceiling once the cuts snapped
to downbeats, and read as a metronome (cv 0.048) to the rhythm check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:16:02 +02:00
DejvinoandClaude Opus 5 144c8c918e The song brings a body, not only an outline
A silhouette is the same picture from every angle, so a scene that turns one is
showing you the same shape rotated. That is the ceiling the cast has been under:
it can be notched and hollowed and it still cannot be walked around.

So the identity generates an ASSEMBLY — two to six parts, each a primitive with
an offset, a scale, a rotation and a boolean op, all under a symmetry. The
symmetry is the load-bearing half: parts unioned at random positions read as
debris, the same parts folded read as designed, and only a designed object is
worth calling a protagonist. Prism parts are the existing 2D profile extruded,
so the solid and the silhouette stay one character rather than two generators
running side by side.

It travels as data like every other artifact: three vec4 rows per part in
u_formPart, plus the scalars. Scenes declare `consumes: ['form']` and get
castSDF3, castMarch, castSolid, castChorusSolid and castLit; with no identity
they fall back to the flat profile extruded, so the helpers are safe to call
unconditionally. The chorus is the same rows with fewer parts and its own
proportions — a relative, not a second generator, and no extra uniforms.

Three scenes carry it. Effigy is new and holds the object still while it turns.
Floating Geometry and Swarm were already loops of stamps and are now loops of
bodies; Swarm is what the chorus solid exists for. That is 34.8% of videos
containing a 3D cast, against 11.9% when only Effigy had it.

Measured, the outline does change rather than merely spin: over one turn the lit
area of Effigy's subject varies 14-113% against Soloist's 3-51% for the same
rotation. Whether that reaches the variety blocks is not yet measured, and
HOWTO-variety says so rather than claiming the win.

Four costs, each found by measuring rather than by reading:

  * every pixel evaluated every instance's field — Swarm at 59ms/frame against a
    60ms ceiling. Bounding-sphere reject first, now 12.3ms.
  * instances overlap several deep at the top of the size range, and marching
    all of them made Floating Geometry's own gate run for minutes. First-wins
    instead of last-wins, which was arbitrary either way.
  * a normal inside the march loop multiplies four copies of the SDF by the step
    count, because GLSL unrolls a fixed bound. Hoisted out.
  * the helpers in the shared preamble made all 68 scenes compile what 3 of them
    call. FORM_PREAMBLE is appended per scene instead.

The lint's backtick check was green through two of my own breakages: quoting a
name in a doc comment adds backticks in PAIRS, so parity survives and the
pair-scanner just re-partitions the file. It now finds where a shader literal
opens and requires the next backtick to be a real terminator — which
immediately found a second stray pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:15:49 +02:00
DejvinoandClaude Opus 5 518e9ea731 Epic 4: record why direction was negative, and what is left
The first measurement offered three readings for a direction of -0.14. The
third one — the arc is not reaching the image — was correct, and it is now
+0.24 against an arcless reference of -0.02, with the floor rising exactly as
§8 predicted.

Records what the two dead channels were, since neither was visible from the
source and both passed every gate that existed: the overlay path had an empty
candidate set (0/144 stacks), and the camera moved the frame by a median of
0.029 of a half-frame at a random angle so successive shots cancelled. Both
cleared their checks because every bound on them was a ceiling. A gate on a
device needs a floor.

Also ranks what remains, with the broken seed-variety instrument first: three
of the four open items are measurements and they all report through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:58:27 +02:00
DejvinoandClaude Opus 5 1ca9fa2f1f Let scenes layer on each other, and give the camera somewhere to look
Two devices that were built, wired up, and doing nothing.

OVERLAYS. buildStack gated layering on `surfaceOf(m) === 'composable'`, and
no scene in the library declared `surface` — so the only thing that could ever
sit on top was the one scene declaring `role: 'accent'`, and the overlay roster
excluded accents by construction. Empty intersection, every time: 0/144 stacks
carried an overlay. Every layered frame in every song was the same particle
field.

Labelled the library from the phase 12 coverage gate rather than by eye: the
37 scenes painting under 30% of the frame are composable. Particle Field loses
its privileged slot and becomes one of them, keeping only `background: false`,
which is the honest part — points in empty space cannot carry a section alone.
The reserved accent slot is gone; one roster, two passes at it. 1 distinct
overlay scene becomes 27, and the rate lands at 34% of stacks after trimming a
base chance that had been tuned while the branch was dead.

THE CAMERA. framing.shift moved the frame by a median of 0.029 of a half-frame
at a fresh random angle every shot, so successive offsets cancelled and the
median jump at a cut was 0.014. Present in every frame, visible in none.

look/Camera.js is the director's camera department: the story says tension,
order and which act a section is in, and this turns that into where the frame
looks and how it travels there. Each director names a camera. Jump distance
follows tension and act, speed follows energy, and the curve is one of four.
Cuts choose between reframing and matching, so two scenes can still read as one
place. Median offset 0.190, median reframe 0.135, and 94% of shots now move
during the shot rather than only at the cut.

Four new gates, each with a FLOOR — the device this replaces passed every
existing check while doing nothing, because the only bound was a ceiling.

Also lands the in-progress Epic 4 story layer it builds on: Story.js, phase 13,
and the direction statistic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:48:13 +02:00
DejvinoandClaude Opus 5 eda8ac3810 Composable vs canvas: say which scenes are meant to sit on top of others
The top of the gallery is full of black frames with a bright element on them.
That is genuinely varied and thin to watch, and the reason it ranks is that the
gallery previews every scene ALONE — which is not how anyone watches one.

Measured, the split is not marginal: 37 of 66 scenes paint under 30% of the
frame, and the sparsest are exactly the ones topping the ranking. Storm Rift and
Pendulum Trace paint 0%, Droste Feedback and Spectrum Sculpture 2%. At the other
end Block Mosh covers 98% and Scan Tear 96%.

So a scene now declares a `surface`. A canvas fills the frame and belongs
underneath; two of them stacked is two pictures fighting rather than one picture
with depth. A composable scene is mostly empty by design and reads as elements
ON something. The distinction was implicit in `role: 'accent'`, which marked the
depth passes and said nothing about the sixty scenes treated as interchangeable
backgrounds while half of them were sparse.

Verified rather than trusted, in the pattern the rest of the project uses: a
gate renders every scene and measures what it actually paints, failing a
composable that covers more than 55% — it would hide whatever it sits on — and a
canvas that covers less than 8%, which is a canvas in name only. The gate also
prints the whole coverage table, since that list is how the library gets
labelled in the first place.

Casting uses it: only composable scenes are drawn as overlays, and because what
goes on top is now guaranteed to leave the shot underneath visible, layering
happens far more often — the overlay chance went from 0.12 to 0.3 at its base.

The gallery shows coverage and surface beside each score, sorts by sparsest, and
says in the header why the two numbers explain each other.

Nothing is labelled `composable` yet. Every scene keeps the behaviour it had
through `surfaceOf`, which defaults to canvas unless the scene is an accent, so
this commit changes the mechanism and not the output. The coverage table is what
the labelling should be read off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:20:24 +02:00
DejvinoandClaude Opus 5 c099501ead Raise the variety bar to 0.1
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>
2026-08-18 18:11:33 +02:00
DejvinoandClaude Opus 5 b879abe230 A region block: how differently the parts of a frame behave from each other
The descriptor could say where a frame's energy sat and not whether one part of
the picture was doing something different from another — and that second thing
is what "nothing to watch" means. A pattern spread evenly over the screen has no
subject precisely because every region is the same region, and `layout`
normalises that away: a uniform field and a field with a form standing in it can
come out at nearly the same distribution.

So each ninth of the frame is now characterised in its own right — how much
detail, which way it runs, how many elements — and expressed as its deviation
from the frame's average. Heterogeneity rather than level, so a brighter or
busier frame does not register as a more varied one.

It is immediately the strongest block in the descriptor, and it credits scenes
the old one was underrating:

    Apollonian Gasket  0.039 -> 0.062   region 0.180
    Plasma Bloom       0.053 -> 0.082   region 0.182
    Moiré Grid         0.037 -> 0.047   region 0.097
    Droste Feedback    0.219 -> 0.324   region 0.446

Apollonian Gasket is the interesting one. It was called out as looking good and
scoring badly, and the answer turns out to be partly that the instrument was
missing the axis it is good on rather than that beauty and variety are simply
different things. Both were true; only one of them was the instrument's fault.

The metric's own gates still hold, which is the condition for believing any of
this: recolour moves structure 0.019 while moving colour 0.723, a quarter turn
moves it 0.0000, and the same scene against itself is 0.0000 against 0.0992 for
two different scenes.

EVERY SCORE HAS MOVED. The total is a mean over six blocks now rather than five,
and the 0.04 bar was derived under the old one. It needs re-deriving from a fresh
gallery before it is used to judge anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:07:11 +02:00
DejvinoandClaude Opus 5 8defef3ed0 Tell a dead shader apart from a boring scene in the gallery
A failed shader compile renders black, and black frames are identical to each
other, so every block scores 0.000 and the row reads as the least varied scene
in the library rather than as a broken one. That happened yesterday — the
subject helpers were declared above the ink they call, GLSL has no forward
declarations, the whole preamble failed to compile, and the gallery reported a
variety of exactly 0.000 with no indication anything was wrong. It is a
particularly bad failure mode for a tool whose entire job is ranking scenes by
that number.

A scene whose first frame has no luminance and no variance is now reported as
broken, with both figures and a pointer to the console, and the row is coloured
apart from the merely flat ones so it cannot be mistaken for a bad score. The
summary counts them separately.

Verified against both answers rather than only the happy one: a healthy Moiré
Grid comes back 0.0365 with no error, and a deliberately broken copy of the same
scene comes back with the diagnosis instead of a zero.

Recorded in HOWTO-variety.md alongside the other instrument traps, since the
general lesson outlives this instance — a broken render produces the most boring
possible numbers rather than an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:55:16 +02:00
DejvinoandClaude Opus 5 c950f8f3b1 Give a full-frame field a subject the eye can track, and let the song choose
what it does

"One pattern across the screen" scenes have nothing to watch. Warping the field
around a focal point was the previous attempt and it did not fix that — measured
on Voronoi it raised orientation variety while layout stayed at 0.004, which is
another way of saying the cells changed and the frame still had no subject.

So a subject is now a form that does something TO the field, and which thing is
the song's decision rather than the scene's: shift, warp, punch, morph or
overlay. The same form punching a hole, bending the pattern or running it at
another rate are three different videos, and across six songs the bank picks
overlay, morph, shift, morph, warp and punch — so it is a real axis rather than
a constant with five names.

subjectSDF, subjectMask, subjectEdge and subjectWarp are in the contract, so any
field scene can take a subject without knowing where the focal points are.

Moiré Grid is the pilot and implements all five impacts. 0.031 to 0.037 — up a
fifth, and the layout block moved from essentially nothing to 0.018, which is
the first time anything has moved layout on a full-frame field. That is the
number that speaks to the complaint: the frame's energy is no longer spread
evenly, so there is somewhere to look.

It is still under the 0.04 bar, and I have not seen it. The score says there is
now a subject; whether it is worth watching is the question the gallery cannot
answer.

One bug worth recording: the subject helpers were placed above the ink in the
preamble, and subjectEdge draws with inkStroke. GLSL has no forward
declarations, so the whole preamble failed to compile and every scene using it
rendered pure black — which the gallery reported as a variety of exactly 0.000
across all six blocks rather than as an error. An all-zero row means a dead
shader, not a boring scene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:52:57 +02:00
DejvinoandClaude Opus 5 10eeddcf6d A guidance document for building visualizers that vary
HOWTO-visualizers.md gets a scene working. Nothing said how to make one look
different from song to song, which is the thing the library is worst at and the
thing every measurement this epic has been about.

Written as evidence rather than advice: each claim carries the number behind it,
so a later change can contradict it. Several already contradict things believed
earlier in the same week.

It records the failures at least as carefully as the wins, because they were more
informative and each looked obviously right beforehand — effects as parameters
inflating a score without changing a picture, concentrating disturbance instead
of adding it, the roster-size theory that a direct sweep found to be nothing, and
reading a form as a metric rather than drawing it. It also documents what the
descriptor cannot see, since half of "why is my score low" is there: brightness,
colour, rotation, quality, and layout for anything that fills the frame.

And it is explicit about which numbers to trust. Direct render comparisons have a
noise floor of zero; aggregate ratios swing enough to have produced four
withdrawn conclusions in one epic.

Ongoing by design, with the open questions listed: coupling has never moved off
zero, fixed-geometry scenes have no known route to variety, and the 0.04 bar was
set against scores that were depressed by a measurement bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:45:38 +02:00
DejvinoandClaude Opus 5 5a9dfa6b2b A scatter point in Isometric Blocks — which measures worse, twice
Asked for, built, and honest about the result: the focal point costs this scene
a quarter of its variety and I could not make it pay for itself.

    uniform scatter, no focus     0.0490
    focus, trading scatter for it 0.0374
    focus, added on top of it     0.0365

The first version spent the scatter budget near the focus and calmed the rest,
which put most of the field back on the rigid lattice and took the orientation
variety with it, 0.164 down to 0.105. That diagnosis looked right, so the second
version made the focus additive — every cell keeps its full scatter and the ones
near the point get more. It changed nothing: 0.0365, orientation still 0.104.

So the diagnosis was wrong and I do not have the real one. The remaining suspect
is the height modulation, which lifts blocks toward the focus and may be
flattening the skyline's variety between songs by dominating it — but that is a
guess, and two guesses have already been wrong here.

The feature is committed rather than reverted because the metric does not
measure what was asked for. A scatter point is a compositional idea, and the
gallery scores how much a scene changes BETWEEN songs, which is a different
question — Apollonian Gasket makes the same point from the other direction. This
may well look better and score worse. It needs eyes on it before the number
decides.

`gather` is a parameter and seed-driven like any other, so the amount varies per
song as asked; the focal points themselves come from the identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:42:55 +02:00
DejvinoandClaude Opus 5 b1e58a6603 A focus: somewhere for a full-frame field to be about, tried on Voronoi
Edge-to-edge textures have no composition to vary. Two songs of Voronoi Shatter
measured 0.004 apart in the layout block, and that was honest rather than a
metric failure — the cells changed and the arrangement did not, because there
was no arrangement.

So the identity now decides a FOCUS: one to three points, a reach, and a pull
that either draws the field in and densifies it or opens a void. The points come
off the same lattice everything else is placed on, so a scene using them is
composing in the song's terms rather than inventing a centre of its own.
focusField and focusWarp are in the contract, available to any field scene that
wants somewhere to be about.

Voronoi Shatter is the pilot. It tiles in the warped coordinate, so cells crowd
toward the focus or pull away from it, and its seams tighten where the field
gathers so the effect reads as a change in the shatter rather than a brightness
blob laid over one. 0.0463 to 0.0588, comfortably clear of the bar.

It did NOT work the way the hypothesis said. The gain is in orientation, 0.140
to 0.194, and in texture; the layout block moved from 0.004 to 0.007, which is
still nothing. Warping where the cells sit changes what they look like without
changing where the energy is, because the field still covers the frame corner to
corner. Layout stays blind to this family until a field is allowed to actually
fall away from its focus and stop being full-frame — which is a bigger change to
what these scenes are, and worth deciding rather than sliding into.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:39:35 +02:00
DejvinoandClaude Opus 5 8d9b8a593a Fix a permanently-zero motion block, and let the song arrange Isometric Blocks
Investigating three pieces of feedback turned up a measurement bug underneath
all of them. The gallery built four of the descriptor's five structural blocks
and never rendered a second frame, so `motion` came back 0.000 for every scene
in the library and dragged every score down by a fifth. It read as a property of
the scenes; it was a missing render. Every number in the gallery so far,
including the ones the 0.04 bar was calibrated against, was depressed by it.

    Plasma Bloom      0.0532 -> 0.0624
    Voronoi Shatter   0.0431 -> 0.0463
    Apollonian Gasket 0.0340 -> 0.0388

The Plasma Bloom versus Voronoi Shatter oddity is real and is the metric's
limit rather than a mistake. Plasma Bloom is a centred subject whose position
moves between songs, so its layout distance is 0.096; Voronoi Shatter is
edge-to-edge cells, and a full-frame texture has the same layout however its
cells fall, so its layout distance is 0.004. The descriptor cannot see "the
cells are different" as composition, and for full-frame work the layout block
contributes almost nothing. Worth knowing before the bar is used to judge that
family.

Apollonian Gasket scoring low with good-looking frames is the gallery working:
it measures how much a scene changes between songs, not how good it looks. A
scene can be beautiful six times and identical six times.

Isometric Blocks gets the displacement it was asked for. Every block sat exactly
on its lattice slot, so the plan of the field was the same plan in every song
and only the heights moved — a layout distance of 0.005. `scatter` steps each
cell off its slot by a fixed amount of its own, bounded under half a cell so
blocks do not cross and break the isometric read, and it drives the block's
height and side wall as well as its ground position. 0.034 to 0.049, clearing
the bar, with orientation variety nearly doubling as the rigid lattice softens.

Its layout distance stayed at 0.004, for the same reason Voronoi's did: the
field fills the frame either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:32:30 +02:00
DejvinoandClaude Opus 5 71ddd37711 A minimum variety bar in the gallery, drawn as a red line
A visualizer that looks the same wherever it appears does not just fail to vary
— it leaks between songs, because it is cast into many of them and the viewer
who watches two videos ends up recognising the shot rather than the song. That
makes the floor a property of the scene, so the gallery now states it and the
scene has to clear it.

0.04 to start with, and honestly a starting bar rather than a derived one: set
where the flat cluster measurably sits, to be raised as scenes are lifted to it.

Drawn twice, because the two readings are different questions. Every meter
carries a red marker at the bar, so a single row can be judged on its own. And
in the default sort the list is cut by one red line where the scores cross it,
which answers the browsing question — everything above that line repeats itself
between songs.

Also restored progressive drawing, which the caching rewrite had quietly lost:
every draw had moved to the end of the build, leaving three minutes of spinner
and nothing to look at. The first rows are the interesting ones, since the sort
puts the repetitive scenes on top.

Verified by seeding the cache with rows spanning the bar and reloading: four
below, four above, the line between them, the marker at 20% of each meter, and
the grade colours switching at the right place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:29:09 +02:00