Day 20,831 Webs doodler update – 570012094830

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>