vaweGitHub

Video componentsGrounds

Aurora drift

Groundsterminal4 s

aurora-drift.md

Aurora drift: final frame of the background animation
00:00.0000:04.00
Tap the clip to play it.

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.

The code

js
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 own

Easing

Each ease is a pair of After Effects handles from core/motion/presets.js. Every curve as CSS.

EaseLeaving a keyArriving at a key
land12% influence, speed 4.875% 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 × 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.

Chain it into a film.

Copy chains of video components from the recipes.