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

182 lines
7.9 KiB
HTML

<!doctype html>
<html data-seconds="8">
<meta charset="utf-8" />
<title>Enso</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<body>
<canvas></canvas>
<!--
Enso: a hand-brushed ensō of light. One stroke, heavy where the brush lands
and dry where it lifts, its bristles drawn as fine streaks along the ring; at
the lifting end the stroke breaks into particles that drift up and away. It
hangs over dark water that holds its reflection in broken bands, with mist
lying on the surface. In the loop a light runs round the stroke twice, the
bristles shimmer, the particles shed and fade, the mist drifts by once.
-->
<script type="x-shader/x-fragment">
// The stroke runs from S0 (left) round through the bottom, the right and the
// top, and lifts at s = END, upper left. Angles are screen angles, y down.
const float S0 = 3.30;
const float END = .90;
// Where pixel f falls: q in heights from the ring's centre, and the ring's radius.
vec2 centre(){ return vec2(mix(.645, .61, step(.9, R.x / R.y)) * R.x, .40 * R.y); }
// How much of the stroke is laid down at s, its ends softened by \`blur\`; the
// gap between the lift and the landing is short, so both ends reach across it.
float laid(float s, float blur){
float a = smoothstep(-blur, .045 + blur, s) * (1. - smoothstep(END - .03 - blur, END + .03 + blur, s));
float b = smoothstep(-blur, .045 + blur, s - 1.) * (1. - smoothstep(END - .03 - blur, END + .03 + blur, s - 1.));
float c = smoothstep(-blur, .045 + blur, s + 1.) * (1. - smoothstep(END - .03 - blur, END + .03 + blur, s + 1.));
return max(a, max(b, c));
}
// The stroke at q: its light, the brush's own grain included.
float stroke(vec2 q, float r0, float soft){
float r = length(q);
float th = atan(q.y, q.x);
float s = fract((S0 - th) / TAU);
// A hand's radius: never quite a circle.
float rs = r0 * (1. + .012 * sin(s * TAU * 2. + 1.3) + .010 * gn(vec2(s * 5., 3.1)));
// The brush lands round, runs heavy, and thins as it lifts.
float landing = smoothstep(0., .045, s);
float w = .032 * (.55 + .45 * landing) * (1. + .3 * exp(-s * 8.)) * (1. - .45 * smoothstep(.5, END + .04, s)) + soft;
float u = (r - rs) / w;
float edge = 1. - smoothstep(.78, 1.02, abs(u) + .12 * gn(vec2(s * 60., u * 3.)));
// Bristles: streaks across the band, long along the stroke, drifting round a small circle per loop.
vec2 d = loop(.05);
float b1 = gn(vec2(u * 7., s * 1.6) + d);
float b2 = gn(vec2(u * 26., s * 4.) - d * 1.5);
float b3 = gn(vec2(u * 80., s * 10.) + d * 2.);
float grain = .45 * b1 + .35 * b2 + .32 * b3;
// Dry brush: bare streaks even where it is heavy, more as it runs out.
float dry = mix(-.12, .2, smoothstep(.45, END, s));
float hair = smoothstep(dry, dry + .12, grain);
float tone = .55 + .45 * smoothstep(-.4, .5, b2 + .5 * b3);
// A light running round the stroke, twice a loop.
float run = .6 + .4 * pow(.5 + .5 * sin(TAU * (s * 2. - P * 2.)), 4.);
float lift = 1. - smoothstep(END - .03, END + .03, s);
float on = landing * lift;
float body = edge * hair * tone * run * on * (1. + .25 * exp(-s * 6.));
// Stray hairs: fine arcs just outside the band, where single bristles splay.
float stray = 0.;
for (int k = 0; k < 4; k++) {
float fk = float(k);
float off = (1.12 + .3 * fk) * (mod(fk, 2.) < 1. ? 1. : -1.);
float du = abs(u - off) * w * R.y;
float seen = smoothstep(.1, .5, gn(vec2(s * 4. + fk * 7.1, fk)) + .15) * on;
stray += seen * (1. - smoothstep(.2, 1.1, du)) * .3;
}
// The halo the stroke throws on the dark around it.
// Its edges in angle soften with distance, so the glow has no hard end where the stroke has one.
float dr = abs(r - rs);
float far = laid(s, dr * .9);
float halo = (exp(-dr / .04) * .09 + exp(-dr / .15) * .03) * far;
return body * 1.5 + stray + halo * 1.4;
}
// The stroke as the water sees it: a soft band of its light, no grain.
float glow(vec2 q, float r0, float w){
float r = length(q);
float s = fract((S0 - atan(q.y, q.x)) / TAU);
float rs = r0 * (1. + .012 * sin(s * TAU * 2. + 1.3) + .010 * gn(vec2(s * 5., 3.1)));
float run = .6 + .4 * pow(.5 + .5 * sin(TAU * (s * 2. - P * 2.)), 4.);
float heavy = (1. + .3 * exp(-s * 8.)) * (1. - .5 * smoothstep(.45, END, s));
return exp(-pow((r - rs) / w, 2.)) * laid(s, .02) * run * heavy;
}
// Particles shed where the brush lifts, drifting up and out, each born and gone within the loop.
float shed(vec2 q, float r0){
float L = 0.;
float px = 1. / R.y;
for (int i = 0; i < 420; i++) {
float fi = float(i);
vec2 h = h22(vec2(fi, 7.7));
vec2 g = h22(vec2(fi, 3.1));
float born = h.x;
float life = .3 + .55 * h.y;
float a = fract(P - born) / life;
if (a >= 1.) continue;
// Most leave where the brush lifts; a few from the dry run before it.
float s = END - .03 + .14 * (g.x - .35) - .25 * pow(h11(fi * 2.9), 3.);
float th = S0 - s * TAU;
vec2 dir = vec2(cos(th), sin(th));
vec2 tang = vec2(sin(th), -cos(th));
vec2 p0 = dir * (r0 + (g.y - .5) * .035);
float speed = .03 + .16 * pow(h11(fi * 1.7), 1.5);
vec2 v = dir * speed * (.5 + g.y) + tang * .07 * h11(fi * 3.3) + vec2(-.04, -.06) * (.5 + h11(fi * 6.1));
vec2 p = p0 + v * a + .01 * vec2(sin(a * 5. + fi), cos(a * 4. + fi * 1.3)) * a;
float size = (.55 + 1.5 * pow(h11(fi * 9.1), 3.)) * px;
float dd = length(q - p);
float fade = pow(sin(PI * a), 1.2) * (1. - .55 * a);
float bright = .5 + 1.1 * h11(fi * 5.3);
L += fade * bright * (exp(-dd * dd / (size * size)) + .15 * exp(-dd / (size * 3.5)));
}
return L;
}
// Fine dust that hangs about the stroke and twinkles.
float dust(vec2 q, float r0){
float L = 0.;
vec2 g = q * 90.;
vec2 id = floor(g);
for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) {
vec2 c = id + vec2(x, y);
vec2 h = h22(c);
if (h.x > .32) continue;
vec2 p = (c + h22(c + 9.)) / 90.;
float near = exp(-abs(length(p) - r0 * 1.03) / .07);
float tw = .5 + .5 * sin(TAU * (P * floor(1. + 3. * h.y) + h.y * 7.));
float dd = length(q - p) * R.y;
L += near * tw * (1.3 * exp(-dd * dd * .9)) * (.4 + h.y);
}
return L;
}
float scene(vec2 f){
vec2 C = centre();
float r0 = .185;
vec2 q = (f - C) / R.y;
vec2 uv = f / R;
float horizon = .655;
float y = f.y / R.y;
float L = 0.;
if (y < horizon) {
L += stroke(q, r0, 0.);
if (q.x < .12 && q.y < .08) L += shed(q, r0);
L += dust(q, r0) * .5;
} else {
// The water: the stroke's reflection about the surface, stretched and broken into bands by the swell.
float d = y - horizon;
float jit = (.001 + .012 * d) * gn(vec2(d * 420., 3.) + loop(.5));
vec2 fr = vec2(f.x + jit * R.y, 2. * horizon * R.y - f.y + d * .7 * R.y);
vec2 qr = (fr - C) / R.y;
float refl = glow(qr, r0, .02 + d * .12);
float bands = smoothstep(-.1, .4, gn(vec2(f.x / R.y * 2.5, d * 520. / (1. + d * 6.)) + loop(.7)));
L += refl * .55 * exp(-d * 3.) * (.15 + .85 * bands);
// Glints on the swell, thickest on the path under the ring.
vec2 gq = vec2(f.x / R.y * 60., d * 1100. / (1. + d * 14.));
float glint = smoothstep(.6, .8, gn(gq + loop(1.4)) * .9 + gn(gq * 2.3 - loop(.9)) * .4);
float path = exp(-pow((f.x - C.x) / (R.y * .17), 2.)) * exp(-d * 5.);
L += glint * path * .4;
// A faint swell across the whole surface.
L += .02 * smoothstep(.2, .9, gn(vec2(f.x / R.y * 2., d * 160.) + loop(.6))) * exp(-d * 3.);
}
// Mist lying on the water, drifting once across per loop.
float m = pfbm(vec2(f.x / R.y * 2.2 + P * 2., y * 9.), 2., 5) * .5 + .5;
m = smoothstep(.38, .95, m + .18 * pn(vec2(f.x / R.y * 5. - P * 4., y * 26.), 4.));
float layer = exp(-pow((y - horizon + .015) / .05, 2.)) + .45 * exp(-pow((y - horizon - .07) / .09, 2.));
L += m * layer * .42;
// The left of the frame is the words': keep it dark.
float clear = smoothstep(.22, .5, uv.x);
L *= mix(.18, 1., clear);
return pow(tone(L * 1.15), .92);
}
</script>
<script src="./gl.js"></script>
</body>
</html>