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

122 lines
5.3 KiB
JavaScript

// The frame every concept is drawn in: one canvas, one fragment shader over the
// whole of it, and a phase that runs 0 → 1 once per loop.
//
// A concept is `<slug>.html`: a `<script type="x-shader/x-fragment">` holding
// `float scene(vec2 f)`, which answers the grey at pixel `f` (top-left origin,
// in pixels) for the phase `P`. Everything here is the shared part: the
// prelude every scene can call (hashes, noise that repeats, fbm, the tone
// curve), four samples per pixel, and the clock.
//
// The clock is the phase, never the wall. `draw(p)` paints phase `p` and
// returns once the GPU has finished it, so film.mjs can step the loop a frame
// at a time and photograph each one; a scene is a pure function of `P`, and
// every motion in it turns a whole number of times per loop, so `draw(1)` is
// `draw(0)` and the loop has no seam. Opened in a browser without `?render`,
// the file plays itself at its own length (`data-seconds` on <html>).
//
// ?w=1200&h=1500 sets the size; ?ss=1 drops the supersampling for a quick look.
(() => {
const q = new URLSearchParams(location.search)
const W = +(q.get('w') || 1200)
const H = +(q.get('h') || 1500)
const SS = q.get('ss') === '1' ? 1 : 4
const canvas = document.querySelector('canvas')
canvas.width = W
canvas.height = H
canvas.style.width = W + 'px'
canvas.style.height = H + 'px'
const gl = canvas.getContext('webgl2', { antialias: false, preserveDrawingBuffer: true, alpha: false })
if (!gl) throw new Error('webgl2 is not available')
const PRELUDE = `#version 300 es
precision highp float;
uniform vec2 R;
uniform float P;
out vec4 O;
#define TAU 6.283185307179586
#define PI 3.141592653589793
float h11(float p){p=fract(p*.1031);p*=p+33.33;p*=p+p;return fract(p);}
float h21(vec2 p){vec3 p3=fract(vec3(p.xyx)*.1031);p3+=dot(p3,p3.yzx+33.33);return fract((p3.x+p3.y)*p3.z);}
vec2 h22(vec2 p){vec3 p3=fract(vec3(p.xyx)*vec3(.1031,.1030,.0973));p3+=dot(p3,p3.yzx+33.33);return fract((p3.xx+p3.yz)*p3.zy);}
vec2 grad(vec2 i){float a=h21(i)*TAU;return vec2(cos(a),sin(a));}
// Gradient noise whose lattice wraps every \`per\` cells in x, so a field slid
// by \`per\` cells across a loop comes back to itself.
float pn(vec2 p,float per){vec2 i=floor(p),f=fract(p);vec2 u=f*f*f*(f*(f*6.-15.)+10.);
vec2 i0=vec2(mod(i.x,per),i.y),i1=vec2(mod(i.x+1.,per),i.y);
float a=dot(grad(i0),f),b=dot(grad(i1),f-vec2(1,0)),c=dot(grad(i0+vec2(0,1)),f-vec2(0,1)),d=dot(grad(i1+vec2(0,1)),f-vec2(1,1));
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);}
float gn(vec2 p){return pn(p,1e6);}
float fbm(vec2 p){float s=0.,a=.5;for(int k=0;k<6;k++){s+=a*gn(p);p=mat2(1.6,1.2,-1.2,1.6)*p;a*=.5;}return s;}
// fbm that repeats every \`per\` units in x: each octave doubles the frequency and the period with it.
float pfbm(vec2 p,float per,int n){float s=0.,a=.5;for(int k=0;k<8;k++){if(k>=n)break;s+=a*pn(p,per);p=p*2.+vec2(0.,17.3);per*=2.;a*=.5;}return s;}
// A point on a circle through noise space: drifts, and comes back once per loop.
vec2 loop(float r){return r*vec2(cos(TAU*P),sin(TAU*P));}
// Light to grey: a soft shoulder, so a bright core never clips to a flat white disc.
float tone(float L){return 1.-exp(-max(L,0.));}
float line(float d,float w){return 1.-smoothstep(w-.75,w+.75,abs(d));}
`
const src = document.querySelector('script[type="x-shader/x-fragment"]').textContent
const fs = `${PRELUDE}
${src}
void main(){
vec2 f=vec2(gl_FragCoord.x,R.y-gl_FragCoord.y);
float c=0.;
${SS === 4
? `c+=scene(f+vec2(.125,.375));c+=scene(f+vec2(.375,-.125));c+=scene(f+vec2(-.125,-.375));c+=scene(f+vec2(-.375,.125));c*=.25;`
: `c=scene(f);`}
// Half a step of fixed dither, so a slow fall of light into the dark does not band.
c+=(h21(floor(gl_FragCoord.xy))-.5)/255.;
O=vec4(vec3(clamp(c,0.,1.)),1.);
}`
const vs = `#version 300 es
in vec2 a;void main(){gl_Position=vec4(a,0,1);}`
const compile = (type, text) => {
const s = gl.createShader(type)
gl.shaderSource(s, text)
gl.compileShader(s)
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(s)
document.body.dataset.error = log
throw new Error(log)
}
return s
}
const prog = gl.createProgram()
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs))
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs))
gl.linkProgram(prog)
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog))
gl.useProgram(prog)
const buf = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW)
const at = gl.getAttribLocation(prog, 'a')
gl.enableVertexAttribArray(at)
gl.vertexAttribPointer(at, 2, gl.FLOAT, false, 0, 0)
const uR = gl.getUniformLocation(prog, 'R')
const uP = gl.getUniformLocation(prog, 'P')
gl.viewport(0, 0, W, H)
gl.uniform2f(uR, W, H)
/** Paint phase `p` (0 → 1 is one loop) and wait for the GPU to finish it. */
window.draw = (p) => {
gl.uniform1f(uP, p - Math.floor(p))
gl.drawArrays(gl.TRIANGLES, 0, 3)
gl.finish()
return true
}
if (q.has('render')) {
window.draw(0)
document.body.dataset.ready = '1'
return
}
const seconds = +(document.documentElement.dataset.seconds || 6)
const tick = (now) => {
window.draw(now / 1000 / seconds)
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
})()