Four structural scenes: a span, a well, an arcade and an aisle
Suspension Span is two curves and a rhythm of hangers where Girder Lattice is a texture of members. Stairwell Descent puts the vanishing point dead centre and turns each flight, so the spiral is in the structure rather than in the motion. Aqueduct March is masonry — the light comes through holes cut in a solid wall — and Data Aisle is the interior, close counterpart to Neon City's exterior. Two bugs worth naming: the arch openings compared a cell-local centre against a global coordinate, which put every arch out in the wings and left a blank wall; and the stairwell's depth fade was keyed on floor index, so it only darkened the few pixels at the centre and measured as doing nothing. Aqueduct March declares no slow axis and records the three candidates measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c8658a6d15
commit
50ceb7afa1
@ -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 = [];
|
||||
|
||||
126
flow-state/src/scenes/shader/aqueduct-march.js
Normal file
126
flow-state/src/scenes/shader/aqueduct-march.js
Normal file
@ -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;
|
||||
114
flow-state/src/scenes/shader/data-aisle.js
Normal file
114
flow-state/src/scenes/shader/data-aisle.js
Normal file
@ -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;
|
||||
90
flow-state/src/scenes/shader/stairwell-descent.js
Normal file
90
flow-state/src/scenes/shader/stairwell-descent.js
Normal file
@ -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;
|
||||
105
flow-state/src/scenes/shader/suspension-span.js
Normal file
105
flow-state/src/scenes/shader/suspension-span.js
Normal file
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user