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

114 lines
4.3 KiB
HTML

<!doctype html>
<html data-seconds="6">
<meta charset="utf-8" />
<title>Kai</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Kai: three panes of glass stacked in isometric depth, and a curtain of thin
vertical lines of light threaded through them — a decision passing down
through its layers. In the loop the panes breathe up and down out of step,
pulses of light run down the threads, and each one flares where it meets a
pane.
-->
<script type="x-shader/x-fragment">
const float C30 = .8660254;
const float S30 = .5;
// A pane's top face at height y0, half-width a: its (x, z), and how far f is
// from its edge in pixels (negative inside).
vec2 face(vec2 p, float y0){
float u = p.x / C30;
float v = (p.y + y0) / S30;
return vec2(u + v, v - u) * .5;
}
// The light a pane gives at p: its edges, its two faces, its sheen.
float pane(vec2 p, float y0, float a, float t, float px, inout float cover){
vec2 xz = face(p, y0);
float dTop = (max(abs(xz.x), abs(xz.y)) - a) / 1.1547;
vec2 xzb = face(p, y0 - t);
float dBot = (max(abs(xzb.x), abs(xzb.y)) - a) / 1.1547;
float L = 0.;
// The rims: the top face sharp, the bottom face dimmer through the glass.
L += line(dTop / px, .6) * 1.25 + exp(-abs(dTop) / (px * 6.)) * .22;
L += line(dBot / px, .5) * .45 + exp(-abs(dBot) / (px * 5.)) * .08;
// The edge of the slab between them, where the two faces' outlines part.
float slab = step(dTop, 0.) + step(dBot, 0.);
bool inTop = dTop < 0.;
if (inTop) {
// The face: a cool glass sheen, brighter toward the far edge, with a soft reflection crossing it.
float far = smoothstep(a, -a, xz.x + xz.y);
float sweep = exp(-pow((xz.x - xz.y) / a * .9 + .9 * sin(TAU * P) - .2, 2.) * 2.);
float grain = .5 + .5 * gn(xz / a * 3. + loop(.3));
L += (.05 + .09 * far + .07 * sweep) * (.75 + .25 * grain);
// A faint lattice in the glass.
vec2 g = abs(fract(xz / a * 3. + .5) - .5) * a / 3. / 1.1547;
L += (line(g.x / px, .35) + line(g.y / px, .35)) * .035;
}
if (dBot < 0. && !inTop) L += .035;
L *= cover;
if (inTop) cover *= .72;
return L;
}
float scene(vec2 f){
float px = 1. / R.y;
vec2 C = vec2(.655 * R.x, .5 * R.y);
vec2 p = (f - C) / R.y;
float a = .108;
float t = .012;
float gap = .13;
float L = 0.;
float cover = 1.;
// The threads: vertical lines of light through the stack, densest at its middle.
float threads = 0.;
float flares = 0.;
for (int i = 0; i < 46; i++) {
float fi = float(i);
vec2 h = h22(vec2(fi, 2.3));
// A thread's place in the panes' plan, mostly inside the footprint.
vec2 xz = (h - .5) * 2. * a * .95;
float x = (xz.x - xz.y) * C30;
float dx = abs(p.x - x);
if (dx > .03) continue;
float base = (xz.x + xz.y) * S30;
float top = base - gap * 1.9 - .06 - .12 * h11(fi * 3.7);
float bot = base + gap * 1.6 + .05 + .14 * h11(fi * 5.1);
float along = smoothstep(top, top + .08, p.y) * (1. - smoothstep(bot - .1, bot, p.y));
float weight = .35 + .65 * h11(fi * 8.3);
// Pulses run down each thread, one or two per loop.
float k = 1. + floor(2. * h11(fi * 4.4));
float head = top + (bot - top) * fract(P * k + h11(fi * 6.6));
float pulse = exp(-pow((p.y - head) / .035, 2.)) + .35 * exp(-max(head - p.y, 0.) / .09) * step(p.y, head) * step(head - .25, p.y);
float core = line(dx / px, .45);
threads += along * weight * (core * (.32 + 1.4 * pulse) + exp(-dx / (px * 4.)) * .05 * (1. + 2. * pulse));
// Where a thread meets a pane it flares, and brightest as a pulse passes.
for (int j = 0; j < 3; j++) {
float fj = float(j);
float y0 = (1. - fj) * gap + .007 * sin(TAU * (P + fj / 3.));
float yy = base - y0;
float dd = length(vec2(p.x - x, (p.y - yy) * 1.6));
float near = exp(-pow((head - yy) / .05, 2.));
flares += weight * (exp(-dd * dd / (px * px * 6.)) * (.5 + 1.8 * near) + exp(-dd / (px * 14.)) * .12 * (1. + 2. * near));
}
}
for (int j = 0; j < 3; j++) {
float fj = float(j);
float y0 = (1. - fj) * gap + .007 * sin(TAU * (P + fj / 3.));
L += pane(p, y0, a, t, px, cover);
}
L += threads + flares;
// The left of the frame is the words': keep it dark.
L *= mix(.15, 1., smoothstep(.2, .45, f.x / R.x));
return pow(tone(L * 1.1), .9);
}
</script>
<script src="./gl.js"></script>
</body>
</html>