Staging: a shared lattice, and the element size the songs were all sharing
The cast and ink slice lowered the floor as predicted but left `scale` — feature
size — consistently WORSE than the legacy arm, 0.033 against 0.053 and well
outside the noise. Every stage still chose its own element size from its own
param range, so every song landed in the same place: the videos shared a cast
and also, accidentally, shared how big everything was.
Staging is the third artifact. A lattice — grid, radial, spiral, scatter,
strata — with jitter, spread, a size hierarchy and a bias toward the middle or
the edges, transported the same way the cast is: uniforms plus a `stageNode`
function in the preamble, no new engine plumbing. Constellation, Swarm and
Procession place on it; the stage keeps its motion and gives up its composition.
Soloist takes only the scale, since a close-up has no composition to share.
`elementScale` is the part that mattered. It is the song's answer to "how big is
this made of", spanning about a factor of six, and it is the decision that was
missing rather than mis-set. Measured, over twelve songs and three runs:
stages spread +0.0111 ±0.0043 → +0.0143 ±0.0008
scale block 0.033 ±0.004 → 0.041 ±0.007
Against the legacy arm at +0.0108 ±0.0071 the harness still says
indistinguishable, and it is right to: the gap is +0.0035 and the legacy arm's
own run-to-run range is twice that. What can be said is narrower and holds up.
Stages have the lowest floor of the three arms by a clear margin — 0.082 against
0.095 and 0.104 — so sharing content does make a video look like itself, which
was the central prediction. And the targeted fix moved the block it was aimed at
in the direction it was aimed.
Also added: a lint check that the shader preamble contains no backticks. Twice
now one has closed the template literal and produced a check page that hangs on
"starting…" with an empty console, which is an expensive way to find a typo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ export const constellation = {
|
||||
name: 'Constellation',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.6,
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
@@ -42,13 +42,15 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
float fi = float(i);
|
||||
vec2 h = hash22(vec2(fi + u_seed, fi * 1.7));
|
||||
|
||||
// A slow wander around a fixed home, so the constellation keeps its
|
||||
// shape while nothing in it is ever quite still.
|
||||
vec2 home = (h - 0.5) * 2.0 * u_spread;
|
||||
vec2 pos = home + vec2(sin(t + h.x * 6.28), cos(t * 0.83 + h.y * 6.28)) * 0.08;
|
||||
// Homes come from the song's lattice — the stage owns the wander, not
|
||||
// the composition. A slow drift around a fixed home, so the
|
||||
// constellation keeps its shape while nothing in it is ever quite still.
|
||||
vec3 node = stageNode(fi, float(u_count));
|
||||
vec2 pos = node.xy * u_spread
|
||||
+ vec2(sin(t + h.x * 6.28), cos(t * 0.83 + h.y * 6.28)) * 0.08;
|
||||
|
||||
float pulse = 0.7 + 0.3 * sin(t * 2.0 + fi) * u_twinkle;
|
||||
float size = u_size * (0.5 + h.x) * pulse;
|
||||
float size = u_size * node.z * pulse;
|
||||
|
||||
vec2 q = (p - pos) / max(size, 1e-3);
|
||||
float d = castChorus(q) * size;
|
||||
|
||||
@@ -9,12 +9,12 @@ export const procession = {
|
||||
name: 'Procession',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
columns: { type: 'int', range: [2, 9], default: 4, uniform: 'u_columns', bias: 'density' },
|
||||
columns: { type: 'int', range: [2, 8], default: 4, uniform: 'u_columns', bias: 'density' },
|
||||
depth: { type: 'int', range: [2, 8], default: 4, uniform: 'u_depth', bias: 'density' },
|
||||
march: { type: 'float', range: [0.05, 1.2],default: 0.3, uniform: 'u_march', bias: 'motion', rate: true },
|
||||
size: { type: 'float', range: [0.1, 0.5], default: 0.28,uniform: 'u_size' },
|
||||
@@ -39,30 +39,29 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
// Rows recede toward the horizon the track shares with every other scene.
|
||||
float horizon = sigHorizonY();
|
||||
|
||||
for (int row = 0; row < 8; row++) {
|
||||
if (row >= u_depth) break;
|
||||
float fr = float(row);
|
||||
// Rows further back are smaller and closer to the horizon.
|
||||
float back = fr / max(float(u_depth), 1.0);
|
||||
float scale = mix(1.0, 0.35, back * u_recede);
|
||||
float y = mix(horizon - 0.9, horizon + 0.15, back);
|
||||
// The formation is the song's lattice; the march is this stage's own idea
|
||||
// of what to do with it. That split is the whole point — identity owns
|
||||
// placement, the stage owns motion.
|
||||
float total = min(float(u_columns * u_depth), 64.0);
|
||||
|
||||
for (int c = 0; c < 9; c++) {
|
||||
if (c >= u_columns) break;
|
||||
float fc = float(c);
|
||||
float lane = (fc / max(float(u_columns) - 1.0, 1.0) - 0.5) * 2.4;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
float fi = float(i);
|
||||
if (fi >= total) break;
|
||||
|
||||
// The march: each row slides at its own pace, wrapping.
|
||||
float phase = fract(t * (0.4 + back * 0.6) + fr * u_stagger + fc * 0.017);
|
||||
float x = lane + (phase - 0.5) * 0.6;
|
||||
vec3 node = stageNode(fi, total);
|
||||
// Depth from the node's own height in the frame, so recession agrees
|
||||
// with wherever the lattice put it rather than with a row index.
|
||||
float back = clamp((node.y - (horizon - 0.9)) / 1.05, 0.0, 1.0);
|
||||
float scale = mix(1.0, 0.35, back * u_recede) * node.z;
|
||||
|
||||
vec2 q = (p - vec2(x, y)) / max(u_size * scale, 1e-3);
|
||||
q = rot(t * u_spin + fr) * q;
|
||||
// The march: each element slides at a pace set by its depth, wrapping.
|
||||
float phase = fract(t * (0.4 + back * 0.6) + fi * u_stagger * 0.13);
|
||||
vec2 pos = node.xy + vec2((phase - 0.5) * 0.6, 0.0);
|
||||
|
||||
float d = castMain(q) * u_size * scale;
|
||||
float a = inkMask(d, uv);
|
||||
col = mix(col, pal(row + c + 2), a * (0.35 + 0.65 * (1.0 - back)));
|
||||
}
|
||||
vec2 q = rot(t * u_spin + fi) * (p - pos) / max(u_size * scale, 1e-3);
|
||||
|
||||
float d = castMain(q) * u_size * scale;
|
||||
col = mix(col, pal(i + 2), inkMask(d, uv) * (0.35 + 0.65 * (1.0 - back)));
|
||||
}
|
||||
|
||||
col += sigGrain(uv);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const soloist = {
|
||||
name: 'Soloist',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.5,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
@@ -32,6 +32,9 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
p = sigCamera(p);
|
||||
p = sigFolded(p);
|
||||
float t = u_time * u_turn + u_seed;
|
||||
// Even a close-up obeys the song's sense of scale — pulled toward it rather
|
||||
// than replaced by it, since a soloist that shrinks to a dot stops being one.
|
||||
float size0 = u_size * mix(1.0, stageScale(), 0.5);
|
||||
|
||||
vec3 col = mix(pal(0) * 0.14, pal(1) * 0.18, length(p) * 0.5);
|
||||
|
||||
@@ -39,17 +42,17 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
for (int i = 6; i >= 1; i--) {
|
||||
if (i > u_echoes) continue;
|
||||
float fi = float(i);
|
||||
float size = u_size * (1.0 + fi * u_echoStep);
|
||||
float size = size0 * (1.0 + fi * u_echoStep);
|
||||
vec2 q = rot(t * (1.0 - fi * 0.15)) * (p - u_offset) / max(size, 1e-3);
|
||||
float d = castMain(q) * size;
|
||||
float a = inkMask(d, uv) * (0.5 / fi);
|
||||
col = mix(col, pal(i + 1), a);
|
||||
}
|
||||
|
||||
vec2 q = rot(t) * (p - u_offset) / max(u_size, 1e-3);
|
||||
float d = castMain(q) * u_size;
|
||||
vec2 q = rot(t) * (p - u_offset) / max(size0, 1e-3);
|
||||
float d = castMain(q) * size0;
|
||||
col = mix(col, pal(2), inkMask(d, uv));
|
||||
col += pal(3) * u_halo * (1.0 - smoothstep(0.0, u_size * 1.5, abs(d))) * 0.5;
|
||||
col += pal(3) * u_halo * (1.0 - smoothstep(0.0, size0 * 1.5, abs(d))) * 0.5;
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(inkValue(col), 1.0);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const swarm = {
|
||||
name: 'Swarm',
|
||||
family: 'organic',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.5,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
@@ -37,18 +37,17 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
for (int i = 0; i < 48; i++) {
|
||||
if (i >= u_count) break;
|
||||
float fi = float(i);
|
||||
vec2 h = hash22(vec2(fi + u_seed * 0.5, fi * 2.3));
|
||||
|
||||
// Each member rides the same flow field, which is what makes it a
|
||||
// swarm rather than a scatter — cohesion decides how strictly.
|
||||
vec2 seed = (h - 0.5) * 2.4;
|
||||
vec2 flow = curl(seed * u_field + t * 0.15, t * 0.1);
|
||||
vec2 pos = seed + flow * mix(0.05, 0.45, u_cohesion);
|
||||
// Each member has a home on the song's lattice and rides the same flow
|
||||
// field away from it, which is what makes it a swarm rather than a
|
||||
// scatter — cohesion decides how far it is allowed to stray.
|
||||
vec3 node = stageNode(fi, float(u_count));
|
||||
vec2 flow = curl(node.xy * u_field + t * 0.15, t * 0.1);
|
||||
vec2 pos = node.xy + flow * mix(0.05, 0.45, u_cohesion);
|
||||
pos += vec2(sin(t * 0.7 + fi), cos(t * 0.6 + fi * 1.3)) * 0.06;
|
||||
|
||||
vec2 q = (p - pos) / max(u_size, 1e-3);
|
||||
vec2 q = (p - pos) / max(u_size * node.z, 1e-3);
|
||||
q = rot(atan(flow.y, flow.x)) * q; // they face where they go
|
||||
float d = castChorus(q) * u_size;
|
||||
float d = castChorus(q) * u_size * node.z;
|
||||
|
||||
col = mix(col, pal(i + 1), inkMask(d, uv));
|
||||
col += pal(2) * u_trail * 0.004 / (0.02 + abs(d));
|
||||
|
||||
Reference in New Issue
Block a user