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

90 lines
3.3 KiB
HTML

<!doctype html>
<html data-seconds="8">
<meta charset="utf-8" />
<title>Zen</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Zen: a matte stone sphere resting in raked sand, the grooves drawn round it
in rings. One low light from behind and to the left catches the crest of
every groove and the stone's shoulder, and falls off into the dark. In the
loop the rings breathe: they swell and settle, and move out by one groove.
-->
<script type="x-shader/x-fragment">
const float RHO = 1.;
// The sand's height and slope at distance r from where the stone rests.
float rake(float r){ return smoothstep(1.32, 1.75, r) * exp(-r * .05); }
float k(){ return TAU / .34; }
float groove(float r){
float breathe = .85 + .15 * sin(TAU * P);
return rake(r) * .045 * breathe * sin(k() * r - TAU * P);
}
float slope(float r){
float breathe = .85 + .15 * sin(TAU * P);
float e = .002;
return (groove(r + e) - groove(r - e)) / (2. * e);
}
float scene(vec2 f){
vec2 uv = (f - vec2(.67 * R.x, .5 * R.y)) / R.y;
uv.y = -uv.y;
vec3 target = vec3(0., RHO * .9, 0.);
float el = radians(24.);
vec3 eye = target + 15. * vec3(-.18, sin(el), -cos(el));
vec3 fw = normalize(target - eye);
vec3 rt = normalize(cross(vec3(0, 1, 0), fw));
vec3 up = cross(fw, rt);
vec3 rd = normalize(fw * 1.45 + rt * uv.x + up * uv.y);
vec3 light = normalize(vec3(-.75, .42, .55));
float L = 0.;
// The stone.
vec3 oc = eye - vec3(0., RHO, 0.);
float b = dot(oc, rd);
float c = dot(oc, oc) - RHO * RHO;
float disc = b * b - c;
float tS = disc > 0. ? -b - sqrt(disc) : 1e9;
// The sand.
float tG = rd.y < 0. ? -eye.y / rd.y : 1e9;
if (tS < tG) {
vec3 p = eye + rd * tS;
vec3 n = normalize(p - vec3(0., RHO, 0.));
float dif = max(dot(n, light), 0.);
// Matte: a broad soft fall from the lit shoulder, a whisper of rim from behind, the sand's bounce below.
float wrap = max((dot(n, light) + .25) / 1.25, 0.);
float rim = pow(1. - max(dot(n, -rd), 0.), 3.) * .25 * max(dot(n, light) + .4, 0.);
float bounce = max(-n.y, 0.) * .05;
float grain = .92 + .08 * gn(n.xz * 40. + n.y * 13.);
L = (pow(wrap, 1.6) * .95 + rim + bounce) * grain;
} else if (tG < 1e8) {
vec3 p = eye + rd * tG;
float r = length(p.xz);
vec2 dir = p.xz / max(r, 1e-4);
float s = slope(r);
vec3 n = normalize(vec3(-s * dir.x, 1., -s * dir.y));
// The grooves catch the low light on their crests.
float dif = max(dot(n, light), 0.);
float crest = pow(dif, 5.) * 2.2 + dif * .35;
// Sand grain, fixed in the sand.
float grain = .8 + .4 * h21(floor(p.xz * 420.)) * smoothstep(9., 2., tG - 13.);
// The pool of light the stone stands in, and the stone's shadow across it.
float pool = exp(-pow(length((p.xz - vec2(1.4, 1.2)) / vec2(6.2, 6.)), 2.) * 1.6);
vec3 toC = vec3(0., RHO, 0.) - p;
float along = dot(toC, light);
float off = length(toC - light * along);
float shade = along > 0. ? smoothstep(RHO * .75, RHO * 1.35, off) : 1.;
float ao = smoothstep(.9, 1.9, r) * smoothstep(.0, .5, r - .2);
L = crest * grain * pool * mix(.18, 1., shade) * mix(.25, 1., ao) * .55;
}
// The left of the frame is the words': keep it dark.
L *= mix(.08, 1., smoothstep(.2, .5, f.x / R.x));
return pow(tone(L * 1.2), .95);
}
</script>
<script src="./gl.js"></script>
</body>
</html>