Aurora drift

When to use
Use when a held title needs a ground that is alive but stays at the edge of attention: three soft bands of light hang from the top of the frame and fold slowly, in one fragment shader, while the copy sits low and the light thins out above it. Clip: aurora-drift.mp4. Demo: demo/aurora-drift.html.
The code
import '../../core/engine/page-api.js';
import { shaderField } from '../../core/surfaces/shader-field.js';
const css = getComputedStyle(document.documentElement);
const rgb = (name) => { const h = css.getPropertyValue(name).trim(); return [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255); };
const frag = `
uniform vec3 u_ground, u_a, u_b;
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7)) + u_seed * 17.0) * 43758.5453); }
float vnoise(vec2 p) { // value noise: smooth, seeded, no state
vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
}
float band(vec2 p, float k) { // k offsets the band: a different fold each
float fold = vnoise(vec2(p.x * 2.0 + k * 5.0, u_time * 0.65 + k * 2.0));
float centre = 0.88 - 0.15 * k + 0.28 * (fold - 0.5) + 0.07 * sin(p.x * 5.0 + u_time * 2.2 + k * 2.0);
float d = (p.y - centre) / (0.055 + 0.02 * k);
float body = d < 0.0 ? exp(-d * d * 2.2) : exp(-d * d * 0.5); // sharp lower edge, soft fade upward
return body * (0.6 + 0.4 * vnoise(vec2(p.x * 12.0 + k * 3.0, u_time * 0.9))); // fine rays
}
vec3 lin(vec3 c) { return pow(c, vec3(2.2)); }
void main() {
vec2 p = gl_FragCoord.xy / u_res.y;
vec3 ground = lin(u_ground), a = lin(u_a), b = lin(u_b);
vec3 col = ground + a * 0.40 * band(p, 0.0) + mix(a, b, 0.5) * 0.28 * band(p, 1.0) + b * 0.20 * band(p, 2.0);
col = mix(ground, col, 0.15 + 0.85 * smoothstep(0.42, 0.74, p.y)); // the copy sits low: thin the light above it
gl_FragColor = vec4(pow(col, vec3(1.0 / 2.2)) + (hash(gl_FragCoord.xy) - 0.5) * 0.012, 1.0); // static dither
}`;
const field = shaderField(frag, { seed: 3, uniforms: { u_ground: rgb('--ground'), u_a: rgb('--accent'), u_b: rgb('--muted') } });
vawe.onFrame((t) => field.draw(t)); // time is the seek, no loop of its ownEasing and timing
The clip runs 4 s. The look is terminal.
The clip uses one named ease from core/motion/presets.js. Each is a pair of After Effects handles: influence is how far along the segment the handle reaches, and speed is how fast the value moves at the key, as a multiple of the segment's average speed. Motion in the docs explains both.
| Ease | Leaving a key | Arriving at a key |
|---|---|---|
land | 12% influence, speed 4.8 | 75% influence, speed 0 |
Notes
The ground is a pure function of the seek time, so frame t is the same on every render. The three colours are the look's ground, accent and muted tokens: on terminal they give a green aurora over a near-black green. Measured on the 4 s demo clip at 640 x 360: the mean luma change is 0.6 per frame at 60 fps (0.45 once the copy has settled), inside the 0.5 to 3.0 band for a calm ground. The worst contrast of the ink against the lightest and darkest pixel behind the text box is 5.7:1 over 12 frames. Two numbers carry the flow: fold speed (u_time * 0.65 and 2.2 in the sine) and band width. Halve the speeds and the luma change falls under 0.3, which reads as a still. The keep line is the contrast guard: a band behind white mono type at full strength falls to 2.6:1, so keep the bands above the copy. The dither is static on purpose: a dark gradient bands in the encode without it, and a dither that changed every frame would shimmer.
Looks (ground, type, radius, accent) live in demo/demo.css; this snippet keeps neutral tokens.
Sound
Sound: none; a held ground is quiet.





