Two helpers made the bulk of it mechanical. `inkStroke` is a drop-in for
sigEdge — the same line at the identity's weight rather than the track's — and
`castForm` is a drop-in for sigForm, same signature so call sites do not change
shape. With those in place the substitution table is one-to-one:
sigForm( -> castForm( sigShape( -> castMain( sigEdge( -> inkStroke(
Forty-seven scenes went through that pass in one run: twenty-six take the cast
and the ink, twenty-one take the ink alone. Then the gate ran over all sixty-five
and found three the pass had broken, which is the entire reason it exists.
Spectrum Sculpture rendered pure black. It had been using sigShape as a RADIAL
METRIC rather than drawing it, and the cast carries notches and a hollow — an
annulus used as a radius turns a sculpture inside out. Reverted to sigShape and
dropped to ink only. The lesson generalises: a scene that reads a form as
geometry is not a scene that draws it, and the classifier cannot tell those
apart from the source.
Eclipse Field stopped honouring its `style` trait. It opts out of surface grain,
so sigEdge was its only style evidence, and the ink replaced it. The trait claim
is now dropped — and so is the lint change that had let inkMask count as style
evidence, which was wrong and was hiding exactly this. A trait is a property of
the track a scene may honour; an artifact is content it draws. Taking the ink
says nothing about whether a scene responds to u_sigLine.
Circuit Bloom went empty at the bottom of its `grown` range, where the pads were
carried by a hairline and the ink's stroke is thinner than the edge it replaced.
Now filled as well as stroked.
Also: the backtick check now covers every shader literal rather than only the
preamble, because a mechanical pass over sixty files reintroduced one
immediately. Three rounds lost to that typo is enough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
5.5 KiB
JavaScript
126 lines
5.5 KiB
JavaScript
// Minimal family: four or five of the track's signature form stacked one on
|
|
// another on an empty plain, leaning further than they should, with a low sun
|
|
// throwing the whole stack across the ground.
|
|
//
|
|
// Salt Flat is the other minimal with a horizon and an object, and its object is
|
|
// one, tiny and miles away: the subject there is the emptiness. Here the subject
|
|
// is close, it is the only thing in the frame, and it is a structure with a
|
|
// problem — it leans, and the low sun throws it across the whole plain. Nearly
|
|
// still, and the stillness is tense rather than calm.
|
|
//
|
|
// No declared slow axis. The sun crossing is the one that should qualify — it
|
|
// regrades the entire sky — and it measures 0.93x against the noise floor,
|
|
// because in a frame this empty the camera's drift moves the average about as
|
|
// much as anything in the scene can. Left undeclared rather than overclaimed.
|
|
|
|
export const balanceStack = {
|
|
name: 'Balance Stack',
|
|
family: 'minimal',
|
|
kind: 'fragment',
|
|
consumes: ['cast', 'ink'],
|
|
traits: ['shape', 'camera', 'space', 'style'],
|
|
|
|
params: {
|
|
count: { type: 'int', range: [2, 6], default: 4, uniform: 'u_count', bias: 'density' },
|
|
lean: { type: 'float', range: [-0.5, 0.5], default: 0.12, uniform: 'u_lean' },
|
|
block: { type: 'float', range: [0.08, 0.4], default: 0.2, uniform: 'u_block' },
|
|
taper: { type: 'float', range: [0.5, 1.0], default: 0.82, uniform: 'u_taper' },
|
|
sun: { type: 'float', range: [-1.0, 1.0], default: -0.6, uniform: 'u_sun' },
|
|
shadow: { type: 'float', range: [0, 1], default: 0.6, uniform: 'u_shadow' },
|
|
rim: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_rim', bias: 'energy' },
|
|
sway: { type: 'float', range: [0.01, 0.2], default: 0.04, uniform: 'u_sway', bias: 'motion', rate: true },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
rim: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
|
shadow: { feature: 'bandLow', amount: 0.2, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_sway + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
float ground = sigHorizonY() * 0.4 - 0.35;
|
|
float above = p.y - ground;
|
|
|
|
// Sky and plain. Two flat tones and a line: everything else in the frame is
|
|
// the stack.
|
|
// The sun's position lights the whole frame, not just the stack: the glow
|
|
// sits where it is on the horizon and the plain takes its colour from it.
|
|
// That is what makes the sun crossing the sky the scene's long journey.
|
|
float sunX = u_sun * 1.2;
|
|
float toSun = abs(p.x - sunX);
|
|
|
|
// The sky is graded ACROSS the frame toward the sun as well as up it, so
|
|
// where the sun stands recolours everything rather than adding a spot.
|
|
vec3 sky = mix(pal(1) * 0.22, pal(0) * 0.07, sat(above * 0.5 + 0.1));
|
|
vec3 warm = mix(pal(3) * 0.3, pal(2) * 0.22, sat(above * 0.6 + 0.2));
|
|
vec3 col = mix(sky, warm, sat(1.0 - toSun * 0.55));
|
|
col += pal(3) * exp(-abs(above) * 8.0) * exp(-toSun * 0.8) * 0.9;
|
|
col += pal(3) * exp(-abs(above) * 2.0) * exp(-toSun * 1.6) * 0.35;
|
|
if (above < 0.0) {
|
|
col = mix(pal(2) * 0.16, pal(0) * 0.09, sat(-above * 1.6));
|
|
col += pal(3) * exp(above * 4.0) * exp(-toSun * 1.2) * 0.5;
|
|
}
|
|
|
|
// The sun is low and to one side; everything about the light comes from it.
|
|
vec2 lightDir = normalize(vec2(u_sun, 0.55));
|
|
|
|
// The shadow first, so the stack sits on top of its own.
|
|
float shade = 0.0;
|
|
float y = ground;
|
|
float x = 0.0;
|
|
// lint: fixed-cost — at most six blocks in the stack
|
|
for (int i = 0; i < 6; i++) {
|
|
if (i >= u_count) break;
|
|
float fi = float(i);
|
|
float size = u_block * pow(u_taper, fi);
|
|
// Stacked by accumulation, so each block actually rests on the one
|
|
// below whatever the taper is doing.
|
|
y += size;
|
|
x += u_lean * size * 1.6 + sin(t * 1.3 + fi * 0.7) * 0.012 * fi;
|
|
float h = y;
|
|
|
|
// Cast along the ground, stretched by how high the block sits.
|
|
vec2 at = vec2(x - lightDir.x * (h - ground) * 2.2, ground - 0.02);
|
|
vec2 q = (p - at) / vec2(size * 2.4, size * 0.32);
|
|
shade = max(shade, exp(-dot(q, q) * 1.6));
|
|
y += size;
|
|
}
|
|
col = mix(col, pal(0) * 0.05, shade * u_shadow * smoothstep(0.02, -0.02, above));
|
|
|
|
// The stack. Drawn back to front, which for a vertical pile is bottom up.
|
|
y = ground;
|
|
x = 0.0;
|
|
// lint: fixed-cost — the same six blocks
|
|
for (int i = 0; i < 6; i++) {
|
|
if (i >= u_count) break;
|
|
float fi = float(i);
|
|
float size = u_block * pow(u_taper, fi);
|
|
y += size;
|
|
x += u_lean * size * 1.6 + sin(t * 1.3 + fi * 0.7) * 0.012 * fi;
|
|
|
|
vec2 at = vec2(x, y);
|
|
float d = castMain((p - at) / size) * size;
|
|
|
|
// Body: shaded across the form, so it has a lit side and a dark side.
|
|
float lit = sat(dot(normalize(p - at + 1e-4), lightDir) * 0.5 + 0.5);
|
|
vec3 body = mix(pal(0) * 0.08, pal(2) * 0.4, lit) * (0.65 + 0.35 * (1.0 - fi * 0.12));
|
|
col = mix(col, body, smoothstep(0.004, -0.004, d));
|
|
|
|
// Rim: the sun catching the edge, in the track's line weight.
|
|
col += pal(4) * inkStroke(d) * (0.3 + u_rim * 0.9) * (0.4 + lit * 0.8);
|
|
y += size;
|
|
}
|
|
|
|
col = sigAir(col, p, smoothstep(0.1, 1.5, abs(p.x) * 0.7 + sat(above * 0.5)));
|
|
col += sigGrain(uv);
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default balanceStack;
|