Files
zeekay 91fbe784d0 film/models: each model's concept, drawn by a shader and cut into a loop
Nine concepts to the owner's mockup, grey on true black, the subject right
of centre and the left 40% kept dark for a card's words: Enso a brushed
ensō shedding particles over water and mist, Kai three panes of glass
threaded by falling light, Zen a sphere in raked sand, Iro mountains and
cloud spilling out of a frame, Koma film frames running into depth, Oto a
mirrored waveform, Koe twisting ribbons, Uta a fan rising over strings,
Mori a wireframe peak.

Each is one fragment shader over film/models/gl.js, a pure function of the
loop's phase, so the frame after the last is the first. film.mjs steps it
on the GPU, refuses a seam, and cuts public/models/<name>.{webp,webm,mp4}:
1200x1500 for every model and a 1500x1500 Enso, 30 fps, each loop under
700 KB. Frames stay in film/models/frames (ignored).
2026-10-30 07:49:48 -07:00

80 lines
3.1 KiB
HTML

<!doctype html>
<html data-seconds="6">
<meta charset="utf-8" />
<title>Oto</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Oto: a sound drawn as its waveform, thin bars mirrored about one line,
tallest and brightest at the centre and falling away to either side, with
a fine spray of points over the loudest. In the loop the bars rise and fall
to a few slow beats, every one of them back where it began.
-->
<script type="x-shader/x-fragment">
// A bar's loudness, -1..1 of its reach, at the loop's phase: a few beats that each turn a whole number of times.
float level(float n){
float a = 0.;
a += .55 * sin(TAU * (P * 1. + h11(n * .37 + 1.) ));
a += .3 * sin(TAU * (P * 2. + h11(n * .71 + 5.) ));
a += .2 * sin(TAU * (P * 3. + n * .11));
return .42 + .58 * abs(a) / 1.05;
}
float scene(vec2 f){
vec2 c = vec2(.645 * R.x, .5 * R.y);
float pitch = R.y * .0064;
float x = f.x - c.x;
float y = abs(f.y - c.y);
float reach = R.y * .26;
float L = 0.;
float i0 = floor(x / pitch + .5);
for (int k = -2; k <= 2; k++) {
float n = i0 + float(k);
float side = R.y * .27;
float u = n * pitch / side;
if (abs(u) > 1.25) continue;
// Tallest at the centre, a little uneven, falling to nothing at the ends.
float env = exp(-pow(u / .5, 2.)) * .9 + .1 * exp(-pow(u / 1., 2.));
env *= .8 + .2 * h11(n * 1.9);
float hgt = reach * env * level(n) + 2.;
float dx = abs(x - n * pitch);
float w = .9 + .5 * env;
float core = 1. - smoothstep(w - .5, w + .5, dx);
// Brightest at the waist, dimming toward the tips, the tips themselves catching light.
float along = y / max(hgt, 1.);
float inBar = 1. - smoothstep(hgt - .8, hgt + .8, y);
float lit = (.4 + .95 * env) * mix(1.25, .4, along);
L += core * inBar * lit;
// A point of light just past each tip.
float tip = length(vec2(dx, y - hgt - 5.)) ;
L += exp(-tip * tip / 2.5) * .5 * env;
// The glow of the loud ones.
L += exp(-dx / 7.) * inBar * .06 * env;
L += exp(-length(vec2(dx, y)) / (R.y * .03)) * .05 * env;
}
// The axis the sound is mirrored about.
L += (1. - smoothstep(.3, 1.2, abs(f.y - c.y))) * .25 * smoothstep(-R.y * .34, -R.y * .2, x) * (1. - smoothstep(R.y * .2, R.y * .34, x));
// A spray of points over the loudest, rising and fading, mirrored.
vec2 g = vec2(x, y) / (R.y * .012);
vec2 id = floor(g);
for (int j = -1; j <= 1; j++) for (int k = -1; k <= 1; k++) {
vec2 cid = id + vec2(k, j);
vec2 h = h22(cid + 3.);
if (h.x > .3) continue;
float life = fract(P * (1. + floor(h.y * 2.)) + h.y * 5.);
vec2 pt = (cid + h22(cid)) * R.y * .012 + vec2(0., life * R.y * .03);
float u = pt.x / (R.y * .27);
float near = exp(-pow(u / .35, 2.)) * smoothstep(reach * .55, reach * .9, pt.y) * (1. - smoothstep(reach * .95, reach * 1.2, pt.y));
float d = length(vec2(x, y) - pt);
L += near * sin(PI * life) * exp(-d * d / 1.8) * .9;
}
// The left of the frame is the words': keep it dark.
L *= mix(.1, 1., smoothstep(.22, .45, f.x / R.x));
return pow(tone(L * 1.15), .9);
}
</script>
<script src="./gl.js"></script>
</body>
</html>