Add lightweight 2D WebGL cloud renderer as alternative to Vanta
This commit is contained in:
1 parent
a82d6cce3d
commit
d8e619e0c3
5 files changed
+406
-2
No files matched your search
+203
-1
@@ -13,6 +13,10 @@
|
||||
* below. To add a new theme, drop a new *.css file in the same
|
||||
* directory and add an entry to window.THEMES.
|
||||
*
|
||||
* renderer - Background renderer: 'vanta' (default, GPU 3D volumetric
|
||||
* clouds, heavy) or 'clouds2d' (lightweight 2D WebGL shader,
|
||||
* very low GPU usage, similar visual style).
|
||||
*
|
||||
* units - Measurement units for the weather widget.
|
||||
* 'metric' (°C, km/h) or 'imperial' (°F, mph)
|
||||
*
|
||||
@@ -79,6 +83,7 @@ window.CONFIG = {
|
||||
debug: 0,
|
||||
theme: 'ash',
|
||||
units: 'metric',
|
||||
renderer: 'clouds2d', // 'vanta' or 'clouds2d'
|
||||
language: 'de',
|
||||
timeFormat: 'DD. MMMM, YYYY ‒ HH:mm:SS',
|
||||
location: {
|
||||
@@ -1081,10 +1086,193 @@ else window.SunCalc = SunCalc;
|
||||
<script src="vendor/vanta.clouds.min.js"></script>
|
||||
<script>
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports._vantaEffect=t():e._vantaEffect=t()}("undefined"!=typeof self?self:this,(function(){return function(e){var t={};function i(o){if(t[o])return t[o].exports;var s=t[o]={i:o,l:!1,exports:{}};return e[o].call(s.exports,s,s.exports,i),s.l=!0,s.exports}return i.m=e,i.c=t,i.d=function(e,t,o){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)i.d(o,s,function(t){return e[t]}.bind(null,s));return o},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=7)}([function(e,t,i){"use strict";function o(e,t){for(let i in t)t.hasOwnProperty(i)&&(e[i]=t[i]);return e}function s(){return"undefined"!=typeof navigator?/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)||window.innerWidth<600:null}i.d(t,"c",(function(){return o})),i.d(t,"e",(function(){return s})),i.d(t,"i",(function(){return n})),i.d(t,"h",(function(){return r})),i.d(t,"g",(function(){return h})),i.d(t,"f",(function(){return a})),i.d(t,"a",(function(){return u})),i.d(t,"b",(function(){return c})),i.d(t,"d",(function(){return l})),Number.prototype.clamp=function(e,t){return Math.min(Math.max(this,e),t)};const n=e=>e[Math.floor(Math.random()*e.length)];function r(e,t){return null==e&&(e=0),null==t&&(t=1),e+Math.random()*(t-e)}function h(e,t){return null==e&&(e=0),null==t&&(t=1),Math.floor(e+Math.random()*(t-e+1))}const a=e=>document.querySelector(e),u=e=>"number"==typeof e?"#"+("00000"+e.toString(16)).slice(-6):e,c=(e,t=1)=>{const i=u(e),o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(i),s=o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:null;return"rgba("+s.r+","+s.g+","+s.b+","+t+")"},l=e=>.299*e.r+.587*e.g+.114*e.b},function(e,t,i){"use strict";i.d(t,"a",(function(){return r}));var o=i(0);const s="object"==typeof window;let n=s&&window.THREE||{};s&&!window.VANTA&&(window.VANTA={});const r=s&&window.VANTA||{};r.register=(e,t)=>r[e]=e=>new t(e),r.version="0.5.21";const h=function(){return Array.prototype.unshift.call(arguments,"[VANTA]"),console.error.apply(this,arguments)};r.VantaBase=class{constructor(e={}){if(!s)return!1;r.current=this,this.windowMouseMoveWrapper=this.windowMouseMoveWrapper.bind(this),this.windowTouchWrapper=this.windowTouchWrapper.bind(this),this.windowGyroWrapper=this.windowGyroWrapper.bind(this),this.resize=this.resize.bind(this),this.animationLoop=this.animationLoop.bind(this),this.restart=this.restart.bind(this);const t="function"==typeof this.getDefaultOptions?this.getDefaultOptions():this.defaultOptions;if(this.options=Object(o.c)({mouseControls:!0,touchControls:!0,gyroControls:!1,minHeight:200,minWidth:200,scale:1,scaleMobile:1},t),(e instanceof HTMLElement||"string"==typeof e)&&(e={el:e}),Object(o.c)(this.options,e),this.options.THREE&&(n=this.options.THREE),this.el=this.options.el,null==this.el)h('Instance needs "el" param!');else if(!(this.options.el instanceof HTMLElement)){const e=this.el;if(this.el=Object(o.f)(e),!this.el)return void h("Cannot find element",e)}this.prepareEl(),this.initThree(),this.setSize();try{this.init()}catch(e){return h("Init error",e),this.renderer&&this.renderer.domElement&&this.el.removeChild(this.renderer.domElement),void(this.options.backgroundColor&&(console.log("[VANTA] Falling back to backgroundColor"),this.el.style.background=Object(o.a)(this.options.backgroundColor)))}this.initMouse(),this.resize(),this.animationLoop();const i=window.addEventListener;i("resize",this.resize),window.requestAnimationFrame(this.resize),this.options.mouseControls&&(i("scroll",this.windowMouseMoveWrapper),i("mousemove",this.windowMouseMoveWrapper)),this.options.touchControls&&(i("touchstart",this.windowTouchWrapper),i("touchmove",this.windowTouchWrapper)),this.options.gyroControls&&i("deviceorientation",this.windowGyroWrapper)}setOptions(e={}){Object(o.c)(this.options,e),this.triggerMouseMove()}prepareEl(){let e,t;if("undefined"!=typeof Node&&Node.TEXT_NODE)for(e=0;e<this.el.childNodes.length;e++){const t=this.el.childNodes[e];if(t.nodeType===Node.TEXT_NODE){const e=document.createElement("span");e.textContent=t.textContent,t.parentElement.insertBefore(e,t),t.remove()}}for(e=0;e<this.el.children.length;e++)t=this.el.children[e],"static"===getComputedStyle(t).position&&(t.style.position="relative"),"auto"===getComputedStyle(t).zIndex&&(t.style.zIndex=1);"Line truncated
|
||||
</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.
|
||||
*
|
||||
* Drop-in replacement for Vanta when CONFIG.renderer === 'clouds2d'.
|
||||
*/
|
||||
|
||||
(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('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('Program link error:', gl.getProgramInfoLog(p));
|
||||
gl.deleteProgram(p);
|
||||
return null;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// Vertex shader: fullscreen triangle covering NDC -1..+1
|
||||
var VERT_SRC = [
|
||||
'attribute vec2 aPos;',
|
||||
'void main() { gl_Position = vec4(aPos, 0.0, 1.0); }'
|
||||
].join('\n');
|
||||
|
||||
// Fragment shader: animated 2D cloud field colored by time-of-day
|
||||
var FRAG_SRC = [
|
||||
'precision highp float;',
|
||||
'uniform vec2 uResolution;',
|
||||
'uniform float uTime;',
|
||||
'uniform vec3 uSky;',
|
||||
'uniform vec3 uCloud;',
|
||||
'uniform vec3 uLight;',
|
||||
'uniform float uSpeed;',
|
||||
// Simple 2D hash + value-noise
|
||||
'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;',
|
||||
'// Cloud field with two scrolling layers (parallax)',
|
||||
'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;',
|
||||
'// Cloud mask: dense bottom, sparse top',
|
||||
'float cloud = smoothstep(0.35, 0.75, n);',
|
||||
'// Slight horizon darkening for sky distance',
|
||||
'float horizon = smoothstep(0.0, 0.45, uv.y);',
|
||||
'// Sky gradient (using y) in vertex direction',
|
||||
'float sky_t = clamp(uv.y, 0.0, 1.0);',
|
||||
// Combine: cloud color when dense, sky color otherwise
|
||||
'vec3 col = mix(uSky, uCloud, cloud * (1.0 - horizon * 0.3));',
|
||||
// Lighten near the top of clouds for the sun/light direction
|
||||
'col = mix(col, uLight, cloud * horizon * 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;
|
||||
|
||||
// Fullscreen triangle
|
||||
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 uLight = gl.getUniformLocation(prog, 'uLight');
|
||||
var uSpeed = gl.getUniformLocation(prog, 'uSpeed');
|
||||
|
||||
return {
|
||||
gl: gl, prog: prog, buf: buf, aPos: aPos,
|
||||
uRes: uRes, uTime: uTime, uSky: uSky, uCloud: uCloud,
|
||||
uLight: uLight, uSpeed: uSpeed
|
||||
};
|
||||
}
|
||||
|
||||
function hex(r, g, b) { return [r/255, g/255, b/255]; }
|
||||
function mixRgb(a, b, t) { return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t]; }
|
||||
|
||||
window.clouds2dRender = function(canvas) {
|
||||
if (!canvas._clouds2d) {
|
||||
canvas._clouds2d = init(canvas);
|
||||
if (!canvas._clouds2d) return;
|
||||
}
|
||||
var s = canvas._clouds2d;
|
||||
var gl = s.gl;
|
||||
var w = canvas.width, h = canvas.height;
|
||||
if (gl.canvas.width !== w || gl.canvas.height !== h) {
|
||||
gl.viewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
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, w, h);
|
||||
gl.uniform1f(s.uTime, performance.now() / 1000);
|
||||
|
||||
// Pick theme colors similar to the Vanta-based setup
|
||||
var cfg = window.CONFIG || {};
|
||||
var t = (cfg.weatherThemes && cfg.currentWeatherTheme && cfg.weatherThemes[cfg.currentWeatherTheme]) ||
|
||||
cfg.weatherThemes && cfg.weatherThemes[cfg.theme] ||
|
||||
null;
|
||||
var sky = [0.39, 0.54, 0.78];
|
||||
var cloud = [0.43, 0.43, 0.43];
|
||||
var light = [0.89, 0.87, 0.82];
|
||||
if (t) {
|
||||
sky = hex((t.skyColor >> 16) & 255, (t.skyColor >> 8) & 255, t.skyColor & 255);
|
||||
cloud = hex((t.cloudColor >> 16) & 255, (t.cloudColor >> 8) & 255, t.cloudColor & 255);
|
||||
light = hex((t.lightColor >> 16) & 255, (t.lightColor >> 8) & 255, t.lightColor & 255);
|
||||
}
|
||||
gl.uniform3fv(s.uSky, sky);
|
||||
gl.uniform3fv(s.uCloud, cloud);
|
||||
gl.uniform3fv(s.uLight, light);
|
||||
gl.uniform1f(s.uSpeed, 1.0);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
var noiseDataUri = 'data:image/png;base64,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 truncated
|
||||
var effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
||||
var useVanta = !(window.CONFIG && window.CONFIG.renderer === 'clouds2d');
|
||||
var effect;
|
||||
if (useVanta && typeof VANTA !== 'undefined') {
|
||||
effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
||||
el: "#vanta",
|
||||
mouseControls: false,
|
||||
touchControls: false,
|
||||
@@ -1101,6 +1289,20 @@ var effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
||||
lightColor: 0xe6dfd1,
|
||||
texturePath: noiseDataUri
|
||||
}) || { setOptions: function() {} };
|
||||
} else {
|
||||
var c2dCanvas = document.getElementById('vanta');
|
||||
var c2dRender = function() {
|
||||
if (!c2dCanvas) return;
|
||||
c2dCanvas.width = window.innerWidth;
|
||||
c2dCanvas.height = window.innerHeight;
|
||||
if (typeof window.clouds2dRender === 'function') {
|
||||
window.clouds2dRender(c2dCanvas);
|
||||
}
|
||||
};
|
||||
effect = { setOptions: function() {} };
|
||||
c2dRender();
|
||||
window.addEventListener('resize', c2dRender);
|
||||
}
|
||||
|
||||
function hexToRgb(h) {
|
||||
return [(h >> 16) & 255, (h >> 8) & 255, h & 255];
|
||||
|
||||
+19
-1
@@ -474,9 +474,13 @@
|
||||
<script src="vendor/three.min.js"></script>
|
||||
<script src="vendor/vanta.clouds.min.js"></script>
|
||||
<script src="vendor/vanta.clouds2.min.js"></script>
|
||||
<script src="vendor/clouds2d.js"></script>
|
||||
<script>
|
||||
var noiseDataUri = 'data:image/png;base64,PLACEHOLDER';
|
||||
var effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
||||
var useVanta = !(window.CONFIG && window.CONFIG.renderer === 'clouds2d');
|
||||
var effect;
|
||||
if (useVanta && typeof VANTA !== 'undefined') {
|
||||
effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
||||
el: "#vanta",
|
||||
mouseControls: false,
|
||||
touchControls: false,
|
||||
@@ -493,6 +497,20 @@ var effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
||||
lightColor: 0xe6dfd1,
|
||||
texturePath: noiseDataUri
|
||||
}) || { setOptions: function() {} };
|
||||
} else {
|
||||
var c2dCanvas = document.getElementById('vanta');
|
||||
var c2dRender = function() {
|
||||
if (!c2dCanvas) return;
|
||||
c2dCanvas.width = window.innerWidth;
|
||||
c2dCanvas.height = window.innerHeight;
|
||||
if (typeof window.clouds2dRender === 'function') {
|
||||
window.clouds2dRender(c2dCanvas);
|
||||
}
|
||||
};
|
||||
effect = { setOptions: function() {} };
|
||||
c2dRender();
|
||||
window.addEventListener('resize', c2dRender);
|
||||
}
|
||||
|
||||
function hexToRgb(h) {
|
||||
return [(h >> 16) & 255, (h >> 8) & 255, h & 255];
|
||||
|
||||
@@ -73,6 +73,7 @@ def main():
|
||||
('three', 'vendor/three.min.js'),
|
||||
('suncalc', 'vendor/suncalc.js'),
|
||||
('vanta2', 'vendor/vanta.clouds2.min.js'),
|
||||
('clouds2d', 'vendor/clouds2d.js'),
|
||||
('i18n', 'i18n.js'),
|
||||
('config', 'config.js'),
|
||||
]:
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
* below. To add a new theme, drop a new *.css file in the same
|
||||
* directory and add an entry to window.THEMES.
|
||||
*
|
||||
* renderer - Background renderer: 'vanta' (default, GPU 3D volumetric
|
||||
* clouds, heavy) or 'clouds2d' (lightweight 2D WebGL shader,
|
||||
* very low GPU usage, similar visual style).
|
||||
*
|
||||
* units - Measurement units for the weather widget.
|
||||
* 'metric' (°C, km/h) or 'imperial' (°F, mph)
|
||||
*
|
||||
@@ -72,6 +76,7 @@ window.CONFIG = {
|
||||
debug: 0,
|
||||
theme: 'ash',
|
||||
units: 'metric',
|
||||
renderer: 'clouds2d', // 'vanta' or 'clouds2d'
|
||||
language: 'de',
|
||||
timeFormat: 'DD. MMMM, YYYY ‒ HH:mm:SS',
|
||||
location: {
|
||||
|
||||
Vendored
+178
@@ -0,0 +1,178 @@
|
||||
/* 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'.
|
||||
*/
|
||||
|
||||
(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('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('Program link error:', gl.getProgramInfoLog(p));
|
||||
gl.deleteProgram(p);
|
||||
return null;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// Vertex shader: fullscreen triangle covering NDC -1..+1
|
||||
var VERT_SRC = [
|
||||
'attribute vec2 aPos;',
|
||||
'void main() { gl_Position = vec4(aPos, 0.0, 1.0); }'
|
||||
].join('\n');
|
||||
|
||||
// Fragment shader: animated 2D cloud field colored by time-of-day
|
||||
var FRAG_SRC = [
|
||||
'precision highp float;',
|
||||
'uniform vec2 uResolution;',
|
||||
'uniform float uTime;',
|
||||
'uniform vec3 uSky;',
|
||||
'uniform vec3 uCloud;',
|
||||
'uniform vec3 uLight;',
|
||||
'uniform float uSpeed;',
|
||||
// Simple 2D hash + value-noise
|
||||
'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;',
|
||||
'// Cloud field with two scrolling layers (parallax)',
|
||||
'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;',
|
||||
'// Cloud mask: dense bottom, sparse top',
|
||||
'float cloud = smoothstep(0.35, 0.75, n);',
|
||||
'// Slight horizon darkening for sky distance',
|
||||
'float horizon = smoothstep(0.0, 0.45, uv.y);',
|
||||
'// Sky gradient (using y) in vertex direction',
|
||||
'float sky_t = clamp(uv.y, 0.0, 1.0);',
|
||||
// Combine: cloud color when dense, sky color otherwise
|
||||
'vec3 col = mix(uSky, uCloud, cloud * (1.0 - horizon * 0.3));',
|
||||
// Lighten near the top of clouds for the sun/light direction
|
||||
'col = mix(col, uLight, cloud * horizon * 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;
|
||||
|
||||
// Fullscreen triangle
|
||||
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 uLight = gl.getUniformLocation(prog, 'uLight');
|
||||
var uSpeed = gl.getUniformLocation(prog, 'uSpeed');
|
||||
|
||||
return {
|
||||
gl: gl, prog: prog, buf: buf, aPos: aPos,
|
||||
uRes: uRes, uTime: uTime, uSky: uSky, uCloud: uCloud,
|
||||
uLight: uLight, uSpeed: uSpeed
|
||||
};
|
||||
}
|
||||
|
||||
function hex(r, g, b) { return [r/255, g/255, b/255]; }
|
||||
function mixRgb(a, b, t) { return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t]; }
|
||||
|
||||
window.clouds2dRender = function(canvas) {
|
||||
if (!canvas._clouds2d) {
|
||||
canvas._clouds2d = init(canvas);
|
||||
if (!canvas._clouds2d) return;
|
||||
}
|
||||
var s = canvas._clouds2d;
|
||||
var gl = s.gl;
|
||||
var w = canvas.width, h = canvas.height;
|
||||
if (gl.canvas.width !== w || gl.canvas.height !== h) {
|
||||
gl.viewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
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, w, h);
|
||||
gl.uniform1f(s.uTime, performance.now() / 1000);
|
||||
|
||||
// Pick theme colors similar to the Vanta-based setup
|
||||
var cfg = window.CONFIG || {};
|
||||
var t = (cfg.weatherThemes && cfg.currentWeatherTheme && cfg.weatherThemes[cfg.currentWeatherTheme]) ||
|
||||
cfg.weatherThemes && cfg.weatherThemes[cfg.theme] ||
|
||||
null;
|
||||
var sky = [0.39, 0.54, 0.78];
|
||||
var cloud = [0.43, 0.43, 0.43];
|
||||
var light = [0.89, 0.87, 0.82];
|
||||
if (t) {
|
||||
sky = hex((t.skyColor >> 16) & 255, (t.skyColor >> 8) & 255, t.skyColor & 255);
|
||||
cloud = hex((t.cloudColor >> 16) & 255, (t.cloudColor >> 8) & 255, t.cloudColor & 255);
|
||||
light = hex((t.lightColor >> 16) & 255, (t.lightColor >> 8) & 255, t.lightColor & 255);
|
||||
}
|
||||
gl.uniform3fv(s.uSky, sky);
|
||||
gl.uniform3fv(s.uCloud, cloud);
|
||||
gl.uniform3fv(s.uLight, light);
|
||||
gl.uniform1f(s.uSpeed, 1.0);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in new issue
Block a user