// 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;