// Geometric family: a field of extruded blocks seen isometrically, rising and
// falling on the low end. A solid, lit surface with a light direction — three
// faces per block, and the whole grid reads as a landscape rather than a chart.
//
// Floating Geometry is bodies adrift in space with nothing under them. This is
// the opposite proposition: everything is on one grid, everything touches the
// ground, and the only freedom any block has is its height. The plan of each
// block is the track's signature form, so a hexagonal video gets a honeycomb of
// columns rather than a chessboard of them.

export const isometricBlocks = {
    name: 'Isometric Blocks',
    family: 'geometric',
    kind: 'fragment',
    texture: 0.6,
    consumes: ['cast', 'ink'],
    traits: ['shape', 'camera', 'style'],

    params: {
        grid:    { type: 'float', range: [2, 10],      default: 4.5,  uniform: 'u_grid', bias: 'density', slowAxis: true },
        height:  { type: 'float', range: [0.1, 0.7],   default: 0.35, uniform: 'u_height', bias: 'energy' },
        // Displacement: how far each block wanders from its slot on the grid.
        //
        // Every block sat exactly on the lattice, so the plan of the field was
        // the same plan in every song and only the heights moved — which is
        // what the gallery was reporting as a layout distance of 0.005, near
        // enough to nothing. Displacing them per cell puts the SONG in the
        // arrangement rather than only in the elevation.
        scatter: { type: 'float', range: [0, 0.85],   default: 0.3,  uniform: 'u_scatter', bias: 'density' },
        // Extra disturbance AT the song's focal points, on top of the scatter
        // everywhere — not instead of it.
        //
        // It was a trade at first, spending the scatter budget near the focus
        // and calming the rest, and that measured worse than no focus at all:
        // 0.049 down to 0.037. Most of the field went back onto the rigid
        // lattice and took the orientation variety with it, 0.164 to 0.105. The
        // focal point has to be something the field gains, not something the
        // rest of it pays for.
        gather:  { type: 'float', range: [0, 1.6],     default: 0.8,  uniform: 'u_gather' },
        plan:    { type: 'float', range: [0.2, 0.5],   default: 0.4,  uniform: 'u_plan' },
        tilt:    { type: 'float', range: [0.35, 0.75], default: 0.55, uniform: 'u_isoTilt' },
        light:   { type: 'float', range: [0, 1.4],     default: 0.65, uniform: 'u_light' },
        wave:    { type: 'float', range: [0, 1],       default: 0.5,  uniform: 'u_wave' },
        roll:    { type: 'float', range: [0.01, 0.08], default: 0.035, uniform: 'u_roll', bias: 'motion', rate: true },
        palette: { type: 'palette', count: 5 },
    },

    // The low end deliberately does NOT drive the block heights. Measured at
    // aggressive settings that read as 4 flashes a second: every tower in the
    // frame grows and shrinks together, which is a whole-frame luminance cycle
    // on the kick and exactly what the flash ceiling exists to stop. The beat
    // lands on a quarter of the tops instead — same reading, local swing.
    reactive: {
        light: { feature: 'loudness', amount: 0.25, response: 'smooth' },
    },

    shader: `
// How tall the block at this cell stands, in screen units. A travelling wave
// plus a fixed per-cell character, so the field has a rhythm running through it
// and a skyline that is still recognisable between beats.
float columnHeight(vec2 cell, float t) {
    float own = hash12(cell + u_seed);
    float wave = sin(dot(cell, vec2(0.7, 0.5)) - t * 1.1) * 0.5 + 0.5;
    // Squared, so most of the field lies flat and the tall ones stand out as
    // individual towers. A linear height makes every column tall enough to
    // occlude its neighbours, and the field turns into vertical stripes.
    return u_height * pow(mix(own, wave, u_wave), 2.4);
}

vec4 scene(vec2 uv, vec2 p) {
    float t = u_time * u_roll + u_seed;
    p = sigCamera(p);

    float tilt = max(u_isoTilt, 0.1);
    float cw = 1.0 / u_grid;                        // one cell across, on screen

    vec3 col = mix(pal(0) * 0.06, pal(1) * 0.14, sat(p.y * 0.4 + 0.5));

    // A block standing on row r covers screen rows ABOVE r's ground line, so a
    // pixel can only belong to a row at or behind it. Walking a fixed number of
    // rows from the back forward and overwriting as we go is the painter's
    // algorithm, and it is the whole of the depth sorting an isometric field
    // needs — no sorting, no z buffer, just the order of the loop.
    float colX = floor(p.x / cw);
    float fx = (p.x / cw - colX - 0.5) * cw;        // screen offset within the column

    // Alternate columns are staggered half a row, which is what interlocks the
    // field into a lattice instead of leaving it a set of independent stacks.
    float stagger = mod(colX, 2.0) * 0.5;
    float rowHere = floor(p.y / (tilt * cw) - stagger);

    // Rows FURTHER AWAY stand higher up the frame, so a pixel can only belong
    // to a block whose ground line is at or below it — row <= rowHere. Counting
    // down from there draws the far ones first and lets the near ones paint
    // over them.
    // lint: fixed-cost — eight rows of depth, back to front
    for (int j = 7; j >= 0; j--) {
        float row = rowHere - float(7 - j);
        vec2 cell = vec2(colX, row);

        // Where this block would stand if nothing disturbed it, so the focus can
        // be asked about the cell rather than about the pixel.
        float restY = (row + stagger) * tilt * cw;
        float focus = focusField(vec2((colX + 0.5) * cw - 0.5, restY));

        // Each cell steps off its slot by a fixed amount of its own, and how far
        // depends on how close it is to what the song is looking at. Spread
        // evenly this is noise on a grid; concentrated, the lattice stands
        // except where it is disturbed, and the field acquires a subject.
        //
        // Bounded under half a cell: past that blocks cross each other and the
        // isometric read — which is the whole point of the scene — breaks down.
        float amount = u_scatter * (1.0 + focus * u_gather * 2.0);
        vec2 offset = (hash22(cell + u_seed + 3.7) - 0.5) * amount * cw * 0.9;

        float groundY = restY + offset.y;           // where this cell meets the ground
        // Blocks rise toward the focus as well as scattering around it, so the
        // disturbance is something you can see in the skyline rather than only
        // in the plan — and rising changes WHERE the frame's energy is, which
        // scattering alone did not.
        float h = columnHeight(cell, t)
                * (1.0 + offset.x * 2.0)
                * (1.0 + focus * u_gather * u_focusPull * 1.3);
        float dy = p.y - groundY;                   // height up the block face

        // Side wall: the plan swept up from the ground line to the top face.
        float inX = smoothstep(u_plan * cw, u_plan * cw * 0.85, abs(fx - offset.x));
        float wall = inX * step(0.0, dy) * step(dy, h);

        float lit = 0.5 + 0.5 * sat(fx / (u_plan * cw) * 0.8 + 0.5);
        // The wall darkens toward the ground, which is the only shading cue
        // that tells a tall block from a short one at a glance.
        // Neither face takes the loudness. Anything that multiplies a face
        // colour multiplies most of the frame, and a field of blocks brightening
        // together on the kick measured at 5 flashes a second. The light level
        // is expressed on the EDGES, which are a few percent of the image.
        vec3 side = mix(pal(0) * 0.5, pal(2) * 0.8, lit) * 0.75;
        side *= 0.45 + 0.55 * sat(dy / max(h, 1e-3));
        col = mix(col, side, wall);

        // Top face: the track's signature form, lying flat, lifted by h and
        // squashed by the tilt — so every block in the field is the same
        // silhouette as every other subject in the video.
        vec2 q = vec2(fx, (p.y - groundY - h) / tilt);
        float dTop = castMain(q / (u_plan * cw)) * (u_plan * cw);
        float topMask = smoothstep(0.004, -0.004, dTop);

        vec3 top = mix(pal(2), pal(3), sat(h / max(u_height, 1e-3))) * 0.8;
        col = mix(col, top, topMask);
        // A quarter of the blocks answer the beat, chosen by cell rather than
        // by anything global, so the field has a pulse without the frame having
        // a flash.
        float picked = step(0.75, hash12(cell * 3.17 + 5.0));
        col += pal(3) * topMask * picked * u_beat * 0.45;
        col += pal(4) * inkStroke(dTop) * (0.2 + u_light * 1.2) * step(0.0, dy);
    }

    col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.3);
    return vec4(inkValue(col), 1.0);
}
`,
};

export default isometricBlocks;
