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

94 lines
3.4 KiB
HTML

<!doctype html>
<html data-seconds="6">
<meta charset="utf-8" />
<title>Koma</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Koma: a row of film frames standing one behind another and running away into
the dark, each holding the same range of mountains a moment further on. The
near frames are lit, the far ones fade. In the loop the row slides forward
by one frame: the nearest leaves past the edge, a new one arrives at the far
end, and the mountains pan as they come.
-->
<script type="x-shader/x-fragment">
const int N = 9;
// A range of peaks along x: one peak to a cell, each its own height and
// width, roughened by a little ridged noise. Endless, so a frame can pan.
float range(float x, float seed){
float top = 0.;
float c = floor(x);
for (int k = -2; k <= 2; k++) {
float id = c + float(k);
vec2 h = h22(vec2(id, seed));
float at = id + h.x;
float hgt = .35 + .65 * h.y;
float w = .55 + .5 * h11(id * 3.1 + seed);
float t = max(1. - abs(x - at) / w, 0.);
top = max(top, hgt * pow(t, 1.5) * (.8 + .2 * t));
}
float n = 0., a = .5, fr = 3.;
for (int k = 0; k < 4; k++) { n += a * (1. - abs(gn(vec2(x * fr, seed)))); fr *= 2.1; a *= .5; }
return top + n * .2 * (.5 + top);
}
// What a frame holds at (u, v), u and v in -1..1, v down; `pan` is how far on it is.
float shot(vec2 uv, float pan){
float x = uv.x * 1.1 + pan;
// A sky lit from behind the ranges, brightest at the horizon.
float L = .04 + .7 * exp(-pow((uv.y - .12) / .42, 2.));
// The far range, pale in the haze.
float far = .2 - .62 * range(x * .9 + 3., 2.);
if (uv.y > far) L = mix(.34, .14, smoothstep(far, far + .4, uv.y)) + exp(-(uv.y - far) * 160.) * .3;
// The near range, dark, its ridge caught by the light behind it.
float near = .42 - .55 * range(x * 1.4 + 7., 9.);
if (uv.y > near) {
float d = uv.y - near;
L = .02 + .035 * (1. - smoothstep(.0, .4, d)) + exp(-d * 130.) * .75;
}
// Mist lying in the valley, thin.
float m = smoothstep(.1, .8, gn(vec2(x * 2., uv.y * 5.)) * .5 + .5);
L += m * exp(-pow((uv.y - .5) / .07, 2.)) * .14;
return L;
}
float scene(vec2 f){
vec2 c = vec2(.57 * R.x, .5 * R.y);
vec2 p = (f - c) / R.y / 1.38;
float px = 1. / R.y / 1.38;
float L = 0.;
// Far to near, each frame over the ones behind it.
for (int i = N; i >= 0; i--) {
float s = float(i) - P;
float z = 1.85 + s * .5;
float x = .37 - s * .078;
vec2 hs = vec2(.18, .3) / z;
vec2 at = vec2(x / z, 0.);
vec2 uv = (p - at) / hs;
// A frame's light: arriving far away, lit near, gone past the edge.
float come = smoothstep(float(N), float(N) - 1.6, s);
float go = smoothstep(-1., -.25, s);
float lit = exp(-s * .17) * come * go;
vec2 e = (abs(p - at) - hs) / px;
float edge = max(e.x, e.y);
if (edge < 1.5) {
float inside = 1. - smoothstep(-.75, .75, edge);
float pic = shot(uv, s * .55) * lit;
// The picture is a little clear, so the frames behind show through it.
L = mix(L, pic, inside * (.72 + .2 * lit));
}
float rim = line(edge, .55 + .5 / z);
L += rim * (.55 + .9 * lit) * lit + exp(-abs(edge) / (5. + 6. / z)) * .1 * lit;
}
// A soft light from the near end, falling off into the depth.
// The left of the frame is the words': keep it dark.
L *= mix(.1, 1., smoothstep(.24, .5, f.x / R.x));
return pow(tone(L * 1.3), .95);
}
</script>
<script src="./gl.js"></script>
</body>
</html>