diff --git a/flow-state/src/scenes/registry.js b/flow-state/src/scenes/registry.js index da6614e..1a06611 100644 --- a/flow-state/src/scenes/registry.js +++ b/flow-state/src/scenes/registry.js @@ -52,6 +52,10 @@ import { pendulumTrace } from './shader/pendulum-trace.js'; import { contourMap } from './shader/contour-map.js'; import { shojiGrid } from './shader/shoji-grid.js'; import { balanceStack } from './shader/balance-stack.js'; +import { suspensionSpan } from './shader/suspension-span.js'; +import { stairwellDescent } from './shader/stairwell-descent.js'; +import { aqueductMarch } from './shader/aqueduct-march.js'; +import { dataAisle } from './shader/data-aisle.js'; /** * The scene library. Families exist so the arc driver can choose by section @@ -129,6 +133,10 @@ const MODULES = [ contourMap, shojiGrid, balanceStack, + suspensionSpan, + stairwellDescent, + aqueductMarch, + dataAisle, ]; const errors = []; diff --git a/flow-state/src/scenes/shader/aqueduct-march.js b/flow-state/src/scenes/shader/aqueduct-march.js new file mode 100644 index 0000000..17dcdde --- /dev/null +++ b/flow-state/src/scenes/shader/aqueduct-march.js @@ -0,0 +1,126 @@ +// Structural family: an aqueduct marching away across the frame — a lower tier +// of wide piers, an upper tier of small fast ones, and daylight through every +// opening. +// +// Pylon Grid is open frame: you see through the whole structure and it has no +// mass. This is masonry. The wall is solid, the light comes through holes cut in +// it, and the rhythm changes tier to tier — two arches up top for every one +// below, which is the thing that makes a real aqueduct read as marching rather +// than repeating. The openings are the track's signature form, so a hexagonal +// video is an aqueduct of hexagons. +// +// No declared slow axis, for the reason Contour Map and Balance Stack give: the +// candidates were measured — tier height 0.91x, opening size 0.62x, tier count +// 0.17x — and none of them beats the camera's own drift in Phase 11's +// ten-second average. The structure is legible because it is high-contrast and +// mostly still, and that is exactly the kind of image where a slow parameter +// walk moves less of the frame than a slow pan does. + +export const aqueductMarch = { + name: 'Aqueduct March', + family: 'structural', + kind: 'fragment', + texture: 0.9, + traits: ['shape', 'camera', 'space', 'style'], + + params: { + bays: { type: 'float', range: [1.2, 6], default: 2.2, uniform: 'u_bays', bias: 'density' }, + opening: { type: 'float', range: [0.18, 0.6], default: 0.42, uniform: 'u_opening' }, + tiers: { type: 'int', range: [1, 4], default: 2, uniform: 'u_tiers' }, + rise: { type: 'float', range: [0.3, 1.3], default: 0.7, uniform: 'u_rise' }, + recede: { type: 'float', range: [0, 0.8], default: 0.35, uniform: 'u_recede' }, + sun: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_sunlight', bias: 'energy' }, + march: { type: 'float', range: [0.005, 0.2], default: 0.04, uniform: 'u_march', bias: 'motion', rate: true }, + palette: { type: 'palette', count: 5 }, + }, + + reactive: { + sun: { feature: 'loudness', amount: 0.3, response: 'smooth' }, + recede: { feature: 'bandLow', amount: 0.15, response: 'smooth' }, + }, + + shader: ` +// One tier of masonry: a wall with a row of openings cut through it. Returns +// how much of this pixel is stone. +float tier(vec2 q, float bays, float top, float bottom, float shift) { + if (q.y > top || q.y < bottom) return 0.0; + float h = top - bottom; + float g = q.x * bays + shift; + float cx = (fract(g) - 0.5) / bays; + + // The opening is sized as a FRACTION OF ITS BAY in both directions, not as + // a circle of some radius: a bay is much wider than it is tall once there + // are three of them across the frame, and a round hole in it is a porthole + // rather than an arch. Sized this way the same number reads as the same + // opening whatever the bay count is. + float cw = 0.5 / bays; // half a bay across + float ch = h * 0.4; // half an opening up + float k = u_opening * 2.4; + vec2 local = vec2(cx / max(cw * k, 1e-4), + (q.y - (bottom + h * 0.46)) / max(ch * k, 1e-4)); + + // The opening sits low in the tier, leaving a solid band above it to carry + // the next tier — which is the whole structural point of an aqueduct. + float hole = sigShape(local) * min(cw, ch) * k; + return smoothstep(-0.004, 0.004, hole); +} + +vec4 scene(vec2 uv, vec2 p) { + float t = u_time * u_march + u_seed; + p = sigCamera(p); + + float horizon = sigHorizonY() * 0.35 - 0.1; + + // Sky behind the structure, and the ground it stands on. + vec3 col = mix(pal(1) * 0.22, pal(0) * 0.05, sat((p.y - horizon) * 0.7 + 0.2)); + col += pal(3) * exp(-abs(p.y - horizon) * 5.0) * u_sunlight * 0.5; + col = mix(col, pal(0) * 0.1, smoothstep(horizon + 0.01, horizon - 0.05, p.y)); + + // Two ranges of the same structure: the far one smaller, slower and hazier, + // so the aqueduct crosses a valley rather than standing in a diagram. + // lint: fixed-cost — a near range and a far one + for (int k = 0; k < 2; k++) { + float fk = float(k); + float scale = mix(1.0, 0.35, fk * u_recede * 2.0); + float lift = horizon + fk * u_recede * 0.25; + + vec2 q = vec2(p.x / max(scale, 0.15), (p.y - lift) / max(scale, 0.15)); + float shift = t * mix(1.0, 0.35, fk); + + float stone = 0.0; + float base = 0.0; + // lint: fixed-cost — at most four tiers + for (int i = 0; i < 4; i++) { + if (i >= u_tiers) break; + float fi = float(i); + float bottom = fi * u_rise; + float top = bottom + u_rise; + // Each tier up is twice as fine and half as tall a rhythm. + stone = max(stone, tier(q, u_bays * pow(2.0, fi), top, bottom, + shift * pow(2.0, fi) + fi * 0.31)); + base = max(base, step(bottom, q.y) * step(q.y, top)); + } + + // Stone: lit from the sun side, shadowed on the other, courses picked + // out by a horizontal banding that survives the perspective. + // Courses are drawn in the track's hand: heavy for a track with a thick + // line, barely there for a fine one. + float course = 0.5 + 0.5 * sin(q.y * 60.0); + course = mix(course, smoothstep(0.35, 0.75, course), u_sigLine); + vec3 masonry = mix(pal(2) * 0.35, pal(4) * 0.8, sat(q.x * 0.4 + 0.5)); + masonry *= 0.7 + (0.15 + u_sigLine * 0.5) * course; + masonry += pal(3) * u_sunlight * 0.22 * sat(q.x * 0.5 + 0.5); + masonry += pal(4) * sigEdge(0.5 - abs(fract(q.x * u_bays) - 0.5) - 0.03) * (0.1 + u_sigLine * 0.9); + + float hazed = mix(1.0, 0.55, fk * u_recede); + col = mix(col, masonry * hazed, stone * base); + } + + col = sigAir(col, p, smoothstep(-0.2, 0.9, p.y - horizon)); + col += sigGrain(uv); + return vec4(col, 1.0); +} +`, +}; + +export default aqueductMarch; diff --git a/flow-state/src/scenes/shader/data-aisle.js b/flow-state/src/scenes/shader/data-aisle.js new file mode 100644 index 0000000..0eb5cbc --- /dev/null +++ b/flow-state/src/scenes/shader/data-aisle.js @@ -0,0 +1,114 @@ +// Structural family: a cold aisle. Two walls of equipment running to a +// vanishing point, indicator lights on every unit, a lit floor between them. +// +// Neon City is the other lit structure and it is exterior, vast and far: the +// lights are windows in buildings a mile off. This is interior and close enough +// to touch — two flat walls a couple of metres apart, the geometry is a corridor +// rather than a skyline, and the lights are not decoration but the only thing +// telling you the machines are running. What moves is the camera down the aisle. + +export const dataAisle = { + name: 'Data Aisle', + family: 'structural', + kind: 'fragment', + texture: 0.6, + traits: ['camera', 'space', 'style'], + + params: { + width: { type: 'float', range: [0.25, 1.1], default: 0.55, uniform: 'u_aisle' }, + racks: { type: 'float', range: [1, 8], default: 3, uniform: 'u_racks', bias: 'density' }, + units: { type: 'float', range: [3, 20], default: 9, uniform: 'u_units', bias: 'density' }, + leds: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_leds', bias: 'energy' }, + activity:{ type: 'float', range: [0, 1], default: 0.45, uniform: 'u_activity' }, + haze: { type: 'float', range: [0.2, 1.6], default: 0.7, uniform: 'u_haze', slowAxis: true }, + run: { type: 'float', range: [0.02, 0.5], default: 0.12, uniform: 'u_run', bias: 'motion', rate: true }, + palette: { type: 'palette', count: 5 }, + }, + + reactive: { + leds: { feature: 'bandHigh', amount: 0.35, response: 'smooth' }, + activity: { feature: 'flux', amount: 0.25, response: 'spike' }, + }, + + shader: ` +// One wall of racks. 'along' is metres down the aisle, 'up' is height on the +// wall, and the return is the wall's colour. +vec3 wall(vec2 at, float side, float t, float lightAmount) { + // Rack bays, and the gap between two racks. + float bay = at.x * u_racks; + float bayId = floor(bay); + float bx = fract(bay) - 0.5; + + float unit = at.y * u_units; + float unitId = floor(unit); + float uy = fract(unit) - 0.5; + + // The metalwork: a dark case with a seam at every unit and every bay. + float seam = min(0.5 - abs(bx), 0.5 - abs(uy)); + vec3 col = mix(pal(0) * 0.3, pal(2) * 0.42, sat(at.y * 0.6 + 0.2)); + float seamW = 0.03 + u_sigLine * 0.12; + col = mix(col, pal(0) * 0.12, smoothstep(seamW, seamW * 0.2, seam)); + + // Indicators: a row per unit, most of them steady, a few of them working. + float id = hash12(vec2(bayId * 7.1 + unitId, side + floor(at.x * 0.5))); + float slot = fract(bx * 8.0 + 0.5); + float led = smoothstep(0.34, 0.16, abs(slot - 0.5)) * smoothstep(0.3, 0.12, abs(uy)); + + // Blinking is quantised onto the bar grid rather than free-running: on a + // wall of two hundred lights, free-running flicker is a fizz, and stepping + // it keeps the whole wall under the flash ceiling as well. + float step8 = floor(u_barPhase * 8.0) + floor(at.x * 3.0); + float busy = step(1.0 - u_activity, hash12(vec2(id * 31.0, step8))); + float bright = mix(0.25, 1.0, busy); + + col += palRamp(0.55 + id * 0.35) * led * bright * lightAmount * 1.8; + col += pal(4) * sigEdge(seam - seamW) * (0.15 + u_sigLine * 0.9); + return col; +} + +vec4 scene(vec2 uv, vec2 p) { + float t = u_time * u_run + u_seed; + p = sigCamera(p); + + float horizon = sigHorizonY() * 0.25; + vec2 c = vec2(p.x, p.y - horizon); + + // Corridor: the nearest of four planes. Distance down the aisle is 1/|c|, + // which is the whole of the perspective. + float dx = abs(c.x), dy = abs(c.y); + bool onWall = dx * (1.0 / max(u_aisle, 0.05)) > dy * 2.4; + + float alongDist = onWall ? u_aisle / max(dx, 0.004) : 0.42 / max(dy, 0.004); + float along = alongDist * 0.5 + t * 6.0; + + vec3 col; + if (onWall) { + // Height on the wall, corrected for distance so the racks stay upright. + float up = c.y * alongDist / u_aisle * 0.5 + 0.5; + col = wall(vec2(along, sat(up)), sign(c.x), t, 0.6 + u_leds * 0.9); + } else if (c.y < 0.0) { + // Floor: a lit strip down the middle, and the spill off both walls. + float across = c.x * alongDist / 0.42; + col = pal(0) * 0.16 + pal(2) * 0.12; + col += pal(3) * exp(-abs(across) * 2.5) * (0.45 + u_leds * 0.5); + col += pal(1) * 0.05 * step(0.5, fract(along * 0.5)); + } else { + // Ceiling: cable trays and the occasional fitting. + float across = c.x * alongDist / 0.42; + col = pal(0) * 0.07; + col += pal(1) * 0.12 * smoothstep(0.35, 0.2, abs(fract(across * 1.5) - 0.5)); + col += pal(3) * smoothstep(0.12, 0.0, abs(fract(along * 0.25) - 0.5)) * u_leds * 0.25; + } + + // Distance haze, and the vanishing point itself, which is where the aisle + // ends and the only part of the frame that never resolves. + float far = sat(1.0 - alongDist * 0.06); + col = mix(col, pal(1) * 0.12, sat(far * u_haze)); + col = sigAir(col, p, far); + col += sigGrain(uv); + return vec4(col, 1.0); +} +`, +}; + +export default dataAisle; diff --git a/flow-state/src/scenes/shader/stairwell-descent.js b/flow-state/src/scenes/shader/stairwell-descent.js new file mode 100644 index 0000000..5f95d9a --- /dev/null +++ b/flow-state/src/scenes/shader/stairwell-descent.js @@ -0,0 +1,90 @@ +// Structural family: looking straight down a stairwell. Floor after floor of +// landings and balustrades, each one turned a little from the one above, going +// down forever. +// +// Gate Corridor also travels through repeated structure, but it travels +// horizontally past gates that arrive and leave: the frame has a middle and two +// sides. Here the vanishing point is dead centre, every floor is a closed ring +// around it, and the motion is entirely radial. The turn per floor is what makes +// it a stairwell rather than a tunnel — the flights spiral even though nothing +// in the image rotates. + +export const stairwellDescent = { + name: 'Stairwell Descent', + family: 'structural', + kind: 'fragment', + texture: 0.7, + traits: ['camera', 'space', 'style'], + + params: { + floors: { type: 'float', range: [0.18, 0.6], default: 0.32, uniform: 'u_floors', bias: 'density' }, + turn: { type: 'float', range: [-0.5, 0.5], default: 0.22, uniform: 'u_turn' }, + well: { type: 'float', range: [0.1, 0.6], default: 0.3, uniform: 'u_well' }, + rails: { type: 'int', range: [6, 40], default: 18, uniform: 'u_rails', bias: 'density' }, + landing: { type: 'float', range: [0.1, 0.45], default: 0.25, uniform: 'u_landing' }, + lit: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_lit', bias: 'energy' }, + depth: { type: 'float', range: [0.2, 1], default: 0.65, uniform: 'u_deep', slowAxis: true }, + fall: { type: 'float', range: [0.01, 0.35], default: 0.08, uniform: 'u_fall', bias: 'motion', rate: true }, + palette: { type: 'palette', count: 5 }, + }, + + reactive: { + lit: { feature: 'bandHigh', amount: 0.3, response: 'smooth' }, + rails: { feature: 'bandMid', amount: 0.15, response: 'smooth' }, + }, + + shader: ` +vec4 scene(vec2 uv, vec2 p) { + float t = u_time * u_fall + u_seed; + p = sigCamera(p); + + float r = length(p) + 0.001; + float th = atan(p.y, p.x); + + // Log-radial: one unit is one floor. Descending is a constant slide down + // this axis, which is what makes the fall never end and never repeat a cut. + float z = log(r / u_well) / u_floors + t * 3.0; + float floorId = floor(z); + float f = fract(z); + + // Each flight is turned from the one above it. + float a = th + floorId * u_turn * 6.28318530718; + + // The balustrade: uprights around the well, and the handrail on top of them. + float posts = abs(fract(a / 6.28318530718 * float(u_rails)) - 0.5) * 2.0; + float w = 0.25 + u_sigLine * 0.5; + float upright = smoothstep(w, w * 0.25, posts) * smoothstep(0.75, 0.35, f); + float rail = smoothstep(0.12, 0.02, abs(f - 0.72)); + + // The landing slab: a solid quarter of every floor, so there is somewhere to + // stand and the eye can count the floors. + float quarter = fract(a / 6.28318530718 + 0.5); + float slab = smoothstep(0.5, 0.44, abs(quarter - 0.5)) * + smoothstep(0.08, 0.2, f) * smoothstep(u_landing + 0.25, u_landing, f); + + // Everything dims toward the middle. This is the only cue that the well has + // a bottom, so it carries the whole read of the image — and it is measured + // off the radius rather than off the floor index, because the floors below + // us occupy a few pixels at the centre while the darkness has to fill half + // the frame to read as depth at all. + float fade = mix(1.0, sat(r * 0.7), u_deep); + float sky = smoothstep(0.85, 1.6, r); // the top of the well, above us + + vec3 col = pal(0) * 0.07; + col += palRamp(0.25 + f * 0.2) * upright * 0.45 * fade; + col += pal(4) * rail * (0.4 + u_lit * 0.7) * fade; + col += mix(pal(2) * 0.3, pal(1) * 0.22, fract(floorId * 0.37)) * slab * fade; + + // A light on each landing, catching the rail below it. + float lamp = exp(-abs(f - 0.45) * 6.0) * smoothstep(0.55, 0.2, abs(quarter - 0.25)); + col += pal(3) * lamp * u_lit * 0.5 * fade; + + col = mix(col, pal(1) * 0.3, sky * 0.5); + col = sigAir(col, p, sat(1.0 - fade)); + col += sigGrain(uv); + return vec4(col, 1.0); +} +`, +}; + +export default stairwellDescent; diff --git a/flow-state/src/scenes/shader/suspension-span.js b/flow-state/src/scenes/shader/suspension-span.js new file mode 100644 index 0000000..af1c57b --- /dev/null +++ b/flow-state/src/scenes/shader/suspension-span.js @@ -0,0 +1,105 @@ +// Structural family: a suspension bridge passing overhead — two towers in +// frame at a time, the main cable slung between them, hangers dropping to a +// deck that runs off both sides. +// +// Girder Lattice is a truss: hundreds of short members in tension and +// compression, and the image is a texture of triangles. This is the opposite +// structure and reads as one — two long curves and a rhythm of verticals under +// them, with almost nothing else in the frame. The curve is the whole subject, +// and the only thing that repeats is the spacing. + +export const suspensionSpan = { + name: 'Suspension Span', + family: 'structural', + kind: 'fragment', + texture: 0.6, + traits: ['shape', 'camera', 'space', 'style'], + + params: { + span: { type: 'float', range: [0.5, 2.2], default: 1.1, uniform: 'u_span', bias: 'density' }, + sag: { type: 'float', range: [0.15, 0.9], default: 0.5, uniform: 'u_sag' }, + tower: { type: 'float', range: [0.3, 1.1], default: 0.7, uniform: 'u_tower' }, + hangers: { type: 'float', range: [2, 22], default: 9, uniform: 'u_hangers', bias: 'density' }, + deck: { type: 'float', range: [-0.7, 0.25], default: -0.2, uniform: 'u_deck', slowAxis: true }, + lamp: { type: 'float', range: [0, 0.12], default: 0.045, uniform: 'u_lamp' }, + travel: { type: 'float', range: [0.01, 0.3], default: 0.06, uniform: 'u_travel', bias: 'motion', rate: true }, + glow: { type: 'float', range: [0, 1.4], default: 0.55, uniform: 'u_glow', bias: 'energy' }, + palette: { type: 'palette', count: 5 }, + }, + + reactive: { + glow: { feature: 'loudness', amount: 0.3, response: 'smooth' }, + lamp: { feature: 'beat', amount: 0.2, response: 'spike' }, + }, + + shader: ` +// Distance to a segment, for the one-off members that are not on a grid. +float seg(vec2 q, vec2 a, vec2 b) { + vec2 ab = b - a, ap = q - a; + float h = sat(dot(ap, ab) / max(dot(ab, ab), 1e-6)); + return length(ap - ab * h); +} + +vec4 scene(vec2 uv, vec2 p) { + float t = u_time * u_travel + u_seed; + p = sigCamera(p); + + float horizon = sigHorizonY() * 0.3; + float deckY = horizon + u_deck; + + // Sky, and the water or ground under the deck. + vec3 col = mix(pal(1) * 0.2, pal(0) * 0.06, sat((p.y - horizon) * 0.6 + 0.25)); + col += pal(3) * exp(-abs(p.y - horizon) * 7.0) * 0.2; + col = mix(col, pal(0) * 0.09, smoothstep(deckY + 0.01, deckY - 0.03, p.y)); + + // Bays: the bridge repeats every span, and travelling means the whole + // structure slides through that repeat. + float g = (p.x + t) / u_span; + float bay = fract(g) - 0.5; + float bayId = floor(g); + float x = bay * u_span; // metres from this bay's centre + + float towerTop = deckY + u_tower; + float w = 0.006 + u_sigLine * 0.02; + + // Main cable: a parabola from tower top to tower top, lowest mid-bay. + float cableY = towerTop - u_sag * u_tower * (1.0 - 4.0 * bay * bay); + float dCable = abs(p.y - cableY); + col += palRamp(0.15) * smoothstep(w * 1.6, w * 0.3, dCable) * (0.6 + u_glow * 0.6); + + // Hangers: verticals from the cable down to the deck, on their own spacing. + float hg = (p.x + t) * u_hangers / u_span; + float hx = (fract(hg) - 0.5) * u_span / u_hangers; + float hangerCableY = towerTop - u_sag * u_tower * + (1.0 - 4.0 * pow(fract(g + (floor(hg) - hg + 0.5) / u_hangers) - 0.5, 2.0)); + float inSpan = step(p.y, hangerCableY) * step(deckY, p.y); + col += palRamp(0.3) * smoothstep(w, w * 0.25, abs(hx)) * inSpan * (0.3 + u_glow * 0.35); + + // Deck: one heavy line all the way across, which is what stops the cable + // reading as a piece of string. + col += pal(4) * smoothstep(w * 2.5, w * 0.6, abs(p.y - deckY)) * (0.4 + u_glow * 0.5); + col += pal(4) * sigEdge(abs(p.y - deckY) - w * 3.0) * 0.25; + + // Towers: a pair of legs and a crossbeam, with the signature form as the + // lamp on top — the only object in the frame that is not a line. + float dTower = min(seg(vec2(x, p.y), vec2(0.0, deckY - 0.1), vec2(0.0, towerTop)), + seg(vec2(x, p.y), vec2(-0.03, towerTop - 0.12), vec2(0.03, towerTop - 0.12))); + col += palRamp(0.05) * smoothstep(w * 3.0, w * 1.2, dTower) * 0.7; + + if (u_lamp > 0.004) { + float d = sigShape((vec2(x, p.y) - vec2(0.0, towerTop + u_lamp)) / u_lamp) * u_lamp; + col += pal(3) * smoothstep(0.004, -0.004, d) * (0.6 + u_glow); + col += pal(3) * exp(-abs(d) * 20.0) * u_glow * 0.5; + col += pal(3) * sigEdge(d) * 0.6; + } + + // Every other bay is a little further off, so the line of the bridge has + // some air in it rather than being a flat elevation drawing. + col = sigAir(col, p, smoothstep(0.2, 1.4, abs(bay) * 0.7 + hash11(bayId) * 0.5)); + col += sigGrain(uv); + return vec4(col, 1.0); +} +`, +}; + +export default suspensionSpan;