A city of light, seen through a shallow lens.
Frames, bars and lamps stand in rows on five decks, drawn in axonometric, and every one of them is dragged back along one axis into a trail. A lens with a shallow depth of field looks at them: one slanted band is sharp, and everything in front of it or behind it spreads into soft light that adds up into haze. Then it all goes onto grainy film. Play it and the trails run on, the focus racks through the decks, and the grain moves. One row of lamps burns in signal.
The field
javascript·sq2.js
// world axes: u runs down and right, v (travel) up and right, y up
const at = (u, v, y) => [w / 2 + 0.866 * (u + v) * U, h / 2 + (0.5 * (u - v) - y) * U];
// a frame: its outline at the front, its corners dragged back along v, fading out
const trail = gradient(at(u, v, y), at(u, v - len, y), [light, transparent]);
for (const corner of corners) line(at(...corner, v), at(...corner, v - len), trail);
outline(corners.map((c) => at(...c, v)), light);
The lens
javascript·sq2.js
// how far each element sits from the focal plane picks its band; a band is blurred as one,
// and the bands are added together, so light out of focus spreads into haze
const blur = abs(depth(e) - focus) * 1.5;
const band = BANDS.findIndex((edge) => blur <= edge);
draw(e, layers[band], { width: 0.9 + band * 0.9, alpha: e.alpha * (1 - band * 0.08) });
for (const [i, layer] of layers.entries()) composite(layer, { mode: "lighter", filter: `blur(${BLUR[i]}px)` });
composite(layers[0], { mode: "lighter", filter: "blur(5px)", alpha: 0.55 }); // bloom off what is sharp
The film
javascript·sq2.js
// grain in overlay darkens and lightens the light; a sparse dust, added, lifts the black
grain[i] = 128 + gaussian() * 46;
dust[i] = { white, alpha: abs(gaussian()) * 22 };
fill(pattern(grain, offset), "overlay", 0.85);
fill(pattern(dust, offset), "lighter", 0.35); // a fresh offset every frame
References