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

111 lines
4.4 KiB
HTML

<!doctype html>
<html data-seconds="8">
<meta charset="utf-8" />
<title>Iro</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Iro: a thin frame, and the picture in it will not stay inside. Ink-wash
mountains rise through its foot and run on past both edges, cloud billows
out of it to the right, and mist lies in the valleys between the ranges. In
the loop the clouds turn over and the mist flows through once.
-->
<script type="x-shader/x-fragment">
// The frame, in heights from the top-left: x0, y0, x1, y1.
vec4 frame(){
float w = R.x / R.y;
return vec4(w * .575, .2, w * .83, .61);
}
// A ridge line: sharp peaks over a softer fall.
float ridge(float x, float seed){
float r = 0., a = .5, fr = 1.;
for (int k = 0; k < 6; k++) {
float n = 1. - abs(gn(vec2(x * fr + seed, seed * 1.7)));
r += a * n * n;
fr *= 2.03; a *= .48;
}
return r;
}
// A mountain's face at p under the ridge: rock and snow, lit from the upper right.
float rock(vec2 p, float depth, float seed){
vec2 q = vec2(p.x * 11., depth * 11.) + seed;
float e = .02;
float n = fbm(q);
float dx = (fbm(q + vec2(e, 0.)) - n) / e;
float gully = .5 + .5 * clamp(dx * .35, -1., 1.);
float snow = smoothstep(.0, .5, fbm(vec2(p.x * 9., depth * 4.) + seed * 2.) + .2 - depth * 3.);
return mix(.05, .42, gully * gully) * (.4 + .6 * snow);
}
// Cloud: billows turning over in place, with a bright crown.
float cloud(vec2 p, vec2 at, vec2 size){
vec2 q = (p - at) / size;
float r = length(q * vec2(1., 1.25));
vec2 w = vec2(pfbm(q * 1.6 + vec2(P * 2., 0.), 2., 4), pfbm(q * 1.6 + vec2(P * 2., 7.), 2., 4));
float n = pfbm(q * 2.2 + w * .9 + vec2(P * 4., 3.), 4., 6);
float body = smoothstep(.15, -.35, r - 1. + n * .9);
float crown = smoothstep(.3, .9, n + .4 - q.y * .3);
return body * (.35 + .65 * crown);
}
float scene(vec2 f){
vec2 p = f / R.y;
vec4 fr = frame();
float cx = (fr.x + fr.z) * .5;
float hw = (fr.z - fr.x) * .5;
float px = 1. / R.y;
vec2 dIn = vec2(max(fr.x - p.x, p.x - fr.z), max(fr.y - p.y, p.y - fr.w));
float outside = max(dIn.x, dIn.y);
float inside = 1. - smoothstep(-px, px, outside);
// Inside the frame, a pale sky of mist over the ranges; outside, the dark.
float L = inside * (.05 + .14 * smoothstep(fr.y, fr.w, p.y));
// Three ranges, far to near. Each one fades out the farther it runs from the frame.
for (int i = 0; i < 3; i++) {
float fi = float(i);
float base = mix(.5, .66, fi / 2.);
float span = mix(.9, 2.2, fi / 2.);
float top = base - .17 * ridge((p.x - cx) * (2.2 + fi * .6) + 1.3 + fi * 2.7, 4. + fi * 3.)
- (.13 - .03 * fi) * exp(-pow((p.x - cx - .04 + .05 * fi) / (.07 + .04 * fi), 2.));
if (p.y > top) {
float depth = p.y - top;
float face = rock(p, depth, fi * 5.);
// Far ranges are paler in the mist; every range washes out into mist at its foot.
float wash = smoothstep(.05 - .02 * fi, .26 - .06 * fi, depth);
float mist = mix(.3, .14, fi / 2.);
float ink = mix(face * (1.1 - .25 * fi), mist, wash * .85);
float rim = exp(-depth * R.y / 2.) * (.45 - .1 * fi);
float reach = inside + (1. - inside) * exp(-pow(max(abs(p.x - cx) - hw, 0.) / (.1 * span), 2.)) * smoothstep(fr.w + .12, top - .02, p.y - .1 * fi);
reach *= 1. - smoothstep(.64, .8, p.y);
L = mix(L, ink + rim, reach);
}
}
// Mist lying in the valleys, flowing through once per loop.
float m = pfbm(vec2(p.x * 2.5 + P * 2., p.y * 10.), 2., 5) * .5 + .5;
float vale = exp(-pow((p.y - .585) / .03, 2.)) + .7 * exp(-pow((p.y - .675) / .035, 2.));
float spread = exp(-pow(max(abs(p.x - cx) - hw, 0.) / .2, 2.));
L += smoothstep(.45, .9, m) * vale * spread * .5 * (1. - smoothstep(.66, .78, p.y));
// Cloud billowing out of the frame to the right, and a smaller one over its foot to the left.
float cl = cloud(p, vec2(fr.z - .01, .5), vec2(.13, .08)) + .7 * cloud(p, vec2(fr.x + .04, .585), vec2(.08, .04));
L = mix(L, 1.25, clamp(cl, 0., 1.) * .88);
// The frame line, fine and bright, lost where the cloud passes in front of it.
float dEdge = abs(outside);
float rim = line(dEdge / px, .7) * (1. - clamp(cl, 0., 1.) * .9);
L += rim * 1.3 + exp(-dEdge / (px * 8.)) * .12 * (1. - clamp(cl, 0., 1.));
// The left of the frame is the words': keep it dark.
L *= mix(.1, 1., smoothstep(.24, .5, f.x / R.x));
return tone(L * 1.6);
}
</script>
<script src="./gl.js"></script>
</body>
</html>