// Two interactive WebGL shader backgrounds. // Each exposes a React component that mounts a , sets up GL, // and runs an rAF loop. Pointer move + click drive uniforms. const VERT = ` attribute vec2 a_pos; void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); } `; // ---------- SHADER 1: blue sky with white clouds ---------- // Sky gradient base + domain-warped cloud field. Pointer parts/pulls the clouds, // click brightens a sun-like halo near the cursor. const FRAG_LIQUID = ` precision highp float; uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; // 0..1 uniform float u_pulse; // 0..1 decays after click uniform float u_warp; // tweak: how strongly cursor pulls clouds uniform float u_blueMix; // tweak: sky saturation / cloud contrast uniform vec3 u_cream; // ~white cloud color uniform vec3 u_blue; // sky accent uniform vec3 u_deep; // deep underside float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); } float noise(vec2 p){ vec2 i=floor(p), f=fract(p); float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1)); vec2 u=f*f*(3.0-2.0*f); return mix(mix(a,b,u.x), mix(c,d,u.x), u.y); } float fbm(vec2 p){ float v=0.0, a=0.5; for(int i=0;i<6;i++){ v+=a*noise(p); p*=2.02; a*=0.5; } return v; } void main(){ vec2 uv = gl_FragCoord.xy / u_res.xy; float aspect = u_res.x / u_res.y; vec2 p = uv; p.x *= aspect; vec2 m = u_mouse; m.x *= aspect; vec2 d = p - m; float r = length(d); float pull = exp(-r*2.0) * (0.4 + 0.6*u_pulse); // Sky gradient: deeper blue at top, paler near the horizon vec3 skyTop = mix(u_blue, u_deep, 0.20); vec3 skyMid = u_blue; vec3 skyLow = mix(u_blue, u_cream, mix(0.55, 0.25, u_blueMix)); vec3 sky = mix(skyLow, skyMid, smoothstep(0.0, 0.55, uv.y)); sky = mix(sky, skyTop, smoothstep(0.55, 1.0, uv.y)); // Domain-warped cloud field — slow drift, gently bent toward cursor float t = u_time*0.04; vec2 q = vec2( fbm(p*1.1 + vec2(0.0, t)), fbm(p*1.1 + vec2(3.1, t+1.7)) ); vec2 s = vec2( fbm(p*1.5 + 3.0*q + vec2(1.7, 9.2) + t), fbm(p*1.5 + 3.0*q + vec2(8.3, 2.8) - t*1.2) ); // cursor parts the clouds — they bend toward where you point s += normalize(-d + 1e-4) * pull * u_warp * 0.5; float cloud = fbm(p*1.0 + 2.6*s); // soft cloud mask + wispy edges float fluff = smoothstep(0.46, 0.78, cloud); float wisp = smoothstep(0.34, 0.62, cloud) * 0.45; // cloud color: bright top, slight blue underside for depth vec3 cloudHi = u_cream; vec3 cloudLo = mix(u_cream, u_blue, 0.20); vec3 cloudCol = mix(cloudLo, cloudHi, smoothstep(0.5, 0.85, cloud)); vec3 col = mix(sky, cloudCol, fluff); col = mix(col, cloudHi, wisp * 0.25); // warm sun halo near cursor — intensifies with click pulse float halo = exp(-r*2.8) * (0.25 + 0.9*u_pulse); col += vec3(1.0, 0.93, 0.78) * halo * 0.28; col = mix(col, u_cream, halo * 0.18); // subtle vignette vec2 cv = uv - 0.5; col *= 1.0 - dot(cv,cv)*0.22; gl_FragColor = vec4(col, 1.0); } `; // ---------- SHADER 2: dot grid with ripples ---------- const FRAG_GRID = ` precision highp float; uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; uniform vec4 u_ripples[6]; // xy=pos(0..1), z=startTime, w=strength uniform float u_density; // tweak: grid cells per shortest side uniform float u_dotSize; // tweak uniform vec3 u_cream; uniform vec3 u_blue; uniform vec3 u_deep; void main(){ vec2 uv = gl_FragCoord.xy / u_res.xy; float aspect = u_res.x / u_res.y; vec2 p = uv; p.x *= aspect; vec2 m = u_mouse; m.x *= aspect; // grid coords float cells = u_density; vec2 g = p * cells; vec2 gi = floor(g); vec2 gf = fract(g) - 0.5; vec2 cellCenter = (gi + 0.5) / cells; vec2 cellPos = cellCenter; cellPos.x *= 1.0; // already aspect-scaled // base displacement: subtle breathing float breath = sin(u_time*0.6 + (gi.x+gi.y)*0.35)*0.05; // cursor influence: dots near cursor shift slightly toward it vec2 toMouse = m - cellPos; float md = length(toMouse); float follow = exp(-md*3.5) * 0.18; vec2 disp = normalize(toMouse + 1e-4) * follow; // ripples float rippleAmp = 0.0; vec2 rippleDisp = vec2(0.0); for(int i=0;i<6;i++){ vec4 rp = u_ripples[i]; if(rp.w <= 0.0) continue; vec2 rpos = rp.xy; rpos.x *= aspect; float age = u_time - rp.z; if(age < 0.0 || age > 3.5) continue; vec2 toR = cellPos - rpos; float dR = length(toR); float wave = sin(dR*18.0 - age*7.0); float env = exp(-pow(dR - age*0.55, 2.0)*9.0) * exp(-age*0.7) * rp.w; rippleAmp += wave * env; rippleDisp += normalize(toR + 1e-4) * wave * env * 0.05; } // displaced dot center vec2 dotCenter = vec2(0.0) + disp*cells*0.6 + rippleDisp*cells*0.6; float r = length(gf - dotCenter); // dot radius scales with influence float baseR = u_dotSize; float boost = follow*1.5 + abs(rippleAmp)*0.6 + breath; float radius = baseR * (1.0 + boost*1.8); float dot = 1.0 - smoothstep(radius - 0.06, radius + 0.02, r); // background cream with faint horizontal banding vec3 bg = mix(u_cream*0.965, u_cream*1.01, 0.5 + 0.5*sin(uv.y*40.0)*0.05); // dot color: blue near cursor / ripple energy, otherwise deep ink float energy = clamp(follow*4.0 + abs(rippleAmp)*2.0, 0.0, 1.0); vec3 dotCol = mix(u_deep, u_blue, energy); vec3 col = mix(bg, dotCol, dot); // subtle ring from active ripples (additive) col += u_blue * abs(rippleAmp) * 0.08; // vignette vec2 cv = uv - 0.5; col *= 1.0 - dot*0.0 - dot*0.0 + 0.0; // no-op kept for clarity col *= 1.0 - dot2(cv); gl_FragColor = vec4(col, 1.0); } float dot2(vec2 v){ return dot(v,v)*0.35; } `; // Note: GLSL doesn't hoist; move dot2 above main. We'll inject a fixed version below. const FRAG_GRID_FIXED = ` precision highp float; uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse; uniform vec4 u_ripples[6]; uniform float u_density; uniform float u_dotSize; uniform vec3 u_cream; uniform vec3 u_blue; uniform vec3 u_deep; void main(){ vec2 uv = gl_FragCoord.xy / u_res.xy; float aspect = u_res.x / u_res.y; vec2 p = uv; p.x *= aspect; vec2 m = u_mouse; m.x *= aspect; float cells = u_density; vec2 g = p * cells; vec2 gi = floor(g); vec2 gf = fract(g) - 0.5; vec2 cellCenter = (gi + 0.5) / cells; float breath = sin(u_time*0.6 + (gi.x+gi.y)*0.35)*0.05; vec2 toMouse = m - cellCenter; float md = length(toMouse); float follow = exp(-md*3.5) * 0.18; vec2 disp = normalize(toMouse + 1e-4) * follow; float rippleAmp = 0.0; vec2 rippleDisp = vec2(0.0); for(int i=0;i<6;i++){ vec4 rp = u_ripples[i]; if(rp.w <= 0.0) continue; vec2 rpos = rp.xy; rpos.x *= aspect; float age = u_time - rp.z; if(age < 0.0 || age > 3.5) continue; vec2 toR = cellCenter - rpos; float dR = length(toR); float wave = sin(dR*18.0 - age*7.0); float env = exp(-pow(dR - age*0.55, 2.0)*9.0) * exp(-age*0.7) * rp.w; rippleAmp += wave * env; rippleDisp += normalize(toR + 1e-4) * wave * env * 0.05; } vec2 dotCenter = disp*cells*0.6 + rippleDisp*cells*0.6; float r = length(gf - dotCenter); float baseR = u_dotSize; float boost = follow*1.5 + abs(rippleAmp)*0.6 + breath; float radius = baseR * (1.0 + boost*1.8); float dotMask = 1.0 - smoothstep(radius - 0.06, radius + 0.02, r); vec3 bg = mix(u_cream*0.965, u_cream*1.01, 0.5 + 0.5*sin(uv.y*40.0)*0.05); float energy = clamp(follow*4.0 + abs(rippleAmp)*2.0, 0.0, 1.0); vec3 dotCol = mix(u_deep, u_blue, energy); vec3 col = mix(bg, dotCol, dotMask); col += u_blue * abs(rippleAmp) * 0.08; vec2 cv = uv - 0.5; col *= 1.0 - dot(cv,cv)*0.35; gl_FragColor = vec4(col, 1.0); } `; // ---------- shared GL boilerplate ---------- function createGL(canvas, fragSrc){ const gl = canvas.getContext('webgl', { antialias: true, premultipliedAlpha: false }); if(!gl) return null; const compile = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)){ console.error(gl.getShaderInfoLog(s), src); return null; } return s; }; const vs = compile(gl.VERTEX_SHADER, VERT); const fs = compile(gl.FRAGMENT_SHADER, fragSrc); if(!vs || !fs) return null; const prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if(!gl.getProgramParameter(prog, gl.LINK_STATUS)){ console.error(gl.getProgramInfoLog(prog)); return null; } gl.useProgram(prog); const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW); const loc = gl.getAttribLocation(prog, 'a_pos'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); return { gl, prog }; } // Parse "#rrggbb" → [r,g,b] floats 0..1 function hexToRGB(h){ const v = h.replace('#',''); return [ parseInt(v.slice(0,2),16)/255, parseInt(v.slice(2,4),16)/255, parseInt(v.slice(4,6),16)/255, ]; } // ---------- Component: LiquidShader ---------- function LiquidShader({ cream='#EFE5CC', blue='#2944BA', deep='#0F1538', warp=1.0, blueMix=0.55, paused=false, className, style }){ const canvasRef = React.useRef(null); const stateRef = React.useRef({ mouse:[0.5,0.5], target:[0.5,0.5], pulse:0, lastT:0 }); React.useEffect(() => { const canvas = canvasRef.current; const setup = createGL(canvas, FRAG_LIQUID); if(!setup) return; const { gl, prog } = setup; const u = { res: gl.getUniformLocation(prog, 'u_res'), time: gl.getUniformLocation(prog, 'u_time'), mouse: gl.getUniformLocation(prog, 'u_mouse'), pulse: gl.getUniformLocation(prog, 'u_pulse'), warp: gl.getUniformLocation(prog, 'u_warp'), blueMix: gl.getUniformLocation(prog, 'u_blueMix'), cream: gl.getUniformLocation(prog, 'u_cream'), blue: gl.getUniformLocation(prog, 'u_blue'), deep: gl.getUniformLocation(prog, 'u_deep'), }; const resize = () => { const dpr = Math.min(window.devicePixelRatio||1, 2); const r = canvas.getBoundingClientRect(); canvas.width = Math.max(1, Math.round(r.width*dpr)); canvas.height = Math.max(1, Math.round(r.height*dpr)); gl.viewport(0,0,canvas.width, canvas.height); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); const onMove = (e) => { const r = canvas.getBoundingClientRect(); stateRef.current.target = [ (e.clientX - r.left) / r.width, 1.0 - (e.clientY - r.top) / r.height, ]; }; const onLeave = () => { stateRef.current.target = [0.5, 0.5]; }; const onDown = () => { stateRef.current.pulse = 1.0; }; window.addEventListener('pointermove', onMove); canvas.addEventListener('pointerleave', onLeave); window.addEventListener('pointerdown', onDown); let raf, t0 = performance.now(); const tick = () => { const t = (performance.now() - t0) / 1000; const dt = Math.min(0.05, t - stateRef.current.lastT); stateRef.current.lastT = t; // ease mouse const s = stateRef.current; s.mouse[0] += (s.target[0] - s.mouse[0]) * 0.08; s.mouse[1] += (s.target[1] - s.mouse[1]) * 0.08; s.pulse *= Math.exp(-dt*2.2); gl.uniform2f(u.res, canvas.width, canvas.height); gl.uniform1f(u.time, t); gl.uniform2f(u.mouse, s.mouse[0], s.mouse[1]); gl.uniform1f(u.pulse, s.pulse); gl.uniform1f(u.warp, warp); gl.uniform1f(u.blueMix, blueMix); gl.uniform3fv(u.cream, hexToRGB(cream)); gl.uniform3fv(u.blue, hexToRGB(blue)); gl.uniform3fv(u.deep, hexToRGB(deep)); gl.drawArrays(gl.TRIANGLES, 0, 6); if(!paused) raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); ro.disconnect(); window.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerleave', onLeave); window.removeEventListener('pointerdown', onDown); }; }, [cream, blue, deep, warp, blueMix, paused]); return ; } // ---------- Component: GridShader ---------- function GridShader({ cream='#EFE5CC', blue='#2944BA', deep='#0F1538', density=42, dotSize=0.14, className, style }){ const canvasRef = React.useRef(null); const stateRef = React.useRef({ mouse:[0.5,0.5], target:[0.5,0.5], ripples: Array.from({length:6}, () => [0,0,-999,0]), // x,y,startTime,strength nextRipple: 0, }); React.useEffect(() => { const canvas = canvasRef.current; const setup = createGL(canvas, FRAG_GRID_FIXED); if(!setup) return; const { gl, prog } = setup; const u = { res: gl.getUniformLocation(prog,'u_res'), time: gl.getUniformLocation(prog,'u_time'), mouse: gl.getUniformLocation(prog,'u_mouse'), ripples: gl.getUniformLocation(prog,'u_ripples'), density: gl.getUniformLocation(prog,'u_density'), dotSize: gl.getUniformLocation(prog,'u_dotSize'), cream: gl.getUniformLocation(prog,'u_cream'), blue: gl.getUniformLocation(prog,'u_blue'), deep: gl.getUniformLocation(prog,'u_deep'), }; const resize = () => { const dpr = Math.min(window.devicePixelRatio||1, 2); const r = canvas.getBoundingClientRect(); canvas.width = Math.max(1, Math.round(r.width*dpr)); canvas.height = Math.max(1, Math.round(r.height*dpr)); gl.viewport(0,0,canvas.width, canvas.height); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); let tNow = 0; const onMove = (e) => { const r = canvas.getBoundingClientRect(); stateRef.current.target = [ (e.clientX - r.left) / r.width, 1.0 - (e.clientY - r.top) / r.height, ]; }; const onDown = (e) => { const r = canvas.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width; const y = 1.0 - (e.clientY - r.top) / r.height; if(x<0||x>1||y<0||y>1) return; const s = stateRef.current; s.ripples[s.nextRipple] = [x, y, tNow, 1.0]; s.nextRipple = (s.nextRipple + 1) % 6; }; window.addEventListener('pointermove', onMove); canvas.addEventListener('pointerdown', onDown); let raf, t0 = performance.now(); const ripBuf = new Float32Array(24); const tick = () => { const t = (performance.now() - t0) / 1000; tNow = t; const s = stateRef.current; s.mouse[0] += (s.target[0] - s.mouse[0]) * 0.1; s.mouse[1] += (s.target[1] - s.mouse[1]) * 0.1; for(let i=0;i<6;i++){ const rp = s.ripples[i]; ripBuf[i*4] = rp[0]; ripBuf[i*4+1] = rp[1]; ripBuf[i*4+2] = rp[2]; ripBuf[i*4+3] = rp[3]; } gl.uniform2f(u.res, canvas.width, canvas.height); gl.uniform1f(u.time, t); gl.uniform2f(u.mouse, s.mouse[0], s.mouse[1]); gl.uniform4fv(u.ripples, ripBuf); gl.uniform1f(u.density, density); gl.uniform1f(u.dotSize, dotSize); gl.uniform3fv(u.cream, hexToRGB(cream)); gl.uniform3fv(u.blue, hexToRGB(blue)); gl.uniform3fv(u.deep, hexToRGB(deep)); gl.drawArrays(gl.TRIANGLES, 0, 6); raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); ro.disconnect(); window.removeEventListener('pointermove', onMove); canvas.removeEventListener('pointerdown', onDown); }; }, [cream, blue, deep, density, dotSize]); return ; } Object.assign(window, { LiquidShader, GridShader });