vaweGitHub

MovesGrounds

Aurora drift

look: terminal4 saurora-drift.md

Aurora drift: final frame of the background animation
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. Clip: aurora-drift.mp4. Demo: demo/aurora-drift.html.

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 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.

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 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.