Web drawing now has wind and a few color mods
https://svonberg.org/wp-content/uploads/2026/02/webs2.html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Zen Spiderweb Generator</title>
<script src=”https://cdn.tailwindcss.com”></script>
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #111827;
font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
transition: background 1s ease;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
canvas {
display: block;
}
/* Custom Scrollbar */
.custom-scroll::-webkit-scrollbar {
width: 6px;
}
.custom-scroll::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.custom-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.custom-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Range Slider Styling */
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #e5e7eb;
cursor: pointer;
margin-top: -6px;
box-shadow: 0 0 5px rgba(0,0,0,0.5);
transition: transform 0.1s;
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
/* Section dividers */
.control-group {
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 1rem;
margin-top: 1rem;
}
/* Zen Button Specifics */
#open-btn {
z-index: 50; /* Ensure it is above everything */
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(0,0,0,0.5);
}
#open-btn:hover {
transform: scale(1.1) rotate(90deg);
}
</style>
</head>
<body>
<!– Canvas Layer –>
<div id=”canvas-container”>
<canvas id=”webCanvas”></canvas>
</div>
<!– UI Overlay –>
<div id=”ui-layer” class=”absolute top-0 right-0 p-4 md:p-6 z-10 w-full md:w-[400px] h-full pointer-events-none transition-transform duration-500 ease-in-out transform translate-x-0″>
<!– Main Controls Panel –>
<div id=”controls-panel” class=”bg-gray-900/90 backdrop-blur-md border border-gray-700/50 rounded-2xl p-5 shadow-2xl text-gray-200 custom-scroll h-full max-h-full overflow-y-auto pointer-events-auto flex flex-col”>
<div class=”flex justify-between items-center mb-4 flex-shrink-0″>
<div>
<h1 class=”text-xl font-light tracking-wider text-white”>Silk Weaver</h1>
<p class=”text-xs text-gray-400″>Procedural Generator v2.1</p>
</div>
<button id=”close-btn” class=”p-2 text-gray-300 hover:text-white transition-colors bg-white/10 hover:bg-white/20 rounded-lg flex items-center gap-2″ title=”Enter Zen Mode”>
<span class=”text-xs font-medium uppercase tracking-wider”>Zen Mode</span>
<svg xmlns=”http://www.w3.org/2000/svg” width=”18″ height=”18″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round”><path d=”M15 3h6v6M14 10l6.1-6.1M9 21H3v-6M10 14l-6.1 6.1″/></svg>
</button>
</div>
<div class=”space-y-4 flex-grow”>
<!– THEME SELECTOR –>
<div>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-2 block”>Atmosphere</label>
<div class=”grid grid-cols-4 gap-2″>
<button class=”theme-btn bg-gray-800 border-2 border-indigo-500 rounded h-8 w-full hover:brightness-110 transition-all” data-theme=”midnight” title=”Midnight”></button>
<button class=”theme-btn bg-green-900 border-2 border-transparent hover:border-gray-400 rounded h-8 w-full transition-all” data-theme=”forest” title=”Forest”></button>
<button class=”theme-btn bg-purple-900 border-2 border-transparent hover:border-gray-400 rounded h-8 w-full transition-all” data-theme=”sunset” title=”Sunset”></button>
<button class=”theme-btn bg-black border-2 border-transparent hover:border-gray-400 rounded h-8 w-full transition-all” data-theme=”cyber” title=”Cyber”></button>
</div>
</div>
<!– CORE SHAPE –>
<div class=”control-group”>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-3 block”>Structure</label>
<div class=”space-y-4″>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Radial Spokes</span>
<span id=”val-density”>12</span>
</div>
<input type=”range” id=”density” min=”5″ max=”30″ value=”12″ step=”1″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Spiral Spacing</span>
<span id=”val-spacing”>20</span>
</div>
<input type=”range” id=”spacing” min=”10″ max=”60″ value=”20″ step=”5″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Center Size</span>
<span id=”val-centerSize”>50</span>
</div>
<input type=”range” id=”centerSize” min=”0″ max=”200″ value=”50″ step=”10″>
</div>
</div>
</div>
<!– PHYSICS & CHAOS –>
<div class=”control-group”>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-3 block”>Physics & Chaos</label>
<div class=”space-y-4″>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Thread Slack (Gravity)</span>
<span id=”val-slack”>0.5</span>
</div>
<input type=”range” id=”slack” min=”0″ max=”1″ value=”0.5″ step=”0.1″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Tear Probability</span>
<span id=”val-tears”>0%</span>
</div>
<input type=”range” id=”tears” min=”0″ max=”0.4″ value=”0″ step=”0.05″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Irregularity</span>
<span id=”val-chaos”>0.3</span>
</div>
<input type=”range” id=”chaos” min=”0″ max=”1″ value=”0.3″ step=”0.1″>
</div>
</div>
</div>
<!– VISUALS –>
<div class=”control-group”>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-3 block”>Visual FX</label>
<div class=”space-y-4″>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Glow Strength</span>
<span id=”val-glow”>Low</span>
</div>
<input type=”range” id=”glow” min=”0″ max=”20″ value=”0″ step=”1″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Wind Speed</span>
<span id=”val-wind”>0</span>
</div>
<input type=”range” id=”wind” min=”0″ max=”5″ value=”0″ step=”0.5″>
</div>
<div class=”flex items-center justify-between”>
<span class=”text-sm text-gray-300″>Morning Dew</span>
<label class=”relative inline-flex items-center cursor-pointer”>
<input type=”checkbox” id=”dew-toggle” class=”sr-only peer”>
<div class=”w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[”] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-500″></div>
</label>
</div>
</div>
</div>
<!– SYSTEM –>
<div class=”control-group”>
<div class=”flex items-center justify-between”>
<span class=”text-sm text-gray-300″>Instant Draw</span>
<label class=”relative inline-flex items-center cursor-pointer”>
<input type=”checkbox” id=”instant-toggle” class=”sr-only peer”>
<div class=”w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[”] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-500″></div>
</label>
</div>
</div>
</div>
<!– Actions –>
<div class=”mt-6 grid grid-cols-2 gap-3 flex-shrink-0″>
<button id=”generate-btn” class=”col-span-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium py-3 px-4 rounded-xl transition-all shadow-lg shadow-indigo-500/20 active:scale-95 flex justify-center items-center gap-2″>
<svg xmlns=”http://www.w3.org/2000/svg” width=”16″ height=”16″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round”><path d=”M21 12a9 9 0 1 1-6.219-8.56″/></svg>
Re-Weave
</button>
<button id=”download-btn” class=”bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm py-2 px-3 rounded-lg transition-colors border border-gray-700″>
Save Img
</button>
<button id=”clear-btn” class=”bg-gray-800 hover:bg-red-900/30 text-gray-300 hover:text-red-200 text-sm py-2 px-3 rounded-lg transition-colors border border-gray-700″>
Clear
</button>
</div>
</div>
</div>
<!– Zen Mode Restore Button (Fixed Visibility) –>
<button id=”open-btn” class=”fixed bottom-6 right-6 bg-white/10 hover:bg-white/20 text-white p-4 rounded-full backdrop-blur-md border border-white/30 hidden transition-all” title=”Exit Zen Mode”>
<svg xmlns=”http://www.w3.org/2000/svg” width=”28″ height=”28″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round”><circle cx=”12″ cy=”12″ r=”3″></circle><path d=”M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z”></path></svg>
</button>
<script>
/**
* Core Logic for Spiderweb Generator
*/
const canvas = document.getElementById(‘webCanvas’);
const ctx = canvas.getContext(‘2d’);
// State
let width, height;
let centerX, centerY;
let animationFrameId;
let time = 0; // For wind animation
// Web Data
let spokes = [];
let spirals = [];
let currentDrawIndex = 0;
let isConstructing = false; // Is the initial build animation running?
// Theme Configuration
const themes = {
midnight: { bg: ‘#111827’, web: ‘rgba(255, 255, 255, 0.2)’, dew: ‘rgba(255, 255, 255, 0.6)’ },
forest: { bg: ‘#064e3b’, web: ‘rgba(209, 250, 229, 0.2)’, dew: ‘rgba(167, 243, 208, 0.6)’ },
sunset: { bg: ‘linear-gradient(to bottom, #4c1d95, #c2410c)’, web: ‘rgba(254, 215, 170, 0.3)’, dew: ‘rgba(255, 237, 213, 0.7)’ },
cyber: { bg: ‘#000000’, web: ‘rgba(50, 255, 100, 0.3)’, dew: ‘rgba(50, 255, 100, 0.8)’ }
};
// Settings
const settings = {
theme: ‘midnight’,
spokeCount: 12,
spacing: 20, // Distance between spiral loops
centerSize: 50,
slack: 0.5,
chaos: 0.3,
tears: 0.0,
glow: 0,
wind: 0.0,
speed: 15,
dew: false,
instant: false
};
// UI Elements
const uiLayer = document.getElementById(‘ui-layer’);
const openBtn = document.getElementById(‘open-btn’);
const themeBtns = document.querySelectorAll(‘.theme-btn’);
// Helper: Random Range
const random = (min, max) => Math.random() * (max – min) + min;
// Helper: Apply Wind to a Point
// Returns a new object so we don’t mutate original geometry permanently
function applyWind(point) {
if (settings.wind <= 0.1) return point;
// Simple vertex displacement based on time and Y position
// Stronger effect further from center?
const dist = Math.sqrt((point.x – centerX)**2 + (point.y – centerY)**2);
const distFactor = Math.min(dist / 500, 1);
const offsetX = Math.sin(time * 0.002 + point.y * 0.01) * (settings.wind * 10) * distFactor;
const offsetY = Math.cos(time * 0.003 + point.x * 0.01) * (settings.wind * 5) * distFactor;
return {
x: point.x + offsetX,
y: point.y + offsetY
};
}
// Initialization
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
centerX = width / 2;
centerY = height / 2;
generateWebData();
startDrawing();
}
window.addEventListener(‘resize’, resize);
// Generate the mathematical model of the web
function generateWebData() {
spokes = [];
spirals = [];
// 1. Generate Spokes (Radials)
const baseAngle = (Math.PI * 2) / settings.spokeCount;
const maxRadius = Math.max(width, height) * 0.8;
for (let i = 0; i < settings.spokeCount; i++) {
let angleOffset = (Math.random() – 0.5) * settings.chaos;
let angle = (i * baseAngle) + angleOffset;
let length = maxRadius * random(0.8, 1.2);
spokes.push({
angle: angle,
length: length,
endX: centerX + Math.cos(angle) * length,
endY: centerY + Math.sin(angle) * length
});
}
// 2. Generate Spiral Segments
let currentRadius = settings.centerSize;
let spokeIndex = 0;
// Prevent infinite loops
let safetyCount = 0;
while (safetyCount < 5000) {
safetyCount++;
let s1 = spokes[spokeIndex];
let nextIndex = (spokeIndex + 1) % spokes.length;
let s2 = spokes[nextIndex];
// R1 and R2 allow the spiral to spiral outwards
let r1 = currentRadius + random(-5, 5) * (settings.chaos * 5);
// Look ahead: The connection point on the next spoke is slightly further out
let spiralStep = (settings.spacing / settings.spokeCount);
let r2 = r1 + spiralStep + random(-2, 2) * (settings.chaos * 5);
// Stop if off screen
if (r1 > s1.length || r2 > s2.length) break;
// Probability to skip a segment (Tear)
if (Math.random() > settings.tears) {
let p1 = {
x: centerX + Math.cos(s1.angle) * r1,
y: centerY + Math.sin(s1.angle) * r1
};
let p2 = {
x: centerX + Math.cos(s2.angle) * r2,
y: centerY + Math.sin(s2.angle) * r2
};
// Control Point for Catenary (gravity sag)
let midX = (p1.x + p2.x) / 2;
let midY = (p1.y + p2.y) / 2;
// Pull vector towards center
let dirX = midX – centerX;
let dirY = midY – centerY;
let pullFactor = settings.slack * 0.4;
let cpX = midX – (dirX * pullFactor);
let cpY = midY – (dirY * pullFactor);
spirals.push({
p1: p1,
p2: p2,
cp: {x: cpX, y: cpY},
dew: Math.random() > 0.7
});
}
// Advance
spokeIndex = nextIndex;
currentRadius += spiralStep; // Increment radius constantly around the spiral
if (currentRadius > Math.max(width, height) * 0.7) break;
}
}
function getThemeColors() {
return themes[settings.theme] || themes[‘midnight’];
}
function drawDew(p1, p2, count) {
const theme = getThemeColors();
ctx.fillStyle = theme.dew;
for(let i=1; i<count; i++) {
const t = i/count;
const x = p1.x + (p2.x – p1.x) * t;
const y = p1.y + (p2.y – p1.y) * t;
ctx.beginPath();
ctx.arc(x, y, random(0.5, 1.5), 0, Math.PI * 2);
ctx.fill();
}
}
function drawDewOnCurve(p1, cp, p2) {
const theme = getThemeColors();
ctx.fillStyle = theme.dew;
const drops = Math.floor(random(1, 4));
for (let i = 0; i < drops; i++) {
const t = random(0.2, 0.8);
// Bezier Point
const x = (1 – t) * (1 – t) * p1.x + 2 * (1 – t) * t * cp.x + t * t * p2.x;
const y = (1 – t) * (1 – t) * p1.y + 2 * (1 – t) * t * cp.y + t * t * p2.y;
ctx.beginPath();
ctx.arc(x, y, random(0.5, 1.8), 0, Math.PI * 2);
ctx.fill();
}
}
function startDrawing() {
if (isConstructing) isConstructing = false; // Reset current build
currentDrawIndex = 0;
// Set Background based on theme
const theme = getThemeColors();
if (theme.bg.includes(‘gradient’)) {
document.body.style.background = theme.bg;
} else {
document.body.style.backgroundColor = theme.bg;
document.body.style.backgroundImage = ‘none’;
}
if (settings.instant) {
currentDrawIndex = spirals.length;
} else {
isConstructing = true;
}
if (!animationFrameId) animate();
}
function renderFrame() {
ctx.clearRect(0, 0, width, height);
const theme = getThemeColors();
// Setup Glow
if (settings.glow > 0) {
ctx.shadowBlur = settings.glow;
ctx.shadowColor = theme.web;
} else {
ctx.shadowBlur = 0;
}
// 1. Draw Spokes
ctx.strokeStyle = theme.web;
ctx.lineWidth = 1;
ctx.lineCap = “round”;
// Only draw spokes if we are generating or they are always visible
spokes.forEach(spoke => {
const start = applyWind({x: centerX, y: centerY});
const end = applyWind({x: spoke.endX, y: spoke.endY});
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
});
// 2. Draw Spirals
// If constructing, limit by currentDrawIndex. If done, draw all.
const limit = isConstructing ? currentDrawIndex : spirals.length;
// Batch drawing for performance? Canvas handles single paths better
// But we need individual segments for wind
ctx.strokeStyle = theme.web; // Re-apply incase changed
ctx.lineWidth = Math.max(0.5, 0.8 – (settings.spokeCount * 0.01)); // Thinner webs for high density
ctx.beginPath();
for(let i=0; i < limit; i++) {
let seg = spirals[i];
// Apply wind to all control points
let p1 = applyWind(seg.p1);
let p2 = applyWind(seg.p2);
let cp = applyWind(seg.cp);
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(cp.x, cp.y, p2.x, p2.y);
}
ctx.stroke();
// 3. Draw Dew (Separate pass for color change)
if (settings.dew) {
ctx.shadowBlur = 0; // No glow on dew for crispness
for(let i=0; i < limit; i++) {
let seg = spirals[i];
if (seg.dew) {
// Recalculate positions for dew to match wind
let p1 = applyWind(seg.p1);
let p2 = applyWind(seg.p2);
let cp = applyWind(seg.cp);
drawDewOnCurve(p1, cp, p2);
}
}
}
// Logic for Construction Animation
if (isConstructing) {
let speed = Math.floor(settings.speed + (currentDrawIndex / 50));
currentDrawIndex += speed;
if (currentDrawIndex >= spirals.length) {
currentDrawIndex = spirals.length;
isConstructing = false;
}
}
}
function animate(timestamp) {
time = timestamp || 0;
renderFrame();
animationFrameId = requestAnimationFrame(animate);
}
// UI Interactions
function updateDisplay(id, val) {
const el = document.getElementById(`val-${id}`);
if (el) el.innerText = val;
}
// Attach listeners to all range inputs
[‘density’, ‘spacing’, ‘centerSize’, ‘slack’, ‘chaos’, ‘tears’, ‘glow’, ‘wind’].forEach(id => {
const input = document.getElementById(id);
input.addEventListener(‘input’, (e) => {
let val = parseFloat(e.target.value);
// Special formatting
if (id === ‘tears’) updateDisplay(id, Math.round(val * 100) + ‘%’);
else if (id === ‘glow’) updateDisplay(id, val === 0 ? ‘Off’ : (val > 15 ? ‘High’ : ‘Low’));
else updateDisplay(id, val);
// Map to settings
if (id === ‘density’) settings.spokeCount = parseInt(val);
else if (id === ‘spacing’) settings.spacing = parseInt(val);
else if (id === ‘centerSize’) settings.centerSize = parseInt(val);
else settings[id] = val;
// Regen logic
if ([‘density’, ‘spacing’, ‘centerSize’, ‘chaos’, ‘tears’, ‘slack’].includes(id)) {
generateWebData();
if (!settings.instant && !isConstructing) startDrawing(); // Restart anim if structural change
}
});
});
// Theme Buttons
themeBtns.forEach(btn => {
btn.addEventListener(‘click’, (e) => {
themeBtns.forEach(b => b.classList.remove(‘border-indigo-500’));
themeBtns.forEach(b => b.classList.add(‘border-transparent’));
e.target.classList.remove(‘border-transparent’);
e.target.classList.add(‘border-indigo-500’);
settings.theme = e.target.getAttribute(‘data-theme’);
startDrawing();
});
});
document.getElementById(‘dew-toggle’).addEventListener(‘change’, (e) => {
settings.dew = e.target.checked;
});
document.getElementById(‘instant-toggle’).addEventListener(‘change’, (e) => {
settings.instant = e.target.checked;
});
document.getElementById(‘generate-btn’).addEventListener(‘click’, () => {
generateWebData();
startDrawing();
});
document.getElementById(‘clear-btn’).addEventListener(‘click’, () => {
isConstructing = false;
currentDrawIndex = 0;
spokes = [];
spirals = [];
ctx.clearRect(0,0,width,height);
});
document.getElementById(‘download-btn’).addEventListener(‘click’, () => {
const link = document.createElement(‘a’);
link.download = `spiderweb_${settings.theme}.png`;
link.href = canvas.toDataURL();
link.click();
});
// Zen Mode Logic
const closeBtn = document.getElementById(‘close-btn’);
let uiVisible = true;
function toggleUI() {
uiVisible = !uiVisible;
if (uiVisible) {
uiLayer.classList.remove(‘translate-x-full’);
openBtn.classList.add(‘hidden’);
} else {
uiLayer.classList.add(‘translate-x-full’);
// Remove hidden immediately, CSS transitions handle the rest if you want opacity,
// but for now simple display toggling ensures it’s clickable.
openBtn.classList.remove(‘hidden’);
}
}
closeBtn.addEventListener(‘click’, toggleUI);
openBtn.addEventListener(‘click’, toggleUI);
document.addEventListener(‘click’, (e) => {
// If Zen mode is active (UI hidden) and we aren’t clicking the restore button
if (!uiVisible && !openBtn.contains(e.target)) {
// Gentle regen in Zen mode
generateWebData();
startDrawing();
}
});
// Start
resize();
animate();
</script>
</body>
</html>






























