← Mike Higgins Home
Custom Code Experiment •

Drifting Clouds

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Drifting Clouds</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
        }
        body {
            background: #0a2f5a;
            overflow: hidden;
            height: 100vh;
            width: 100vw;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
            cursor: crosshair;
        }
        .info {
            position: fixed;
            bottom: 16px;
            right: 20px;
            color: rgba(255,255,255,0.7);
            background: rgba(0,0,0,0.3);
            backdrop-filter: blur(4px);
            padding: 6px 14px;
            border-radius: 40px;
            font-size: 13px;
            font-family: monospace;
            pointer-events: none;
            z-index: 10;
        }
    </style>
</head>
<body>
    <canvas id="skyCanvas"></canvas>
    <div class="info">☁️ clouds drifting · infinite horizon</div>

    <script>
        (function() {
            const canvas = document.getElementById('skyCanvas');
            if (!canvas) {
                console.error('❌ Canvas element with id "skyCanvas" not found!');
                return;
            }
            let ctx = canvas.getContext('2d');
            let clouds = [];
            let animationId = null;
            let isAnimating = true;

            // helper
            function randomRange(min, max) {
                return min + Math.random() * (max - min);
            }

            // create a fluffy cloud from overlapping circles
            function createCloud(x, y, baseSize, speed) {
                const template = [
                    { dx: 0.0,  dy: 0.0,  rFactor: 1.0 },
                    { dx: -0.42, dy: -0.28, rFactor: 0.72 },
                    { dx: 0.48,  dy: -0.22, rFactor: 0.78 },
                    { dx: 0.22,  dy: 0.35,  rFactor: 0.68 },
                    { dx: -0.35, dy: 0.30,  rFactor: 0.64 },
                    { dx: 0.52,  dy: 0.08,  rFactor: 0.60 },
                    { dx: -0.50, dy: 0.05,  rFactor: 0.58 },
                    { dx: 0.05,  dy: -0.48, rFactor: 0.55 },
                    { dx: -0.15, dy: 0.55,  rFactor: 0.52 }
                ];
                const circles = [];
                for (let t of template) {
                    let dxJitter = (Math.random() - 0.5) * 0.1;
                    let dyJitter = (Math.random() - 0.5) * 0.1;
                    let rJitter = 0.92 + Math.random() * 0.16;
                    let rFactor = Math.min(1.05, Math.max(0.48, t.rFactor * rJitter));
                    circles.push({
                        dx: t.dx + dxJitter,
                        dy: t.dy + dyJitter,
                        radiusFactor: rFactor
                    });
                }
                // compute actual radius and max extent
                let maxExtent = 0;
                const actualCircles = circles.map(c => {
                    const rad = baseSize * c.radiusFactor;
                    const distFromCenter = Math.hypot(c.dx * baseSize, c.dy * baseSize);
                    const furthest = distFromCenter + rad;
                    if (furthest > maxExtent) maxExtent = furthest;
                    return { dx: c.dx, dy: c.dy, radius: rad };
                });
                return {
                    x, y, baseSize, speed,
                    circles: actualCircles,
                    maxExtent: maxExtent,
                    alpha: 0.92 + Math.random() * 0.07
                };
            }

            // draw one cloud
            function drawCloud(ctx, cloud) {
                ctx.save();
                ctx.globalAlpha = cloud.alpha;
                ctx.fillStyle = '#FFFFFF';
                for (let circ of cloud.circles) {
                    ctx.beginPath();
                    ctx.arc(cloud.x + circ.dx, cloud.y + circ.dy, circ.radius, 0, Math.PI * 2);
                    ctx.fill();
                }
                ctx.restore();
            }

            // draw sky gradient
            function drawSky(ctx, w, h) {
                const grad = ctx.createLinearGradient(0, 0, 0, h);
                grad.addColorStop(0, '#4a90e2');
                grad.addColorStop(0.4, '#6bb5ff');
                grad.addColorStop(0.7, '#8fcbff');
                grad.addColorStop(1, '#b9dcff');
                ctx.fillStyle = grad;
                ctx.fillRect(0, 0, w, h);
                // optional faint haze
                ctx.fillStyle = 'rgba(255,255,255,0.05)';
                ctx.fillRect(0, h * 0.7, w, h * 0.3);
            }

            // update positions and wrap around
            function updateClouds(canvasW, canvasH) {
                for (let c of clouds) {
                    c.x += c.speed;
                    // wrap right → left
                    if (c.x > canvasW + c.maxExtent) {
                        c.x = -c.maxExtent;
                        // slight random vertical shift for variety
                        let minY = c.maxExtent;
                        let maxY = canvasH - c.maxExtent;
                        if (maxY > minY) {
                            let delta = (Math.random() - 0.5) * 30;
                            c.y = Math.min(maxY, Math.max(minY, c.y + delta));
                        } else {
                            c.y = canvasH / 2;
                        }
                    }
                    // vertical clamping
                    let minY = c.maxExtent;
                    let maxY = canvasH - c.maxExtent;
                    if (maxY > minY) {
                        c.y = Math.min(maxY, Math.max(minY, c.y));
                    } else {
                        c.y = canvasH / 2;
                    }
                }
            }

            // generate a set of clouds for current canvas size
            function generateClouds(w, h) {
                let count = Math.min(14, Math.max(7, Math.floor(6 + w / 400 + Math.random() * 3)));
                let newClouds = [];
                for (let i = 0; i < count; i++) {
                    let baseSize = randomRange(48, 105);
                    let speed = randomRange(0.35, 0.85) * (w / 1300);
                    speed = Math.min(1.2, Math.max(0.3, speed));
                    let randX = randomRange(-baseSize * 1.5, w + baseSize);
                    // estimate Y bounds
                    let approxExtent = baseSize * 1.3;
                    let minY = approxExtent + 10;
                    let maxY = h - approxExtent - 10;
                    if (maxY <= minY) {
                        minY = 30;
                        maxY = h - 30;
                    }
                    let randY = randomRange(minY, maxY);
                    let cloud = createCloud(randX, randY, baseSize, speed);
                    // re-clamp Y with exact maxExtent
                    let exactMin = cloud.maxExtent;
                    let exactMax = h - cloud.maxExtent;
                    if (exactMax > exactMin) {
                        cloud.y = Math.min(exactMax, Math.max(exactMin, randY));
                    } else {
                        cloud.y = h / 2;
                    }
                    newClouds.push(cloud);
                }
                return newClouds;
            }

            // resize handler – reposition clouds proportionally
            function resizeCanvasAndClouds() {
                const newW = window.innerWidth;
                const newH = window.innerHeight;
                if (newW === 0 || newH === 0) return;

                const oldW = canvas.width;
                const oldH = canvas.height;
                canvas.width = newW;
                canvas.height = newH;
                ctx = canvas.getContext('2d');  // refresh context (safe)

                if (clouds.length > 0 && oldW > 0 && oldH > 0) {
                    const ratioX = newW / oldW;
                    const ratioY = newH / oldH;
                    for (let c of clouds) {
                        c.x *= ratioX;
                        c.y *= ratioY;
                        // keep inside vertical bounds
                        let minY = c.maxExtent;
                        let maxY = newH - c.maxExtent;
                        if (maxY > minY) c.y = Math.min(maxY, Math.max(minY, c.y));
                        else c.y = newH / 2;
                        // horizontal wrap safety
                        if (c.x > newW + c.maxExtent) c.x = -c.maxExtent;
                        if (c.x < -c.maxExtent) c.x = newW + c.maxExtent;
                    }
                } else {
                    clouds = generateClouds(newW, newH);
                }
                // force a redraw
                if (ctx) {
                    drawSky(ctx, newW, newH);
                    for (let c of clouds) drawCloud(ctx, c);
                }
            }

            // animation loop
            function animate() {
                if (!isAnimating) return;
                if (!ctx || !canvas) return;
                const w = canvas.width;
                const h = canvas.height;
                if (w === 0 || h === 0) {
                    animationId = requestAnimationFrame(animate);
                    return;
                }
                drawSky(ctx, w, h);
                for (let c of clouds) drawCloud(ctx, c);
                updateClouds(w, h);
                animationId = requestAnimationFrame(animate);
            }

            // debounced resize
            let resizeTimer;
            function onResize() {
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(() => {
                    resizeCanvasAndClouds();
                }, 100);
            }

            // initialization
            function init() {
                canvas.width = window.innerWidth;
                canvas.height = window.innerHeight;
                ctx = canvas.getContext('2d');
                clouds = generateClouds(canvas.width, canvas.height);
                isAnimating = true;
                animate();
                window.addEventListener('resize', onResize);
                console.log('✅ Animation started | Canvas size:', canvas.width, 'x', canvas.height);
            }

            // start when DOM ready
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', init);
            } else {
                init();
            }

            // optional cleanup
            window.addEventListener('beforeunload', () => {
                if (animationId) cancelAnimationFrame(animationId);
                window.removeEventListener('resize', onResize);
            });
        })();
    </script>
</body>
</html>