// The hero background: the five stripes of the genderfluid flag, bent by layered noise. const vertexSource = ` attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; const fragmentSource = ` #ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif uniform vec2 resolution; uniform float time; uniform vec2 pointer; vec3 stripe(float i) { if (i < 0.5) return vec3(1.0, 0.463, 0.643); if (i < 1.5) return vec3(0.88, 0.85, 0.92); if (i < 2.5) return vec3(0.753, 0.067, 0.843); if (i < 3.5) return vec3(0.05, 0.04, 0.08); return vec3(0.184, 0.235, 0.745); } float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); return mix( mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y ); } float fbm(vec2 p) { float value = 0.0; float amp = 0.5; for (int i = 0; i < 4; i++) { value += amp * noise(p); p = p * 2.02 + vec2(1.7, 9.2); amp *= 0.5; } return value; } void main() { vec2 uv = gl_FragCoord.xy / resolution; vec2 p = uv * vec2(resolution.x / resolution.y, 1.0); float t = time * 0.035; vec2 q = vec2(fbm(p * 0.9 + vec2(0.0, t)), fbm(p * 0.9 + vec2(5.2, -t))); vec2 r = vec2( fbm(p * 0.8 + 2.2 * q + vec2(1.7, 9.2) + t * 0.6), fbm(p * 0.8 + 2.2 * q + vec2(8.3, 2.8) - t * 0.4) ); float d = distance(uv, pointer); float bend = 0.1 * exp(-d * d * 14.0); float y = uv.y + (r.y - 0.5) * 0.9 + (q.x - 0.5) * 0.4 + bend; float s = clamp(1.0 - y, 0.0, 0.9999) * 5.0; float i = floor(s); float f = fract(s); vec3 col = mix(stripe(i), stripe(min(i + 1.0, 4.0)), smoothstep(0.3, 1.0, f)); col *= 0.55 + 0.45 * smoothstep(0.15, 0.85, r.x); col = mix(vec3(0.047, 0.039, 0.071), col, 0.8); gl_FragColor = vec4(col, 1.0); } `; function compile(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.warn(gl.getShaderInfoLog(shader)); return null; } return shader; } export function initFluid(canvas) { const gl = canvas.getContext('webgl', { antialias: false, depth: false, powerPreference: 'low-power' }); if (!gl) return; const vertex = compile(gl, gl.VERTEX_SHADER, vertexSource); const fragment = compile(gl, gl.FRAGMENT_SHADER, fragmentSource); if (!vertex || !fragment) return; const program = gl.createProgram(); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return; gl.useProgram(program); // one triangle that covers the whole viewport gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); const position = gl.getAttribLocation(program, 'position'); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); const uniforms = { resolution: gl.getUniformLocation(program, 'resolution'), time: gl.getUniformLocation(program, 'time'), pointer: gl.getUniformLocation(program, 'pointer'), }; const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); const finePointer = matchMedia('(pointer: fine)').matches; // The noise is soft anyway, rendering at a fraction of the size is barely visible and saves a lot const scale = Math.min(devicePixelRatio || 1, 2) * (finePointer ? 0.5 : 0.35); const offset = Math.random() * 200; const pointer = { x: 0.7, y: 0.6, targetX: 0.7, targetY: 0.6 }; let visible = true; let frame = 0; let start = performance.now(); function resize() { const width = Math.max(1, Math.round(canvas.clientWidth * scale)); const height = Math.max(1, Math.round(canvas.clientHeight * scale)); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; gl.viewport(0, 0, width, height); } } function draw(now) { resize(); pointer.x += (pointer.targetX - pointer.x) * 0.04; pointer.y += (pointer.targetY - pointer.y) * 0.04; gl.uniform2f(uniforms.resolution, canvas.width, canvas.height); gl.uniform1f(uniforms.time, offset + (now - start) / 1000); gl.uniform2f(uniforms.pointer, pointer.x, pointer.y); gl.drawArrays(gl.TRIANGLES, 0, 3); } const animated = () => !reducedMotion.matches; function loop(now) { frame = 0; if (!visible || document.hidden || !animated()) return; draw(now); frame = requestAnimationFrame(loop); } function play() { if (!frame && visible && !document.hidden && animated()) frame = requestAnimationFrame(loop); } draw(start); canvas.classList.add('is-ready'); new ResizeObserver(() => { if (!animated()) draw(start); }).observe(canvas); new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; play(); }).observe(canvas); document.addEventListener('visibilitychange', play); reducedMotion.addEventListener('change', () => { start = performance.now(); play(); }); if (finePointer) { addEventListener( 'pointermove', (event) => { const rect = canvas.getBoundingClientRect(); pointer.targetX = (event.clientX - rect.left) / rect.width; pointer.targetY = 1 - (event.clientY - rect.top) / rect.height; }, { passive: true }, ); } play(); }