Fix clouds2d: create own canvas element

The 2D WebGL renderer now creates its own <canvas> overlay inside
the target element, instead of expecting the target to already be a
canvas. This fixes 'canvas.getContext is not a function' when the
target is a <div>.
This commit is contained in:
Richard Zink committed 2026-10-07 18:49:42 +02:00
1 parent e8df75eebc
commit c6764fd534
4 files changed
+77 -63

No files matched your search

+38 -31
View File
@@ -927,7 +927,7 @@ else window.SunCalc = SunCalc;
<body> <body>
<div id="vanta" style="background: #1c1b1f;"></div> <div id="vanta" style="background: #1c1b1f;"></div>
<div id="debug-panel"> <div id="debug-panel">
<div class="debug-title expanded" onclick="toggleDebugPanel()"><span>DEBUG - Palette: <span id="dbg-palette-name">Ash</span></span><span class="debug-title-toggle" id="debug-toggle-icon">▼</span></div> <div class="debug-title expanded" onclick="toggleDebugPanel()">\n<span>DEBUG - Palette: <span id="dbg-palette-name">Ash</span></span><span class="debug-title-toggle" id="debug-toggle-icon">▼</span></div>
<div id="debug-content" class="debug-content"> <div id="debug-content" class="debug-content">
<div class="debug-controls"> <div class="debug-controls">
<select id="dbg-theme-select" class="debug-select" onchange="setDebugTheme(this.value)"> <select id="dbg-theme-select" class="debug-select" onchange="setDebugTheme(this.value)">
@@ -1094,6 +1094,7 @@ else window.SunCalc = SunCalc;
* fragment shader to produce animated cloud-like patterns. * fragment shader to produce animated cloud-like patterns.
* *
* Drop-in replacement for Vanta when CONFIG.renderer === 'clouds2d'. * Drop-in replacement for Vanta when CONFIG.renderer === 'clouds2d'.
* Creates its own <canvas> overlay and attaches to the target element.
*/ */
(function() { (function() {
@@ -1180,7 +1181,7 @@ var FRAG_SRC = [
function init(canvas) { function init(canvas) {
var gl = canvas.getContext('webgl', { antialias: false, alpha: false }); var gl = canvas.getContext('webgl', { antialias: false, alpha: false });
if (!gl) { if (!gl) {
console.error('clouds2d: WebGL not available, using fallback'); console.error('clouds2d: WebGL not available');
return null; return null;
} }
var prog = makeProgram(gl, VERT_SRC, FRAG_SRC); var prog = makeProgram(gl, VERT_SRC, FRAG_SRC);
@@ -1209,22 +1210,38 @@ function init(canvas) {
function hex(r, g, b) { return [r/255, g/255, b/255]; } function hex(r, g, b) { return [r/255, g/255, b/255]; }
window.clouds2dRender = function(canvas) { window.clouds2dRender = function(target) {
if (!canvas) { if (!target || typeof target !== 'object') {
console.error('clouds2d: no canvas'); console.error('clouds2d: invalid target');
return; return;
} }
if (!canvas._clouds2d) { // Get or create the WebGL canvas
canvas._clouds2d = init(canvas); var canvas = target.__clouds2dCanvas;
if (!canvas._clouds2d) { if (!canvas) {
// Fallback: render the body background color via 2D canvas // Replace the target's content with a WebGL canvas
return; canvas = document.createElement('canvas');
} canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
// Empty the target and insert the canvas as the only child
while (target.firstChild) target.removeChild(target.firstChild);
target.appendChild(canvas);
target.style.position = 'relative';
target.__clouds2dCanvas = canvas;
} }
var s = canvas._clouds2d;
if (!canvas.__state) {
canvas.__state = init(canvas);
if (!canvas.__state) return;
}
var s = canvas.__state;
var gl = s.gl; var gl = s.gl;
var w = canvas.width, h = canvas.height; canvas.width = target.clientWidth || window.innerWidth;
gl.viewport(0, 0, w, h); canvas.height = target.clientHeight || window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1); gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
@@ -1234,14 +1251,12 @@ window.clouds2dRender = function(canvas) {
gl.enableVertexAttribArray(s.aPos); gl.enableVertexAttribArray(s.aPos);
gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(s.uRes, w, h); gl.uniform2f(s.uRes, canvas.width, canvas.height);
gl.uniform1f(s.uTime, performance.now() / 1000); gl.uniform1f(s.uTime, performance.now() / 1000);
// Default theme colors (used until renderWeather() sets the right ones)
var sky = [0.39, 0.54, 0.78]; var sky = [0.39, 0.54, 0.78];
var cloud = [0.43, 0.43, 0.43]; var cloud = [0.43, 0.43, 0.43];
var light = [0.89, 0.87, 0.82]; var light = [0.89, 0.87, 0.82];
// Pull from the same source as Vanta does
var cfg = window.CONFIG || {}; var cfg = window.CONFIG || {};
var theme = (cfg.currentWeatherTheme || (cfg.weatherThemes && cfg.weatherThemes[cfg.theme])) || null; var theme = (cfg.currentWeatherTheme || (cfg.weatherThemes && cfg.weatherThemes[cfg.theme])) || null;
if (theme) { if (theme) {
@@ -1257,18 +1272,6 @@ window.clouds2dRender = function(canvas) {
gl.drawArrays(gl.TRIANGLES, 0, 3); gl.drawArrays(gl.TRIANGLES, 0, 3);
}; };
// Auto-render loop: keep the render in sync with rAF so the canvas
// always shows the latest frame.
window.clouds2dStart = function() {
if (window._clouds2dRaf) return;
function loop() {
var c = document.getElementById('vanta');
if (c) window.clouds2dRender(c);
window._clouds2dRaf = requestAnimationFrame(loop);
}
window._clouds2dRaf = requestAnimationFrame(loop);
};
})(); })();
</script> </script>
<script> <script>
@@ -2455,7 +2458,11 @@ window.weatherThemes = {
} }
function tick() { function tick() {
updateClockAndEvents(); try {
updateClockAndEvents();
} catch (e) {
console.error('Tick error:', e);
}
setTimeout(tick, 1000); setTimeout(tick, 1000);
} }
@@ -2721,7 +2728,7 @@ window.weatherThemes = {
// Fetch location label from lat/lon if available // Fetch location label from lat/lon if available
fetchLocationLabel(); fetchLocationLabel();
animate(); animate();
tick(); try { tick(); } catch (e) { console.error('Initial tick error:', e); }
fetchWeather(); fetchWeather();
setInterval(fetchWeather, 15 * 60 * 1000); setInterval(fetchWeather, 15 * 60 * 1000);
+7 -3
View File
@@ -324,7 +324,7 @@
<body> <body>
<div id="vanta" style="background: #1c1b1f;"></div> <div id="vanta" style="background: #1c1b1f;"></div>
<div id="debug-panel"> <div id="debug-panel">
<div class="debug-title expanded" onclick="toggleDebugPanel()"><span>DEBUG - Palette: <span id="dbg-palette-name">Ash</span></span><span class="debug-title-toggle" id="debug-toggle-icon">▼</span></div> <div class="debug-title expanded" onclick="toggleDebugPanel()">\n<span>DEBUG - Palette: <span id="dbg-palette-name">Ash</span></span><span class="debug-title-toggle" id="debug-toggle-icon">▼</span></div>
<div id="debug-content" class="debug-content"> <div id="debug-content" class="debug-content">
<div class="debug-controls"> <div class="debug-controls">
<select id="dbg-theme-select" class="debug-select" onchange="setDebugTheme(this.value)"> <select id="dbg-theme-select" class="debug-select" onchange="setDebugTheme(this.value)">
@@ -1659,7 +1659,11 @@ window.weatherThemes = {
} }
function tick() { function tick() {
updateClockAndEvents(); try {
updateClockAndEvents();
} catch (e) {
console.error('Tick error:', e);
}
setTimeout(tick, 1000); setTimeout(tick, 1000);
} }
@@ -1925,7 +1929,7 @@ window.weatherThemes = {
// Fetch location label from lat/lon if available // Fetch location label from lat/lon if available
fetchLocationLabel(); fetchLocationLabel();
animate(); animate();
tick(); try { tick(); } catch (e) { console.error('Initial tick error:', e); }
fetchWeather(); fetchWeather();
setInterval(fetchWeather, 15 * 60 * 1000); setInterval(fetchWeather, 15 * 60 * 1000);
+1 -1
View File
@@ -73,7 +73,7 @@
* Mon Oct * Mon Oct
*/ */
window.CONFIG = { window.CONFIG = {
debug: 1, debug: 0,
theme: 'ash', theme: 'ash',
units: 'metric', units: 'metric',
renderer: 'clouds2d', // 'vanta' or 'clouds2d' renderer: 'clouds2d', // 'vanta' or 'clouds2d'
+31 -28
View File
@@ -4,6 +4,7 @@
* fragment shader to produce animated cloud-like patterns. * fragment shader to produce animated cloud-like patterns.
* *
* Drop-in replacement for Vanta when CONFIG.renderer === 'clouds2d'. * Drop-in replacement for Vanta when CONFIG.renderer === 'clouds2d'.
* Creates its own <canvas> overlay and attaches to the target element.
*/ */
(function() { (function() {
@@ -90,7 +91,7 @@ var FRAG_SRC = [
function init(canvas) { function init(canvas) {
var gl = canvas.getContext('webgl', { antialias: false, alpha: false }); var gl = canvas.getContext('webgl', { antialias: false, alpha: false });
if (!gl) { if (!gl) {
console.error('clouds2d: WebGL not available, using fallback'); console.error('clouds2d: WebGL not available');
return null; return null;
} }
var prog = makeProgram(gl, VERT_SRC, FRAG_SRC); var prog = makeProgram(gl, VERT_SRC, FRAG_SRC);
@@ -119,22 +120,38 @@ function init(canvas) {
function hex(r, g, b) { return [r/255, g/255, b/255]; } function hex(r, g, b) { return [r/255, g/255, b/255]; }
window.clouds2dRender = function(canvas) { window.clouds2dRender = function(target) {
if (!canvas) { if (!target || typeof target !== 'object') {
console.error('clouds2d: no canvas'); console.error('clouds2d: invalid target');
return; return;
} }
if (!canvas._clouds2d) { // Get or create the WebGL canvas
canvas._clouds2d = init(canvas); var canvas = target.__clouds2dCanvas;
if (!canvas._clouds2d) { if (!canvas) {
// Fallback: render the body background color via 2D canvas // Replace the target's content with a WebGL canvas
return; canvas = document.createElement('canvas');
} canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
// Empty the target and insert the canvas as the only child
while (target.firstChild) target.removeChild(target.firstChild);
target.appendChild(canvas);
target.style.position = 'relative';
target.__clouds2dCanvas = canvas;
} }
var s = canvas._clouds2d;
if (!canvas.__state) {
canvas.__state = init(canvas);
if (!canvas.__state) return;
}
var s = canvas.__state;
var gl = s.gl; var gl = s.gl;
var w = canvas.width, h = canvas.height; canvas.width = target.clientWidth || window.innerWidth;
gl.viewport(0, 0, w, h); canvas.height = target.clientHeight || window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1); gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
@@ -144,14 +161,12 @@ window.clouds2dRender = function(canvas) {
gl.enableVertexAttribArray(s.aPos); gl.enableVertexAttribArray(s.aPos);
gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(s.uRes, w, h); gl.uniform2f(s.uRes, canvas.width, canvas.height);
gl.uniform1f(s.uTime, performance.now() / 1000); gl.uniform1f(s.uTime, performance.now() / 1000);
// Default theme colors (used until renderWeather() sets the right ones)
var sky = [0.39, 0.54, 0.78]; var sky = [0.39, 0.54, 0.78];
var cloud = [0.43, 0.43, 0.43]; var cloud = [0.43, 0.43, 0.43];
var light = [0.89, 0.87, 0.82]; var light = [0.89, 0.87, 0.82];
// Pull from the same source as Vanta does
var cfg = window.CONFIG || {}; var cfg = window.CONFIG || {};
var theme = (cfg.currentWeatherTheme || (cfg.weatherThemes && cfg.weatherThemes[cfg.theme])) || null; var theme = (cfg.currentWeatherTheme || (cfg.weatherThemes && cfg.weatherThemes[cfg.theme])) || null;
if (theme) { if (theme) {
@@ -167,16 +182,4 @@ window.clouds2dRender = function(canvas) {
gl.drawArrays(gl.TRIANGLES, 0, 3); gl.drawArrays(gl.TRIANGLES, 0, 3);
}; };
// Auto-render loop: keep the render in sync with rAF so the canvas
// always shows the latest frame.
window.clouds2dStart = function() {
if (window._clouds2dRaf) return;
function loop() {
var c = document.getElementById('vanta');
if (c) window.clouds2dRender(c);
window._clouds2dRaf = requestAnimationFrame(loop);
}
window._clouds2dRaf = requestAnimationFrame(loop);
};
})(); })();