/* Texture-based 2D cloud renderer. * * Inspired by fizzer's tiny ShaderToy cloud shader: instead of * computing fBm in-shader with hash functions, sample a noise * texture (4 lookups = 4 octaves) and weight the results. This * trades a tiny amount of texture memory for a large drop in ALU * per pixel — significant on low-end integrated GPUs. * * Texture is loaded from window.__noiseDataUri (a data URI baked * from noise.png by the build). Falls back to /noise.png if the * URI is unavailable. Theme-agnostic: parent owns colour via * __currentPalette. */ (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'); // 4-octave fBm by sampling a noise texture. The texture is REPEAT- // wrapped so we can scroll through it endlessly. Scrolling uses a // hash-based per-pixel jitter so the regular tiling pattern doesn't // cause horizon flicker (same fix as clouds3d). var FRAG_SRC = [ 'precision highp float;', 'uniform vec2 uResolution;', 'uniform float uTime;', 'uniform sampler2D uNoise;', 'uniform vec3 uSky;', 'uniform vec3 uCloud;', 'uniform vec3 uCloudShadow;', 'uniform vec3 uLight;', 'uniform vec3 uBg;', 'uniform float uSpeed;', 'uniform float uDensity;', 'uniform float uDayProgress;', 'uniform float uSunAlt;', 'float hash21(vec2 p) {', 'p = fract(p * vec2(123.34, 456.21));', 'p += dot(p, p + 45.32);', 'return fract(p.x * p.y);', '}', // 4-octave texture fBm. Each octave doubles frequency and halves // amplitude (1, 0.5, 0.25, 0.125). Total normalised by 1/0.9375. 'float fbm(vec2 p) {', 'float total = 0.0;', 'total += 1.0 * texture2D(uNoise, p * 1.0).r;', 'total += 0.5 * texture2D(uNoise, p * 2.0).r;', 'total += 0.25 * texture2D(uNoise, p * 4.0).r;', 'total += 0.125 * texture2D(uNoise, p * 8.0).r;', 'return total / 1.875;', '}', 'void main() {', 'vec2 uv = gl_FragCoord.xy / uResolution.xy;', // Scroll the noise field horizontally with time, plus a small // vertical drift for slow cloud movement. Multiply by uv's // aspect so clouds don't squash on wide displays. 'float aspect = uResolution.x / uResolution.y;', 'vec2 p = vec2(', '(uv.x - 0.5) * aspect * 3.0 + uTime * 0.04 * uSpeed,', 'uv.y * 2.5 + uTime * 0.012 * uSpeed', ');', // Per-pixel jitter to break the regular tiling pattern that // would otherwise flicker at the cloud horizon. 'p += (hash21(gl_FragCoord.xy + uTime) - 0.5) * 0.015;', 'float n = fbm(p);', // Density threshold widens with weather (sunny = sparse, // stormy = overcast). 'float thresh = mix(0.70, 0.35, uDensity);', 'float cloud = smoothstep(thresh, thresh + 0.18, n);', // Sky gradient: bg below, sky above 'vec3 sky = mix(uBg, uSky, smoothstep(0.0, 0.6, uv.y));', // Cloud body: shadow on the bottom, lit on the top 'vec3 cloudCol = mix(uCloudShadow, uCloud, smoothstep(0.0, 0.7, uv.y));', // Light tint on the lit side (always slightly, so even at // night the cloud edge catches some illumination) 'cloudCol = mix(cloudCol, uLight, cloud * uv.y * 0.45 * max(0.15, uDayProgress));', // Composite 'vec3 col = mix(sky, cloudCol, cloud);', // Night darkening, driven by sun altitude (not uDayProgress // which is a 24h clock progress, not "is it night") 'float nightFactor = smoothstep(0.05, -0.15, uSunAlt);', 'col = mix(col, mix(uBg, col, 0.4), nightFactor * 0.45);', 'gl_FragColor = vec4(col, 1.0);', '}' ].join('\n'); function hex(rgb) { return [((rgb >> 16) & 255) / 255, ((rgb >> 8) & 255) / 255, (rgb & 255) / 255]; } var WEATHER_DENSITY = { sunny: 0.20, cloudy: 0.70, stormy: 1.00, night: 0.45 }; // Load the noise image and upload it as a REPEAT-wrapped texture. // Priority: window.__noiseDataUri (if it has real data, not // PLACEHOLDER), else fetch /noise.png from the server. function loadNoiseTexture(gl) { return new Promise(function(resolve, reject) { var inline = window.__noiseDataUri; var hasInline = inline && inline.indexOf('PLACEHOLDER') < 0; var src = hasInline ? inline : 'noise.png'; var img = new Image(); img.onload = function() { var tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); resolve(tex); }; img.onerror = function() { reject(new Error('clouds2d: failed to load noise image: ' + src)); }; img.src = src; }); } 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; } var gl = canvas.__gl; if (!gl) { gl = canvas.getContext('webgl', { antialias: false, alpha: false }); if (!gl) { console.error('clouds2d: WebGL not available'); return; } canvas.__gl = gl; } if (!canvas.__state) { // Compile shaders synchronously, load texture asynchronously. 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 prog = makeProgram(gl, VERT_SRC, FRAG_SRC); if (!prog) return; canvas.__state = { gl: gl, prog: prog, buf: buf, aPos: gl.getAttribLocation(prog, 'aPos'), uRes: gl.getUniformLocation(prog, 'uResolution'), uTime: gl.getUniformLocation(prog, 'uTime'), uNoise: gl.getUniformLocation(prog, 'uNoise'), uSky: gl.getUniformLocation(prog, 'uSky'), uCloud: gl.getUniformLocation(prog, 'uCloud'), uCloudShadow: gl.getUniformLocation(prog, 'uCloudShadow'), uLight: gl.getUniformLocation(prog, 'uLight'), uBg: gl.getUniformLocation(prog, 'uBg'), uSpeed: gl.getUniformLocation(prog, 'uSpeed'), uDensity: gl.getUniformLocation(prog, 'uDensity'), uDayProgress: gl.getUniformLocation(prog, 'uDayProgress'), uSunAlt: gl.getUniformLocation(prog, 'uSunAlt') }; canvas.__noiseReady = false; loadNoiseTexture(gl).then(function(tex) { canvas.__noise = tex; canvas.__noiseReady = true; }).catch(function(err) { console.error(err); }); } var s = canvas.__state; canvas.width = target.clientWidth || window.innerWidth; canvas.height = target.clientHeight || window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); // Texture not loaded yet — just clear, frame after will draw. if (!canvas.__noiseReady) { gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); return; } 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.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, canvas.__noise); gl.uniform1i(s.uNoise, 0); gl.uniform2f(s.uRes, canvas.width, canvas.height); gl.uniform1f(s.uTime, performance.now() / 1000); 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; var sunAlt = window.__currentSunAlt; 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.uniform1f(s.uSunAlt, (typeof sunAlt === 'number') ? sunAlt : 0); gl.drawArrays(gl.TRIANGLES, 0, 3); }; })();