mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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).
182 lines
7.9 KiB
HTML
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>
|