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

86 lines
3.0 KiB
HTML

<!doctype html>
<html data-seconds="8">
<meta charset="utf-8" />
<title>Mori</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Mori: a mountain as a mesh. A heightfield peak, ridged and broken, drawn as
its wireframe only — dense lines along one axis, sparser across — lit
brightest at the summit and falling away into the dark at its foot. In the
loop the terrain turns slowly one way and back.
-->
<script type="x-shader/x-fragment">
float terrain(vec2 p){
float r = length(p);
float peak = 1.6 * exp(-pow(r / .9, 1.45));
// Ridges running down from the summit, and a broken surface over everything.
float ridged = 0., a = .5, fr = .9;
vec2 q = p;
for (int k = 0; k < 4; k++) {
float n = 1. - abs(gn(q * fr + 3.7));
ridged += a * n * n;
q = mat2(.8, .6, -.6, .8) * q;
fr *= 2.; a *= .5;
}
float shoulder = .35 * exp(-dot(p - vec2(1.1, .5), p - vec2(1.1, .5)) / .5) + .25 * exp(-dot(p + vec2(.9, -.8), p + vec2(.9, -.8)) / .6);
float base = exp(-r * r / 4.5);
return peak * (.72 + .28 * ridged) + shoulder * (.6 + .4 * ridged) + .12 * ridged * base;
}
float scene(vec2 f){
vec2 uv = (f - vec2(.64 * R.x, .56 * R.y)) / R.y;
uv.y = -uv.y;
// The slow turn: one way and back, once a loop.
float yaw = .55 + .2 * sin(TAU * P);
vec3 target = vec3(0., .55, 0.);
float el = radians(25.);
vec3 eye = target + 9.5 * vec3(sin(yaw) * cos(el), sin(el), -cos(yaw) * 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.75 + rt * uv.x + up * uv.y);
float L = 0.;
// March the heightfield.
float t = 2.;
float hit = -1.;
for (int i = 0; i < 220; i++) {
vec3 p = eye + rd * t;
float d = p.y - terrain(p.xz);
if (d < .001 * t) { hit = t; break; }
t += max(d * .45, .004);
if (t > 22.) break;
}
if (hit > 0.) {
// Refine the crossing.
float lo = hit - .02, hi = hit;
for (int i = 0; i < 8; i++) {
float mid = (lo + hi) * .5;
vec3 p = eye + rd * mid;
if (p.y - terrain(p.xz) < 0.) hi = mid; else lo = mid;
}
vec3 p = eye + rd * hi;
float h = terrain(p.xz);
// The mesh: dense lines across, sparser along, as thin as a pixel wherever they fall.
vec2 g = p.xz * vec2(16., 6.);
vec2 w = fwidth(g);
vec2 a = abs(fract(g - .5) - .5) / max(w, 1e-4);
float wire = max(1. - smoothstep(.2, 1.1, a.y), .55 * (1. - smoothstep(.2, 1.1, a.x)));
// Fade the mesh where its lines crowd past seeing, at a grazing angle or far away.
float crowd = 1. - smoothstep(.35, .9, max(w.x, w.y));
float r = length(p.xz);
float lit = (.1 + 1.25 * smoothstep(.05, 1.5, h)) * exp(-r * r / 5.);
L = wire * crowd * lit * 1.6;
// A faint skin, so the near slopes hide the far ones.
L += .025 * lit;
}
// The left of the frame is the words': keep it dark.
L *= mix(.08, 1., smoothstep(.22, .48, f.x / R.x));
return pow(tone(L * 1.1), .9);
}
</script>
<script src="./gl.js"></script>
</body>
</html>