// Organic family: ink dropped into wet paper, spreading along the fibre. // // The bleed is done with feedback — each frame the previous one is sampled // slightly outward along a noise-warped direction and darkened, which is a // diffusion step in everything but name. Distinct from Curl Flow's trails // (advected along a flow field, so they streak) because this expands in all // directions at once, so it blooms. // // A base field is always drawn, so the scene stands alone before feedback has // converged and survives a seek. export const inkBleed = { name: 'Ink Bleed', family: 'organic', kind: 'fragment', traits: ['camera', 'space', 'style'], params: { drops: { type: 'int', range: [1, 6], default: 3, uniform: 'u_drops', bias: 'density' }, spread: { type: 'float', range: [0.002, 0.02], default: 0.007, uniform: 'u_spread' }, fibre: { type: 'float', range: [0.5, 8], default: 3.0, uniform: 'u_fibre', bias: 'density' }, soak: { type: 'float', range: [0.7, 0.99], default: 0.93, uniform: 'u_soak' }, density: { type: 'float', range: [0.1, 1.2], default: 0.5, uniform: 'u_density', bias: 'energy' }, pace: { type: 'float', range: [0.02, 0.4], default: 0.1, uniform: 'u_pace', bias: 'motion', rate: true }, palette: { type: 'palette', count: 5 }, }, reactive: { density: { feature: 'bandLow', amount: 0.35, response: 'smooth' }, fibre: { feature: 'bandAir', amount: 0.2 }, }, shader: ` vec4 scene(vec2 uv, vec2 p) { float t = u_time * u_pace + u_seed; p = sigCamera(p); // Paper: a still fibre texture that the ink will follow. float grainField = fbm(p * u_fibre * 2.0 + 17.0, 4); vec3 col = mix(pal(0) * 0.09, pal(1) * 0.14, grainField); // Fresh ink. Each drop pulses in and out on its own slow cycle, so the page // is never uniformly saturated and there is always somewhere new bleeding. float ink = 0.0; for (int i = 0; i < 6; i++) { if (i >= u_drops) break; float fi = float(i); float s = u_seed + fi * 61.3; vec2 at = vec2(sin(t * 0.6 + s) * 0.6, cos(t * 0.47 + s * 1.7) * 0.45); float life = 0.5 + 0.5 * sin(t * 1.3 + s * 2.1); float d = length(p - at) + (grainField - 0.5) * 0.15; ink += exp(-d * d * 90.0) * life; } ink = sat(ink) * u_density; vec3 inkColor = palRamp(0.45 + grainField * 0.3); col = mix(col, inkColor, ink); // The bleed: sample the previous frame outward along the fibre. Reading four // offsets rather than one is what makes it spread in every direction instead // of sliding — one sample is a smear, four is diffusion. vec2 warp = (vec2(fbm(p * u_fibre + 3.0, 3), fbm(p * u_fibre - 7.0, 3)) - 0.5) * 2.0; float r = u_spread; vec3 soaked = ( prev(uv + (vec2( 1.0, 0.0) + warp * 0.6) * r) + prev(uv + (vec2(-1.0, 0.0) + warp * 0.6) * r) + prev(uv + (vec2( 0.0, 1.0) + warp * 0.6) * r) + prev(uv + (vec2( 0.0, -1.0) + warp * 0.6) * r) ) * 0.25; col = max(col, soaked * u_soak); col = sigAir(col, p, smoothstep(0.0, 1.7, length(p))); col += sigGrain(uv); return vec4(col, 1.0); } `, }; export default inkBleed;