|
| 1 | +/* ================================================================= |
| 2 | + Hero "digital commons" — a lazy, GPU-driven generative WebGL field |
| 3 | + that lives BEHIND the < > mark. This is the one deliberate spectacle |
| 4 | + (the owner priced the perf trade). It is an ENHANCEMENT ONLY: |
| 5 | + · no WebGL / reduced-motion / no-JS / automation → never loads, |
| 6 | + and the static < > mark + breathing gold halo remain as the poster. |
| 7 | + Kept tiny (a fullscreen-quad fragment shader, DPR-capped, fps-capped, |
| 8 | + paused off-screen) so the trade is as small as it can be. |
| 9 | + ================================================================= */ |
| 10 | + |
| 11 | +type Controller = { start: () => void; stop: () => void; destroy: () => void }; |
| 12 | + |
| 13 | +const VERT = `attribute vec2 p; void main(){ gl_Position = vec4(p, 0.0, 1.0); }`; |
| 14 | + |
| 15 | +const FRAG = ` |
| 16 | +precision highp float; |
| 17 | +uniform vec2 u_res; |
| 18 | +uniform float u_time; |
| 19 | +uniform vec2 u_mouse; // -1..1 |
| 20 | +uniform float u_dark; // 1 dark, 0 light |
| 21 | +uniform float u_fund; // 0..1 share of the launch goal funded |
| 22 | +
|
| 23 | +float hash21(vec2 p){ p = fract(p*vec2(123.34,345.45)); p += dot(p, p+34.345); return fract(p.x*p.y); } |
| 24 | +vec2 hash22(vec2 p){ float n = sin(dot(p, vec2(41.0, 289.0))); return fract(vec2(262144.0, 32768.0)*n); } |
| 25 | +float noise(vec2 p){ vec2 i=floor(p), f=fract(p); vec2 u=f*f*(3.0-2.0*f); |
| 26 | + float a=hash21(i), b=hash21(i+vec2(1.0,0.0)), c=hash21(i+vec2(0.0,1.0)), d=hash21(i+vec2(1.0,1.0)); |
| 27 | + return mix(mix(a,b,u.x), mix(c,d,u.x), u.y); } |
| 28 | +float fbm(vec2 p){ float s=0.0, a=0.5; for(int i=0;i<5;i++){ s+=a*noise(p); p*=2.02; a*=0.5; } return s; } |
| 29 | +
|
| 30 | +void main(){ |
| 31 | + vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / u_res.y; |
| 32 | + uv += u_mouse * 0.05; |
| 33 | + float t = u_time * 0.045; |
| 34 | +
|
| 35 | + // slow domain-warped energy — "the commons is alive" |
| 36 | + vec2 q = uv * 2.1; |
| 37 | + float warp = fbm(q + vec2(t, -t*0.6)); |
| 38 | + float flow = fbm(q + warp*1.4 + vec2(-t*0.5, t*0.35)); |
| 39 | +
|
| 40 | + // layered constellation of drifting nodes (dependencies / maintainers / stars) |
| 41 | + // + thin links between near nodes in a cell — a living dependency network. |
| 42 | + float pts = 0.0; |
| 43 | + float gold = 0.0; |
| 44 | + float links = 0.0; |
| 45 | + for(int L=0; L<4; L++){ |
| 46 | + float fl = float(L); |
| 47 | + float sc = 5.5 + fl*6.5; |
| 48 | + vec2 gp = uv*sc + vec2(t*(0.32+fl*0.16), t*0.11 + fl*1.7); |
| 49 | + vec2 cell = floor(gp); |
| 50 | + vec2 f = fract(gp) - 0.5; |
| 51 | + // this cell's node + the neighbour node to build a short link |
| 52 | + vec2 rnd = hash22(cell); |
| 53 | + vec2 rnd2 = hash22(cell + vec2(1.0, 0.0)); |
| 54 | + float layerFade = 1.0 / (1.0 + fl*0.5); |
| 55 | + if (rnd.x >= 0.42) { |
| 56 | + vec2 off = (rnd - 0.5) * 0.74; |
| 57 | + float d = length(f - off); |
| 58 | + float tw = 0.5 + 0.5*sin(u_time*(0.7+rnd.x*2.4) + rnd.y*6.2831); |
| 59 | + float pt = smoothstep(0.085, 0.0, d) * tw * layerFade; |
| 60 | + pts += pt; |
| 61 | + if (rnd.y > 0.8) gold += pt; // a few nodes are patron-gold |
| 62 | + // link to the node one cell to the right, if it exists |
| 63 | + if (rnd2.x >= 0.42) { |
| 64 | + vec2 a = off; |
| 65 | + vec2 b = (rnd2 - 0.5)*0.74 + vec2(1.0, 0.0); |
| 66 | + vec2 pa = f - a, ba = b - a; |
| 67 | + float h = clamp(dot(pa, ba)/dot(ba, ba), 0.0, 1.0); |
| 68 | + float dl = length(pa - ba*h); |
| 69 | + links += smoothstep(0.02, 0.0, dl) * 0.5 * layerFade; |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | +
|
| 74 | + float r = length(uv); |
| 75 | + float core = smoothstep(1.2, 0.0, r); // concentrate toward the < > mark |
| 76 | + float vig = smoothstep(1.3, 0.12, r); // fade to transparent at the edges |
| 77 | +
|
| 78 | + // Patron nodes: a few gold nodes orbit and periodically FIRE a bright gold link |
| 79 | + // into the < > core — a legible "funding the commons" gesture, so the hero reads |
| 80 | + // on first glance rather than only on a second look. |
| 81 | + float patron = 0.0; |
| 82 | + float beam = 0.0; |
| 83 | + for(int i=0; i<4; i++){ |
| 84 | + float fi = float(i); |
| 85 | + // more patron nodes light + fire as the launch goal fills (live funding %) |
| 86 | + float active = smoothstep(fi*0.25 - 0.12, fi*0.25 + 0.12, u_fund + 0.18); |
| 87 | + float ang = fi*1.5708 + t*0.22; |
| 88 | + float rad = 0.62 + 0.1*sin(t*0.8 + fi*1.3); |
| 89 | + vec2 pn = vec2(cos(ang), sin(ang)) * rad; |
| 90 | + float pulse = pow(0.5 + 0.5*sin(u_time*0.8 + fi*2.1), 8.0); // sharp + infrequent |
| 91 | + patron += smoothstep(0.06, 0.0, length(uv - pn)) * (0.3 + pulse*1.4) * active; |
| 92 | + vec2 pa = uv - pn, ba = -pn; |
| 93 | + float h = clamp(dot(pa, ba)/dot(ba, ba), 0.0, 1.0); |
| 94 | + beam += smoothstep(0.013, 0.0, length(pa - ba*h)) * pulse * (1.0 - h*0.25) * active; |
| 95 | + } |
| 96 | +
|
| 97 | + vec3 cGreen = vec3(0.24, 0.88, 0.48); |
| 98 | + vec3 cGold = vec3(0.91, 0.72, 0.29); |
| 99 | +
|
| 100 | + vec3 col = cGreen*(pts-gold)*1.9 + cGold*gold*2.1 + cGreen*links*1.1; |
| 101 | + col += cGold*(patron*1.5 + beam*1.4); // patrons + funding beams |
| 102 | + col += cGreen*flow*0.14*core; // living energy |
| 103 | + col += cGold*core*0.07; // soft central gilt |
| 104 | +
|
| 105 | + float alpha = (pts*1.9 + links*0.9 + patron*1.4 + beam*1.15 + flow*0.13*core + core*0.06) * vig; |
| 106 | +
|
| 107 | + // light theme: no glow reads on parchment, so ink the field down + soften |
| 108 | + vec3 cInk = vec3(0.10, 0.10, 0.07); |
| 109 | + col = mix(mix(cInk, cGold*0.55, gold*0.9) * (pts + core*0.25), col, u_dark); |
| 110 | + alpha *= mix(0.42, 1.0, u_dark); |
| 111 | +
|
| 112 | + gl_FragColor = vec4(col, clamp(alpha, 0.0, 1.0)); |
| 113 | +}`; |
| 114 | + |
| 115 | +function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader | null { |
| 116 | + const sh = gl.createShader(type); |
| 117 | + if (!sh) return null; |
| 118 | + gl.shaderSource(sh, src); |
| 119 | + gl.compileShader(sh); |
| 120 | + if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { |
| 121 | + gl.deleteShader(sh); |
| 122 | + return null; |
| 123 | + } |
| 124 | + return sh; |
| 125 | +} |
| 126 | + |
| 127 | +export function initHeroField(canvas: HTMLCanvasElement): Controller | null { |
| 128 | + const gl = (canvas.getContext('webgl', { alpha: true, antialias: true, premultipliedAlpha: false }) || |
| 129 | + canvas.getContext('experimental-webgl', { alpha: true })) as WebGLRenderingContext | null; |
| 130 | + if (!gl) return null; |
| 131 | + |
| 132 | + const vs = compile(gl, gl.VERTEX_SHADER, VERT); |
| 133 | + const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG); |
| 134 | + if (!vs || !fs) return null; |
| 135 | + const prog = gl.createProgram(); |
| 136 | + if (!prog) return null; |
| 137 | + gl.attachShader(prog, vs); |
| 138 | + gl.attachShader(prog, fs); |
| 139 | + gl.linkProgram(prog); |
| 140 | + if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null; |
| 141 | + gl.useProgram(prog); |
| 142 | + |
| 143 | + const buf = gl.createBuffer(); |
| 144 | + gl.bindBuffer(gl.ARRAY_BUFFER, buf); |
| 145 | + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); |
| 146 | + const loc = gl.getAttribLocation(prog, 'p'); |
| 147 | + gl.enableVertexAttribArray(loc); |
| 148 | + gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); |
| 149 | + |
| 150 | + const uRes = gl.getUniformLocation(prog, 'u_res'); |
| 151 | + const uTime = gl.getUniformLocation(prog, 'u_time'); |
| 152 | + const uMouse = gl.getUniformLocation(prog, 'u_mouse'); |
| 153 | + const uDark = gl.getUniformLocation(prog, 'u_dark'); |
| 154 | + |
| 155 | + gl.enable(gl.BLEND); |
| 156 | + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); |
| 157 | + |
| 158 | + const mouse = { x: 0, y: 0 }; |
| 159 | + const isDark = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 1 : 0); |
| 160 | + |
| 161 | + const resize = () => { |
| 162 | + const dpr = Math.min(window.devicePixelRatio || 1, 1.5); |
| 163 | + const w = Math.max(1, Math.round(canvas.clientWidth * dpr)); |
| 164 | + const h = Math.max(1, Math.round(canvas.clientHeight * dpr)); |
| 165 | + if (canvas.width !== w || canvas.height !== h) { |
| 166 | + canvas.width = w; |
| 167 | + canvas.height = h; |
| 168 | + } |
| 169 | + gl.viewport(0, 0, canvas.width, canvas.height); |
| 170 | + }; |
| 171 | + |
| 172 | + const draw = (timeSec: number) => { |
| 173 | + gl.uniform2f(uRes, canvas.width, canvas.height); |
| 174 | + gl.uniform1f(uTime, timeSec); |
| 175 | + gl.uniform2f(uMouse, mouse.x, mouse.y); |
| 176 | + gl.uniform1f(uDark, isDark()); |
| 177 | + gl.drawArrays(gl.TRIANGLES, 0, 3); |
| 178 | + }; |
| 179 | + |
| 180 | + let raf = 0; |
| 181 | + let running = false; |
| 182 | + let startMs = 0; |
| 183 | + let last = 0; |
| 184 | + const frameMs = 1000 / 40; // fps cap — keep the GPU/CPU cost bounded |
| 185 | + |
| 186 | + const onPointer = (e: PointerEvent) => { |
| 187 | + const r = canvas.getBoundingClientRect(); |
| 188 | + mouse.x = ((e.clientX - r.left) / r.width - 0.5) * 2; |
| 189 | + mouse.y = ((e.clientY - r.top) / r.height - 0.5) * -2; |
| 190 | + }; |
| 191 | + |
| 192 | + const loop = (nowMs: number) => { |
| 193 | + if (!running) return; |
| 194 | + raf = requestAnimationFrame(loop); |
| 195 | + if (nowMs - last < frameMs) return; |
| 196 | + last = nowMs; |
| 197 | + if (!startMs) startMs = nowMs; |
| 198 | + resize(); |
| 199 | + draw((nowMs - startMs) / 1000); |
| 200 | + }; |
| 201 | + |
| 202 | + const start = () => { |
| 203 | + if (running) return; |
| 204 | + running = true; |
| 205 | + startMs = 0; |
| 206 | + last = 0; |
| 207 | + window.addEventListener('pointermove', onPointer, { passive: true }); |
| 208 | + raf = requestAnimationFrame(loop); |
| 209 | + }; |
| 210 | + const stop = () => { |
| 211 | + running = false; |
| 212 | + if (raf) cancelAnimationFrame(raf); |
| 213 | + raf = 0; |
| 214 | + window.removeEventListener('pointermove', onPointer); |
| 215 | + }; |
| 216 | + const destroy = () => { |
| 217 | + stop(); |
| 218 | + gl.deleteProgram(prog); |
| 219 | + gl.deleteShader(vs); |
| 220 | + gl.deleteShader(fs); |
| 221 | + gl.deleteBuffer(buf); |
| 222 | + }; |
| 223 | + |
| 224 | + // paint one deterministic frame immediately (also the reduced-motion poster) |
| 225 | + resize(); |
| 226 | + draw(6.0); |
| 227 | + |
| 228 | + return { start, stop, destroy }; |
| 229 | +} |
0 commit comments