Three organic scenes: a reaction, a mat and a surface
Turing Bloom runs activator-inhibitor in the feedback buffer, so the pattern is formed rather than drawn — the one organic here that will not go uniform. It declares no slow axis on purpose, and says why: its own convergence path moves the ten-second average by 0.10, and every candidate axis measured under that. Mycelium Web puts an organic on the ground instead of standing in front of the camera; the colony front is legible as area, which is also what makes its axis measurable. Scale Mosaic is a log-polar lattice with a shear, so the rows are spirals and every scale is the track's signature form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
10a4cff0f6
commit
b01e3aff6f
@ -45,6 +45,9 @@ import { timeSmear } from './shader/time-smear.js';
|
|||||||
import { rainColumn } from './shader/rain-column.js';
|
import { rainColumn } from './shader/rain-column.js';
|
||||||
import { magnetLines } from './shader/magnet-lines.js';
|
import { magnetLines } from './shader/magnet-lines.js';
|
||||||
import { karmanStreet } from './shader/karman-street.js';
|
import { karmanStreet } from './shader/karman-street.js';
|
||||||
|
import { turingBloom } from './shader/turing-bloom.js';
|
||||||
|
import { myceliumWeb } from './shader/mycelium-web.js';
|
||||||
|
import { scaleMosaic } from './shader/scale-mosaic.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scene library. Families exist so the arc driver can choose by section
|
* The scene library. Families exist so the arc driver can choose by section
|
||||||
@ -115,6 +118,9 @@ const MODULES = [
|
|||||||
rainColumn,
|
rainColumn,
|
||||||
magnetLines,
|
magnetLines,
|
||||||
karmanStreet,
|
karmanStreet,
|
||||||
|
turingBloom,
|
||||||
|
myceliumWeb,
|
||||||
|
scaleMosaic,
|
||||||
];
|
];
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|||||||
99
flow-state/src/scenes/shader/mycelium-web.js
Normal file
99
flow-state/src/scenes/shader/mycelium-web.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// Organic family: a fungal mat seen in perspective, creeping toward the camera.
|
||||||
|
// Hyphae branch across the substrate, brighten where two threads cross, and the
|
||||||
|
// colony spreads outward from where it started.
|
||||||
|
//
|
||||||
|
// Flora grows upright, symmetric and toward the light; every organic scene in
|
||||||
|
// the library is a thing standing in a space. This one is a thing lying flat in
|
||||||
|
// one — a mat on the ground, receding to the track's horizon — so it reads as
|
||||||
|
// surface rather than as subject, and the branching is lateral and off-centre
|
||||||
|
// instead of radial.
|
||||||
|
|
||||||
|
export const myceliumWeb = {
|
||||||
|
name: 'Mycelium Web',
|
||||||
|
family: 'organic',
|
||||||
|
kind: 'fragment',
|
||||||
|
traits: ['camera', 'space', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
threads: { type: 'float', range: [1.5, 9], default: 3.5, uniform: 'u_threads', bias: 'density' },
|
||||||
|
branch: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_branch' },
|
||||||
|
fine: { type: 'float', range: [0.02, 0.35], default: 0.18, uniform: 'u_fine' },
|
||||||
|
front: { type: 'float', range: [0.4, 6], default: 1.6, uniform: 'u_front', slowAxis: true },
|
||||||
|
nodes: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_nodes', bias: 'energy' },
|
||||||
|
creep: { type: 'float', range: [0.01, 0.35], default: 0.07, uniform: 'u_creep', bias: 'motion', rate: true },
|
||||||
|
damp: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_damp' },
|
||||||
|
palette: { type: 'palette', count: 5 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
nodes: { feature: 'bandHigh', amount: 0.35, response: 'smooth' },
|
||||||
|
branch: { feature: 'bandMid', amount: 0.25, response: 'smooth' },
|
||||||
|
damp: { feature: 'loudness', amount: 0.2, response: 'smooth' },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
// Hyphae: ridged noise warped by itself, so threads run in bundles and fork
|
||||||
|
// rather than lying in a regular weave.
|
||||||
|
float hyphae(vec2 g, float scale, float warp) {
|
||||||
|
vec2 w = vec2(fbm(g * scale * 0.7 + 3.1, 3), fbm(g * scale * 0.7 - 7.4, 3)) - 0.5;
|
||||||
|
float n = vnoise(g * scale + w * warp * 3.0);
|
||||||
|
return 1.0 - abs(n * 2.0 - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_creep + u_seed;
|
||||||
|
p = sigCamera(p);
|
||||||
|
|
||||||
|
float horizon = sigHorizonY() * 0.45 + 0.25;
|
||||||
|
float below = horizon - p.y;
|
||||||
|
|
||||||
|
// Air above the mat: nothing but the track's wash.
|
||||||
|
vec3 col = mix(pal(1) * 0.16, pal(0) * 0.06, sat((p.y - horizon) * 1.2 + 0.2));
|
||||||
|
|
||||||
|
if (below > 0.001) {
|
||||||
|
// Ground plane. Everything from here on is drawn in substrate
|
||||||
|
// coordinates, so the mat lies flat and the weave compresses with
|
||||||
|
// distance instead of being a flat texture pinned to the frame.
|
||||||
|
float depth = 1.0 / max(below, 0.02);
|
||||||
|
vec2 g = vec2(p.x * depth, depth) * 0.6;
|
||||||
|
g.y -= t * 2.0; // the colony creeps forward
|
||||||
|
|
||||||
|
float coarse = hyphae(g, u_threads, u_branch);
|
||||||
|
float fine = hyphae(g * 2.7 + 11.0, u_threads, u_branch * 0.6);
|
||||||
|
|
||||||
|
float w = u_fine * (0.4 + u_sigLine * 1.6);
|
||||||
|
float thread = smoothstep(w, w * 0.15, 1.0 - coarse);
|
||||||
|
thread += smoothstep(w * 0.6, 0.0, 1.0 - fine) * 0.5;
|
||||||
|
|
||||||
|
// The colony has a front: it is dense where it started and thins out at
|
||||||
|
// the edge of where it has reached.
|
||||||
|
float reach = smoothstep(u_front, u_front * 0.25, length(vec2(g.x, g.y + t * 2.0)) * 0.5);
|
||||||
|
thread *= mix(0.04, 1.0, reach);
|
||||||
|
|
||||||
|
// Anastomosis — where two hyphae meet and fuse. Those junctions are the
|
||||||
|
// only bright points in the mat, so the eye reads it as a network.
|
||||||
|
float node = sat(coarse * fine * 1.8 - 0.55) * reach;
|
||||||
|
|
||||||
|
vec3 mat = mix(pal(0) * 0.12, pal(2) * 0.95, sat(thread));
|
||||||
|
mat += pal(3) * sat(thread) * 0.22;
|
||||||
|
mat += palRamp(0.6 + node) * node * u_nodes * 1.6;
|
||||||
|
mat += pal(4) * sigEdge(1.0 - coarse - w) * u_nodes * 0.2;
|
||||||
|
|
||||||
|
// Wet substrate underneath, so the mat is not floating on black.
|
||||||
|
mat += pal(1) * 0.1 * (1.0 - sat(thread)) * u_damp * smoothstep(0.0, 0.6, below);
|
||||||
|
|
||||||
|
// Outside the front there is substrate and nothing else, so how far the
|
||||||
|
// colony has spread is legible as area rather than as brightness.
|
||||||
|
mat *= mix(0.3, 1.0, reach);
|
||||||
|
|
||||||
|
col = mix(col, mat, smoothstep(0.0, 0.04, below));
|
||||||
|
}
|
||||||
|
|
||||||
|
col = sigAir(col, p, sat(1.0 - below * 1.1));
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default myceliumWeb;
|
||||||
89
flow-state/src/scenes/shader/scale-mosaic.js
Normal file
89
flow-state/src/scenes/shader/scale-mosaic.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Organic family: a mosaic of overlapping scales spiralling out from the centre,
|
||||||
|
// the way a pinecone looks from the tip — every scale the same shape, each one
|
||||||
|
// larger and further round than the one inside it, no straight row anywhere.
|
||||||
|
// The whole surface breathes on the bar.
|
||||||
|
//
|
||||||
|
// Cell Divide is the other packed organic and it is about the event: cells split
|
||||||
|
// and the arrangement changes. Nothing here ever divides. The lattice is fixed
|
||||||
|
// and what moves is the surface itself — scales lifting, tilting and catching
|
||||||
|
// the light. Built in log-polar space with a shear, so the rows ARE the spirals
|
||||||
|
// rather than being rings with a pattern painted on them. Each scale is the
|
||||||
|
// track's signature form, so a hexagonal video is tiled in hexagons.
|
||||||
|
|
||||||
|
export const scaleMosaic = {
|
||||||
|
name: 'Scale Mosaic',
|
||||||
|
family: 'organic',
|
||||||
|
kind: 'fragment',
|
||||||
|
texture: 0.8,
|
||||||
|
traits: ['shape', 'camera', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
arms: { type: 'int', range: [8, 40], default: 22, uniform: 'u_arms', bias: 'density' },
|
||||||
|
growth: { type: 'float', range: [0.1, 0.45], default: 0.22, uniform: 'u_growth', slowAxis: true },
|
||||||
|
twist: { type: 'float', range: [-3, 3], default: 1.6, uniform: 'u_twist' },
|
||||||
|
size: { type: 'float', range: [0.25, 0.48], default: 0.42, uniform: 'u_size' },
|
||||||
|
lift: { type: 'float', range: [0, 0.6], default: 0.25, uniform: 'u_lift' },
|
||||||
|
breathe: { type: 'float', range: [0, 0.3], default: 0.1, uniform: 'u_breathe' },
|
||||||
|
spin: { type: 'float', range: [0.0, 0.35], default: 0.05, uniform: 'u_spin', bias: 'motion', rate: true },
|
||||||
|
sheen: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_sheen', bias: 'energy' },
|
||||||
|
palette: { type: 'palette', count: 5 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
sheen: { feature: 'bandHigh', amount: 0.35, response: 'smooth' },
|
||||||
|
lift: { feature: 'beat', amount: 0.2, response: 'spike' },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_spin + u_seed;
|
||||||
|
p = sigCamera(p);
|
||||||
|
|
||||||
|
float r = length(p) + 0.02;
|
||||||
|
float th = atan(p.y, p.x) + t;
|
||||||
|
|
||||||
|
// Log-polar: one unit of q.y is one ring outward, one unit of q.x is one
|
||||||
|
// scale around. The shear is what turns the rings into spirals — without it
|
||||||
|
// this is a set of concentric rows, which is the one thing a phyllotactic
|
||||||
|
// surface never has.
|
||||||
|
vec2 q = vec2(th / 6.28318530718 * float(u_arms), log(r) / u_growth);
|
||||||
|
q.x += q.y * u_twist;
|
||||||
|
|
||||||
|
vec2 cell = floor(q);
|
||||||
|
vec2 f = fract(q) - 0.5;
|
||||||
|
|
||||||
|
// Overlap: the scale is drawn slightly larger than its cell, so each one
|
||||||
|
// laps over the one below and the surface reads as layered rather than tiled.
|
||||||
|
float lift = sin(cell.y * 1.7 + cell.x * 0.9 + u_barPhase * 6.2831 + hash12(cell) * 6.0) * u_lift;
|
||||||
|
float breath = 1.0 + sin(u_barPhase * 6.28318530718) * u_breathe;
|
||||||
|
float rad = u_size * breath * (1.0 + lift * 0.3);
|
||||||
|
|
||||||
|
// Drawn in cell space, where the cell is the unit square, and converted
|
||||||
|
// back to screen distance at the end. A scale is therefore as wide as its
|
||||||
|
// slice of the turn and as tall as its ring — wider ones further out, which
|
||||||
|
// is what growing on a cone looks like.
|
||||||
|
float d = sigShape(f / max(rad, 1e-3)) * max(rad, 1e-3) * u_growth * r;
|
||||||
|
|
||||||
|
// Shade by lift, so the surface has a direction of light across it.
|
||||||
|
float shade = 0.3 + 0.7 * sat(0.5 + lift * 1.4);
|
||||||
|
vec3 body = palRamp(fract(cell.y * 0.13 + cell.x * 0.037) * 0.5 + 0.2) * shade;
|
||||||
|
|
||||||
|
vec3 col = pal(0) * 0.07;
|
||||||
|
col = mix(col, body * 0.6, smoothstep(0.004, -0.02, d));
|
||||||
|
|
||||||
|
// Rim light along every scale edge, in the track's line weight.
|
||||||
|
col += pal(4) * sigEdge(d) * (0.3 + u_sheen * 0.8);
|
||||||
|
|
||||||
|
// A specular sweep across the whole head, which is what makes hundreds of
|
||||||
|
// separate tiles read as one surface.
|
||||||
|
float sweep = sat(0.5 + 0.5 * sin(dot(p, vec2(0.7, 0.5)) * 2.2 - u_time * 0.35));
|
||||||
|
col += palRamp(0.75) * sweep * u_sheen * 0.2 * smoothstep(0.02, -0.01, d);
|
||||||
|
|
||||||
|
col *= 0.72 + 0.28 * exp(-r * r * 0.3);
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default scaleMosaic;
|
||||||
125
flow-state/src/scenes/shader/turing-bloom.js
Normal file
125
flow-state/src/scenes/shader/turing-bloom.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// Organic family: a reaction–diffusion skin. Short-range activation and
|
||||||
|
// long-range inhibition, run in the feedback buffer, so spots and stripes form
|
||||||
|
// themselves, drift, split, and settle into a living equilibrium.
|
||||||
|
//
|
||||||
|
// Ink Bleed is the other feedback organic, and it only ever spreads: ink goes
|
||||||
|
// outward, fades, and the frame tends to uniform. This tends to the opposite —
|
||||||
|
// a pattern that will not go uniform, because every blob is actively suppressing
|
||||||
|
// its neighbours. Left alone it converges to a scale and then keeps rearranging
|
||||||
|
// at that scale forever, which is the one thing a diffusion scene cannot do.
|
||||||
|
//
|
||||||
|
// A base field is always drawn, so the scene stands alone before the pattern has
|
||||||
|
// grown and survives a seek.
|
||||||
|
//
|
||||||
|
// NO DECLARED SLOW AXIS, deliberately. Phase 11 measures an axis by comparing
|
||||||
|
// ten-second averages, and this scene's own path noise is 0.10 of full range —
|
||||||
|
// the reaction converges to a different arrangement from every start, which is
|
||||||
|
// what a reaction–diffusion system is. Every candidate axis measured 0.08-0.11
|
||||||
|
// against that floor, so a declaration would be a claim the measurement cannot
|
||||||
|
// support. The arc driver still walks a guessed axis, which is the fallback
|
||||||
|
// that case exists for. What develops here over the track is the reaction
|
||||||
|
// itself, and the check has no way to see that.
|
||||||
|
|
||||||
|
export const turingBloom = {
|
||||||
|
name: 'Turing Bloom',
|
||||||
|
family: 'organic',
|
||||||
|
kind: 'fragment',
|
||||||
|
traits: ['camera', 'style'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
grain: { type: 'float', range: [0.004, 0.03], default: 0.012, uniform: 'u_grainR' },
|
||||||
|
gain: { type: 'float', range: [0.4, 3.0], default: 1.5, uniform: 'u_gain', bias: 'energy' },
|
||||||
|
sharpen: { type: 'float', range: [0.1, 1.2], default: 0.55, uniform: 'u_sharpen' },
|
||||||
|
seedIn: { type: 'float', range: [0.003, 0.04], default: 0.008, uniform: 'u_seedIn', bias: 'density' },
|
||||||
|
settle: { type: 'float', range: [0.95, 0.999], default: 0.985, uniform: 'u_settle' },
|
||||||
|
fill: { type: 'float', range: [0.15, 0.85], default: 0.5, uniform: 'u_fill' },
|
||||||
|
pace: { type: 'float', range: [0.006, 0.06], default: 0.015, uniform: 'u_pace', bias: 'motion', rate: true },
|
||||||
|
glow: { type: 'float', range: [0, 1.3], default: 0.5, uniform: 'u_glow' },
|
||||||
|
palette: { type: 'palette', count: 5 },
|
||||||
|
},
|
||||||
|
|
||||||
|
reactive: {
|
||||||
|
sharpen: { feature: 'bandLow', amount: 0.3, response: 'smooth' },
|
||||||
|
seedIn: { feature: 'flux', amount: 0.25, response: 'spike' },
|
||||||
|
glow: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: `
|
||||||
|
// The substrate the pattern grows on. Drawn every frame regardless of what the
|
||||||
|
// feedback buffer holds, so the scene is never waiting on it.
|
||||||
|
vec3 bedColour(vec2 p, float t) {
|
||||||
|
// The substrate is drawn at the pattern's own scale. That is what makes the
|
||||||
|
// reaction radius the scene's long journey and not just a detail setting:
|
||||||
|
// walking it takes the whole image from fine speckled tissue to a coarse
|
||||||
|
// one, feedback or no feedback.
|
||||||
|
float k = 0.012 / clamp(u_grainR, 0.004, 0.03);
|
||||||
|
float bed = fbm(p * 2.4 * k + vec2(t * 0.3, -t * 0.2), 4);
|
||||||
|
// Veined tissue rather than a wash: the pattern has to look like it is
|
||||||
|
// growing on something, and the first seconds after a cut are all substrate.
|
||||||
|
float veins = abs(fbm(p * 3.6 * k + bed * 1.8, 3) - 0.5);
|
||||||
|
vec3 col = mix(pal(0) * 0.1, pal(1) * 0.34, bed);
|
||||||
|
col += pal(2) * smoothstep(0.1, 0.0, veins) * 0.28;
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ring average of the previous frame at radius r, in uv units so the pattern
|
||||||
|
// scale is the same at 720p and at 4K.
|
||||||
|
float ring(vec2 uv, float r) {
|
||||||
|
float s = 0.0;
|
||||||
|
// lint: fixed-cost — an eight-tap ring, not a search
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
float a = float(i) * 0.7853981634;
|
||||||
|
vec3 c = prev(uv + vec2(cos(a), sin(a) * u_aspect) * r);
|
||||||
|
s += dot(c, vec3(0.333));
|
||||||
|
}
|
||||||
|
return s * 0.125;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 scene(vec2 uv, vec2 p) {
|
||||||
|
float t = u_time * u_pace + u_seed;
|
||||||
|
p = sigCamera(p);
|
||||||
|
|
||||||
|
vec3 col = bedColour(p, t);
|
||||||
|
|
||||||
|
float here = dot(prev(uv), vec3(0.333));
|
||||||
|
|
||||||
|
// Activation minus inhibition: the near ring feeds the pattern, the far ring
|
||||||
|
// starves it. The whole of Turing's argument, in two texture rings.
|
||||||
|
float near = ring(uv, u_grainR);
|
||||||
|
float far = ring(uv, u_grainR * 3.2);
|
||||||
|
float react = (near - far) * u_gain;
|
||||||
|
|
||||||
|
// Fresh substrate keeps being stirred in, so a seek always has something to
|
||||||
|
// converge from. It is deliberately STILL in scene space rather than
|
||||||
|
// scrolling: a moving seed field means the pattern that grows is whatever
|
||||||
|
// noise happened to be passing, and the same scene converges somewhere
|
||||||
|
// different every time it is cut to. Pinned, the skin settles into the same
|
||||||
|
// arrangement the substrate implies, and then rearranges within it.
|
||||||
|
float feed = (fbm(p * 6.0 * (0.012 / clamp(u_grainR, 0.004, 0.03)) + 19.0, 3) - 0.45) * u_seedIn;
|
||||||
|
|
||||||
|
// Growth bias: whether the skin is sparse spots on bare substrate or a
|
||||||
|
// covered surface with holes punched in it. Small per frame, but it
|
||||||
|
// accumulates through the feedback, which is what makes it the scene's long
|
||||||
|
// journey rather than a brightness knob.
|
||||||
|
float a = here * u_settle + react * 0.25 + feed + (u_fill - 0.5) * 0.03;
|
||||||
|
// Push toward the two states — hard for a track drawn with a sharp edge,
|
||||||
|
// barely at all for a soft one, so the skin is crisp scales or wet blobs
|
||||||
|
// depending on the track's hand rather than on this scene's taste.
|
||||||
|
a = sat(a + (a - 0.5) * u_sharpen * (1.6 - u_sigSoft));
|
||||||
|
|
||||||
|
vec3 skin = palRamp(0.35 + a * 0.45);
|
||||||
|
col = mix(col, skin, sat(a * 1.6));
|
||||||
|
|
||||||
|
// Rims: where the pattern is changing fastest, which is the edge of every
|
||||||
|
// blob and the seam of every stripe.
|
||||||
|
float rim = sat(abs(near - far) * 6.0);
|
||||||
|
col += pal(4) * rim * u_glow * (0.15 + u_sigLine * 1.8);
|
||||||
|
|
||||||
|
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.2);
|
||||||
|
col += sigGrain(uv);
|
||||||
|
return vec4(col, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default turingBloom;
|
||||||
Loading…
Reference in New Issue
Block a user