clouds2d: texture-based 4-octave fBm (fizzer-inspired)
Rewrite clouds2d.js to sample a noise texture instead of computing fBm with hash functions. Each octave is one texture lookup with doubled UV scale and halved amplitude. - Same visual range as before but ~4x cheaper on ALU - REPEAT-wrapped texture tiles seamlessly when scrolled by time - Per-pixel hash21 jitter keeps the regular tiling from flickering - Exposes noise as a uniform, so multiple fragment shaders could share it - Theme-agnostic: pulls colours from __currentPalette Also adds #renderer=clouds2d URL-hash override for quick A/B testing between the volumetric clouds3d and the 2D version.
This commit is contained in:
1 parent
f6f35776bc
commit
bfbb73a2af
3 files changed
+300
-209
No files matched your search
+157
-111
@@ -1089,14 +1089,18 @@ else window.SunCalc = SunCalc;
|
||||
</script>
|
||||
<script src="vendor/clouds3d.js"></script>
|
||||
<script>
|
||||
/* 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.
|
||||
/* Texture-based 2D cloud renderer.
|
||||
*
|
||||
* 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).
|
||||
* 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() {
|
||||
@@ -1134,115 +1138,80 @@ var VERT_SRC = [
|
||||
'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.
|
||||
// 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;', // 0..1 how thick the clouds are
|
||||
'uniform float uDayProgress;', // 0..1 (day vs night)
|
||||
'float hash(vec2 p) {',
|
||||
'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);',
|
||||
'}',
|
||||
'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;',
|
||||
'}',
|
||||
// 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 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;',
|
||||
'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;',
|
||||
'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
|
||||
// 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);',
|
||||
// At night, blend everything toward bg color (darken)
|
||||
'col = mix(col, mix(uBg, cloudCol, 0.3), (1.0 - uDayProgress) * 0.5);',
|
||||
// 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 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,
|
||||
@@ -1250,6 +1219,30 @@ var WEATHER_DENSITY = {
|
||||
night: 0.45
|
||||
};
|
||||
|
||||
// Load the noise image and upload it as a REPEAT-wrapped texture.
|
||||
// Resolves with the gl texture on success.
|
||||
function loadNoiseTexture(gl) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var src = window.__noiseDataUri || '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');
|
||||
@@ -1269,32 +1262,74 @@ window.clouds2dRender = function(target) {
|
||||
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) {
|
||||
canvas.__state = init(canvas);
|
||||
if (!canvas.__state) return;
|
||||
// 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;
|
||||
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);
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
@@ -1305,10 +1340,12 @@ window.clouds2dRender = function(target) {
|
||||
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.uCloudShadow, hex(
|
||||
pal.cloudShadow !== undefined ? pal.cloudShadow : pal.sky));
|
||||
gl.uniform3fv(s.uLight, hex(pal.light));
|
||||
gl.uniform3fv(s.uBg, hex(pal.bg));
|
||||
|
||||
@@ -1324,6 +1361,8 @@ window.clouds2dRender = function(target) {
|
||||
? 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);
|
||||
};
|
||||
|
||||
@@ -1332,6 +1371,7 @@ window.clouds2dRender = function(target) {
|
||||
<script>
|
||||
window.__clouds2dStarted = true;
|
||||
try {
|
||||
window.__noiseDataUri = 'data:image/png;base64,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 truncated
|
||||
var noiseDataUri = 'data:image/png;base64,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 truncated
|
||||
var renderer = (window.CONFIG && window.CONFIG.renderer) || 'vanta';
|
||||
var useVanta = (renderer !== 'clouds2d' && renderer !== 'clouds3d');
|
||||
@@ -1407,7 +1447,8 @@ if (useVanta && typeof VANTA !== 'undefined') {
|
||||
var keys = [
|
||||
'void', 'panel', 'raised', 'border', 'mid', 'ash',
|
||||
'icon', 'stone', 'bone', 'chalk',
|
||||
'ember', 'gold', 'blood', 'rust', 'rot', 'moss', 'steel', 'steelLight', 'violet'
|
||||
'ember', 'gold', 'blood', 'rust', 'rot', 'moss',
|
||||
'steel', 'steelLight', 'blue', 'blueLight', 'violet'
|
||||
];
|
||||
var map = {
|
||||
void: 'black',
|
||||
@@ -1423,7 +1464,9 @@ if (useVanta && typeof VANTA !== 'undefined') {
|
||||
rot: 'pink',
|
||||
moss: 'green',
|
||||
steel: 'blue',
|
||||
steelLight: 'blue-light',
|
||||
steelLight: 'blue',
|
||||
blue: 'blue',
|
||||
blueLight: 'blue-light',
|
||||
violet: 'purple'
|
||||
};
|
||||
var result = {};
|
||||
@@ -1445,7 +1488,8 @@ if (useVanta && typeof VANTA !== 'undefined') {
|
||||
var keys = [
|
||||
'void', 'panel', 'raised', 'border', 'mid', 'ash',
|
||||
'icon', 'stone', 'bone', 'chalk',
|
||||
'ember', 'gold', 'blood', 'rust', 'rot', 'moss', 'steel', 'steelLight', 'violet'
|
||||
'ember', 'gold', 'blood', 'rust', 'rot', 'moss',
|
||||
'steel', 'steelLight', 'blue', 'blueLight', 'violet'
|
||||
];
|
||||
var map = {
|
||||
void: 'black',
|
||||
@@ -1461,7 +1505,9 @@ if (useVanta && typeof VANTA !== 'undefined') {
|
||||
rot: 'pink',
|
||||
moss: 'green',
|
||||
steel: 'blue',
|
||||
steelLight: 'blue-light',
|
||||
steelLight: 'blue',
|
||||
blue: 'blue',
|
||||
blueLight: 'blue-light',
|
||||
violet: 'purple'
|
||||
};
|
||||
var result = {};
|
||||
@@ -1484,21 +1530,21 @@ if (useVanta && typeof VANTA !== 'undefined') {
|
||||
{ alt: -0.087, sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.panel), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
||||
{ alt: 0.0, sky: hexToInt(ASH.rust), cloud: hexToInt(ASH.border), light: hexToInt(ASH.gold), bg: hexToInt(ASH.rust), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.gold), sunLight: hexToInt(ASH.ember), sunVis: 0.8 },
|
||||
{ alt: 0.087, sky: hexToInt(ASH.blood), cloud: hexToInt(ASH.border), light: hexToInt(ASH.ember), bg: hexToInt(ASH.blood), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.ember), sunLight: hexToInt(ASH.ember), sunVis: 0.7 },
|
||||
{ alt: 0.4, sky: hexToInt(ASH.steel), cloud: hexToInt(ASH.border), light: hexToInt(ASH.bone), bg: hexToInt(ASH.steel), cloudShadow: hexToInt(ASH.steel), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 },
|
||||
{ alt: Math.PI / 2, sky: hexToInt(ASH.steelLight), cloud: hexToInt(ASH.border), light: hexToInt(ASH.chalk), bg: hexToInt(ASH.steelLight), cloudShadow: hexToInt(ASH.steelLight), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 }
|
||||
{ alt: 0.4, sky: hexToInt(ASH.blue), cloud: hexToInt(ASH.border), light: hexToInt(ASH.bone), bg: hexToInt(ASH.blue), cloudShadow: hexToInt(ASH.steel), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 },
|
||||
{ alt: Math.PI / 2, sky: hexToInt(ASH.blueLight), cloud: hexToInt(ASH.border), light: hexToInt(ASH.chalk), bg: hexToInt(ASH.blueLight), cloudShadow: hexToInt(ASH.blue), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 }
|
||||
];
|
||||
window.nightPaletteNew = { sky: hexToInt(ASH.void), cloud: hexToInt(ASH.raised), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone) };
|
||||
window.nightPaletteFull = { sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.border), light: hexToInt(ASH.icon), bg: hexToInt(ASH.panel), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.icon), sunGlare: hexToInt(ASH.mid), sunLight: hexToInt(ASH.icon) };
|
||||
window.weatherThemes = {
|
||||
sunny: {
|
||||
skyColor: hexToInt(ASH.steelLight),
|
||||
skyColor: hexToInt(ASH.blueLight),
|
||||
cloudColor: hexToInt(ASH.stone),
|
||||
lightColor: hexToInt(ASH.chalk),
|
||||
backgroundColor: hexToInt(ASH.mid),
|
||||
backgroundColor: hexToInt(ASH.bone),
|
||||
speed: 1.0
|
||||
},
|
||||
cloudy: {
|
||||
skyColor: hexToInt(ASH.steel),
|
||||
skyColor: hexToInt(ASH.blue),
|
||||
cloudColor: hexToInt(ASH.mid),
|
||||
lightColor: hexToInt(ASH.bone),
|
||||
backgroundColor: hexToInt(ASH.border),
|
||||
@@ -1535,8 +1581,8 @@ window.sunPalettes = [
|
||||
{ alt: -0.087, sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.panel), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
||||
{ alt: 0.0, sky: hexToInt(ASH.rust), cloud: hexToInt(ASH.border), light: hexToInt(ASH.gold), bg: hexToInt(ASH.rust), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.gold), sunLight: hexToInt(ASH.ember), sunVis: 0.8 },
|
||||
{ alt: 0.087, sky: hexToInt(ASH.blood), cloud: hexToInt(ASH.border), light: hexToInt(ASH.ember), bg: hexToInt(ASH.blood), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.ember), sunLight: hexToInt(ASH.ember), sunVis: 0.7 },
|
||||
{ alt: 0.4, sky: hexToInt(ASH.steel), cloud: hexToInt(ASH.border), light: hexToInt(ASH.bone), bg: hexToInt(ASH.steel), cloudShadow: hexToInt(ASH.steel), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 },
|
||||
{ alt: Math.PI / 2, sky: hexToInt(ASH.steelLight), cloud: hexToInt(ASH.border), light: hexToInt(ASH.chalk), bg: hexToInt(ASH.steelLight), cloudShadow: hexToInt(ASH.steelLight), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 }
|
||||
{ alt: 0.4, sky: hexToInt(ASH.blue), cloud: hexToInt(ASH.border), light: hexToInt(ASH.bone), bg: hexToInt(ASH.blue), cloudShadow: hexToInt(ASH.steel), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 },
|
||||
{ alt: Math.PI / 2, sky: hexToInt(ASH.blueLight), cloud: hexToInt(ASH.border), light: hexToInt(ASH.chalk), bg: hexToInt(ASH.blueLight), cloudShadow: hexToInt(ASH.blue), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 }
|
||||
];
|
||||
|
||||
// Night palette based on moon phase (0 = new, 0.5 = full, 1 = new) - Ash colors
|
||||
@@ -2190,14 +2236,14 @@ if (moonTimes.set) {
|
||||
|
||||
window.weatherThemes = {
|
||||
sunny: {
|
||||
skyColor: hexToInt(ASH.steelLight),
|
||||
skyColor: hexToInt(ASH.blueLight),
|
||||
cloudColor: hexToInt(ASH.stone),
|
||||
lightColor: hexToInt(ASH.chalk),
|
||||
backgroundColor: hexToInt(ASH.mid),
|
||||
backgroundColor: hexToInt(ASH.bone),
|
||||
speed: 1.0
|
||||
},
|
||||
cloudy: {
|
||||
skyColor: hexToInt(ASH.steel),
|
||||
skyColor: hexToInt(ASH.blue),
|
||||
cloudColor: hexToInt(ASH.mid),
|
||||
lightColor: hexToInt(ASH.bone),
|
||||
backgroundColor: hexToInt(ASH.border),
|
||||
|
||||
+4
-1
@@ -479,8 +479,11 @@
|
||||
<script>
|
||||
window.__clouds2dStarted = true;
|
||||
try {
|
||||
window.__noiseDataUri = 'data:image/png;base64,PLACEHOLDER';
|
||||
var noiseDataUri = 'data:image/png;base64,PLACEHOLDER';
|
||||
var renderer = (window.CONFIG && window.CONFIG.renderer) || 'vanta';
|
||||
// URL override for easy A/B testing: #renderer=clouds2d / #renderer=clouds3d
|
||||
var _urlRenderer = (location.hash.match(/renderer=(\w+)/) || [])[1];
|
||||
var renderer = _urlRenderer || (window.CONFIG && window.CONFIG.renderer) || 'vanta';
|
||||
var useVanta = (renderer !== 'clouds2d' && renderer !== 'clouds3d');
|
||||
var effect;
|
||||
if (useVanta && typeof VANTA !== 'undefined') {
|
||||
|
||||
Vendored
+139
-97
@@ -1,11 +1,15 @@
|
||||
/* 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.
|
||||
/* Texture-based 2D cloud renderer.
|
||||
*
|
||||
* 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).
|
||||
* 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() {
|
||||
@@ -43,115 +47,80 @@ var VERT_SRC = [
|
||||
'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.
|
||||
// 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;', // 0..1 how thick the clouds are
|
||||
'uniform float uDayProgress;', // 0..1 (day vs night)
|
||||
'float hash(vec2 p) {',
|
||||
'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);',
|
||||
'}',
|
||||
'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;',
|
||||
'}',
|
||||
// 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 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;',
|
||||
'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;',
|
||||
'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
|
||||
// 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);',
|
||||
// At night, blend everything toward bg color (darken)
|
||||
'col = mix(col, mix(uBg, cloudCol, 0.3), (1.0 - uDayProgress) * 0.5);',
|
||||
// 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 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,
|
||||
@@ -159,6 +128,33 @@ var WEATHER_DENSITY = {
|
||||
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');
|
||||
@@ -178,32 +174,74 @@ window.clouds2dRender = function(target) {
|
||||
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) {
|
||||
canvas.__state = init(canvas);
|
||||
if (!canvas.__state) return;
|
||||
// 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;
|
||||
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);
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
@@ -214,10 +252,12 @@ window.clouds2dRender = function(target) {
|
||||
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.uCloudShadow, hex(
|
||||
pal.cloudShadow !== undefined ? pal.cloudShadow : pal.sky));
|
||||
gl.uniform3fv(s.uLight, hex(pal.light));
|
||||
gl.uniform3fv(s.uBg, hex(pal.bg));
|
||||
|
||||
@@ -233,6 +273,8 @@ window.clouds2dRender = function(target) {
|
||||
? 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);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user