// Organic family: merging metaballs on an analytic orbit. // // Positions come from closed-form orbits rather than any simulation, which keeps // the scene seek-exact — the same rule the 3D particle layer follows. export const metaballs = { name: 'Metaballs', family: 'organic', kind: 'fragment', // Personality: see look/Personality.js. consumes: ['cast', 'ink', 'staging'], traits: ['shape', 'camera', 'style'], params: { count: { type: 'int', range: [2, 10], default: 5, uniform: 'u_count', bias: 'density' }, radius: { type: 'float', range: [0.1, 0.6], default: 0.3, uniform: 'u_radius', bias: 'energy' }, threshold: { type: 'float', range: [0.4, 2.2], default: 1.0, uniform: 'u_threshold' }, speed: { type: 'float', range: [0.03, 0.5], default: 0.15, uniform: 'u_speed', bias: 'motion', rate: true }, spread: { type: 'float', range: [0.2, 1.1], default: 0.6, uniform: 'u_spread' }, rim: { type: 'float', range: [0, 1], default: 0.45, uniform: 'u_rim' }, palette: { type: 'palette', count: 5 }, }, reactive: { radius: { feature: 'bandLow', amount: 0.25 }, rim: { feature: 'beat', amount: 0.12, response: 'smooth' }, threshold: { feature: 'flux', amount: 0.2, response: 'inverse' }, }, shader: ` vec4 scene(vec2 uv, vec2 p) { float t = u_time * u_speed + u_seed; p = sigCamera(p); float field = 0.0; vec3 tint = vec3(0.0); for (int i = 0; i < 10; i++) { if (i >= u_count) break; float fi = float(i); float s = u_seed + fi * 71.3; // Homes on the song's lattice, orbited by this scene's own motion. vec3 node = stageNode(fi, float(u_count)); vec2 centre = node.xy * u_spread * vec2(1.0, 0.62) + vec2(sin(t * (0.7 + fract(s * 0.13)) + s), cos(t * (0.5 + fract(s * 0.29)) + s * 1.7)) * 0.25; // Distance measured in the song's FORM rather than as a circle: for a // round cast this is exactly length(p - centre), and for a hexagonal // one the blobs merge as hexagons. float radius = u_radius * node.z; float d = castMain((p - centre) / max(radius, 1e-3)) * radius + radius; float contribution = (radius * radius) / max(d * d, 1e-4); field += contribution; tint += pal(i) * contribution; } tint /= max(field, 1e-4); float surface = smoothstep(u_threshold - 0.25, u_threshold + 0.25, field); float rim = smoothstep(u_threshold + 0.35, u_threshold, field) * smoothstep(u_threshold - 0.3, u_threshold, field); vec3 col = pal(0) * 0.06; col = mix(col, tint, surface); // Beat window — rim brightens only where a blob is near a focal point, // so the highlight is a spot travelling over the surface, not a global wash. float metabBeatMask = clamp(focusField(p) * 1.3, 0.0, 1.0) * 0.7 + 0.18; float maskedRim = u_rim + metabBeatMask * u_beat * 0.45; col += tint * rim * maskedRim; return vec4(inkValue(col), 1.0); } `, }; export default metaballs;