/* Custom 2D WebGL cloud renderer. * Much lighter than Vanta's volumetric raymarching while still * GPU-accelerated. Renders a fullscreen quad and uses a custom * fragment shader to produce animated cloud-like patterns. * * Drop-in replacement for Vanta when CONFIG.renderer === 'clouds2d'. * Reads day/night palette + weather theme from window globals set * by the parent (background.html's animate() loop). */ (function() { function makeShader(gl, type, src) { var s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.error('clouds2d: Shader compile error:', gl.getShaderInfoLog(s)); gl.deleteShader(s); return null; } return s; } function makeProgram(gl, vsSrc, fsSrc) { var vs = makeShader(gl, gl.VERTEX_SHADER, vsSrc); var fs = makeShader(gl, gl.FRAGMENT_SHADER, fsSrc); if (!vs || !fs) return null; var p = gl.createProgram(); gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p); if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { console.error('clouds2d: Program link error:', gl.getProgramInfoLog(p)); gl.deleteProgram(p); return null; } return p; } var VERT_SRC = [ 'attribute vec2 aPos;', 'void main() { gl_Position = vec4(aPos, 0.0, 1.0); }' ].join('\n'); // Fragment shader: animates an fBm cloud field, modulates density // by weather, blends day/night palette colours, and adds a soft // horizon gradient so the sky transitions naturally. var FRAG_SRC = [ 'precision highp float;', 'uniform vec2 uResolution;', 'uniform float uTime;', 'uniform vec3 uSky;', 'uniform vec3 uCloud;', 'uniform vec3 uCloudShadow;', 'uniform vec3 uLight;', 'uniform vec3 uBg;', 'uniform float uSpeed;', 'uniform float uDensity;', // 0..1 how thick the clouds are 'uniform float uDayProgress;', // 0..1 (day vs night) 'float hash(vec2 p) {', 'p = fract(p * vec2(123.34, 456.21));', 'p += dot(p, p + 45.32);', 'return fract(p.x * p.y);', '}', 'float noise(vec2 p) {', 'vec2 i = floor(p);', 'vec2 f = fract(p);', 'float a = hash(i);', 'float b = hash(i + vec2(1.0, 0.0));', 'float c = hash(i + vec2(0.0, 1.0));', 'float d = hash(i + vec2(1.0, 1.0));', 'vec2 u = f * f * (3.0 - 2.0 * f);', 'return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;', '}', 'float fbm(vec2 p) {', 'float v = 0.0;', 'float a = 0.5;', 'for (int i = 0; i < 5; i++) {', 'v += a * noise(p);', 'p *= 2.02;', 'a *= 0.5;', '}', 'return v;', '}', 'void main() {', 'vec2 uv = gl_FragCoord.xy / uResolution.xy;', 'vec2 p1 = uv * vec2(3.0, 1.7) + vec2(uTime * 0.012, uTime * 0.004);', 'vec2 p2 = uv * vec2(5.0, 2.6) + vec2(uTime * 0.020, -uTime * 0.006);', 'float n = fbm(p1 * uSpeed) * 0.6 + fbm(p2 * uSpeed) * 0.4;', // Density threshold widens with uDensity (1 = always cloudy, 0 = clear sky) 'float thresh = mix(0.85, 0.30, uDensity);', 'float cloud = smoothstep(thresh - 0.10, thresh + 0.10, n);', // Soft horizon: top of frame is more sky, bottom more clouds 'float horizon = smoothstep(0.0, 0.5, uv.y);', // Sky gradient (top vs bottom) 'vec3 sky = mix(uBg, uSky, horizon);', // Cloud body (lit top + shadow bottom) 'vec3 cloudCol = mix(uCloudShadow, uCloud, horizon);', // Add light tint to the lit side 'cloudCol = mix(cloudCol, uLight, cloud * horizon * 0.4 * uDayProgress);', // Final composite: sky where there's no cloud, cloud where there is 'vec3 col = mix(sky, cloudCol, cloud);', // At night, blend everything toward bg color (darken) 'col = mix(col, mix(uBg, cloudCol, 0.3), (1.0 - uDayProgress) * 0.5);', 'gl_FragColor = vec4(col, 1.0);', '}' ].join('\n'); function init(canvas) { var gl = canvas.getContext('webgl', { antialias: false, alpha: false }); if (!gl) { console.error('clouds2d: WebGL not available'); return null; } var prog = makeProgram(gl, VERT_SRC, FRAG_SRC); if (!prog) return null; var 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); var aPos = gl.getAttribLocation(prog, 'aPos'); var uRes = gl.getUniformLocation(prog, 'uResolution'); var uTime = gl.getUniformLocation(prog, 'uTime'); var uSky = gl.getUniformLocation(prog, 'uSky'); var uCloud = gl.getUniformLocation(prog, 'uCloud'); var uCloudShadow = gl.getUniformLocation(prog, 'uCloudShadow'); var uLight = gl.getUniformLocation(prog, 'uLight'); var uBg = gl.getUniformLocation(prog, 'uBg'); var uSpeed = gl.getUniformLocation(prog, 'uSpeed'); var uDensity = gl.getUniformLocation(prog, 'uDensity'); var uDayProgress = gl.getUniformLocation(prog, 'uDayProgress'); return { gl: gl, prog: prog, buf: buf, aPos: aPos, uRes: uRes, uTime: uTime, uSky: uSky, uCloud: uCloud, uCloudShadow: uCloudShadow, uLight: uLight, uBg: uBg, uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress }; } // Convert 24-bit int (0xRRGGBB) to [r,g,b] 0..1 floats. // Theme-agnostic: caller decides what colour value to pass in. function hex(rgb) { return [((rgb >> 16) & 255) / 255, ((rgb >> 8) & 255) / 255, (rgb & 255) / 255]; } // Cloud density per weather (0 = clear, 1 = overcast). // Names mirror the weather themes defined in background.html. var WEATHER_DENSITY = { sunny: 0.20, cloudy: 0.70, stormy: 1.00, night: 0.45 }; window.clouds2dRender = function(target) { if (!target || typeof target !== 'object') { console.error('clouds2d: invalid target'); return; } var canvas = target.__clouds2dCanvas; if (!canvas) { canvas = document.createElement('canvas'); canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.left = '0'; canvas.style.width = '100%'; canvas.style.height = '100%'; while (target.firstChild) target.removeChild(target.firstChild); target.appendChild(canvas); target.style.position = 'relative'; target.__clouds2dCanvas = canvas; } if (!canvas.__state) { canvas.__state = init(canvas); if (!canvas.__state) return; } var s = canvas.__state; var gl = s.gl; canvas.width = target.clientWidth || window.innerWidth; canvas.height = target.clientHeight || window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(s.prog); gl.bindBuffer(gl.ARRAY_BUFFER, s.buf); gl.enableVertexAttribArray(s.aPos); gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0); gl.uniform2f(s.uRes, canvas.width, canvas.height); gl.uniform1f(s.uTime, performance.now() / 1000); // Pull palette + weather from globals (set by animate() in background.html). // No hardcoded fallbacks: until the parent has published a palette we // skip the draw and leave the WebGL context cleared. This keeps the // renderer 100% theme-agnostic — the parent fully owns colour decisions. var pal = window.__currentPalette; if (!pal) { gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); return; } var weather = window.__currentWeatherName; var dayProgress = window.__currentDayProgress; var themeSpeed = window.__currentThemeSpeed; gl.uniform3fv(s.uSky, hex(pal.sky)); gl.uniform3fv(s.uCloud, hex(pal.cloud)); gl.uniform3fv(s.uCloudShadow, hex(pal.cloudShadow !== undefined ? pal.cloudShadow : pal.sky)); gl.uniform3fv(s.uLight, hex(pal.light)); gl.uniform3fv(s.uBg, hex(pal.bg)); var density = (weather && WEATHER_DENSITY[weather] !== undefined) ? WEATHER_DENSITY[weather] : 0.5; gl.uniform1f(s.uDensity, density); var day = (typeof dayProgress === 'number') ? Math.max(0, Math.min(1, dayProgress)) : 0.5; gl.uniform1f(s.uDayProgress, day); var speed = (typeof themeSpeed === 'number') ? Math.max(0.1, Math.min(3, themeSpeed)) : 1.0; gl.uniform1f(s.uSpeed, speed); gl.drawArrays(gl.TRIANGLES, 0, 3); }; })();