Files
animated_wallpaper/vendor/clouds2d.js
T
Richard Zink 3812f80f40 Make clouds2d.js theme-agnostic
- Remove all hardcoded fallback hex/rgb values
- Skip draw until __currentPalette is published by parent
- applyWeatherTheme now also mixes cloudShadow (was dropped)
2026-10-07 18:53:15 +02:00

239 lines
8.4 KiB
JavaScript

/* 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);
};
})();