コード · Mega Bubble Phonics

CC BY 4.0 · @joeのクレジットを残してください · v1

← ゲームの詳細

持っていく。リミックスする。授業で使う。

下のコードをChatGPT、Claude、Gemini、Grokなど好きなツールにコピーし、クラス向けに変えてから、クレジット付きでリミックスをここに公開してください。

⬇ HTMLをダウンロード ⬇ ZIPをダウンロード
AI編集プロンプトの下書き
この教室ゲームを電子黒板向けに編集してください。大きなタッチ対象と学級全体での遊びを保ってください。トラッキング、ログイン、個人情報の収集、遠隔APIの呼び出しは禁止です。単一HTMLの構成を保ってください。教育目標は保ってください。元の制作者: ClassBoard Gamesの@joe(CC BY 4.0)。
mega-bubble-phonics.html · 26.5 KB
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Mega Bubble Phonics</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap');

        body {
            margin: 0;
            overflow: hidden;
            background-color: #87CEEB; /* Soft Sky Blue */
            font-family: 'Fredoka', sans-serif;
            touch-action: none; /* Prevent pull-to-refresh on mobile */
            user-select: none;
        }

        #gameCanvas {
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }

        /* UI Overlay */
        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 10;
            pointer-events: none; /* Let clicks pass through to canvas */
        }

        .pointer-events-auto {
            pointer-events: auto;
        }

        .hud-text {
            text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2), 0 0 10px rgba(255, 255, 255, 0.8);
        }

        /* Cute Modal Styles */
        .cute-panel {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(10px);
            border: 4px solid #FF9AA2; /* Pastel Pink */
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), inset 0 0 20px rgba(255, 154, 162, 0.2);
            border-radius: 2rem;
        }

        .cute-button {
            transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
            border-bottom-width: 6px;
        }
        
        .cute-button:active {
            transform: translateY(4px);
            border-bottom-width: 2px;
        }

        /* Animations */
        @keyframes bounce-in {
            0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
            80% { transform: translate(-50%, -50%) scale(1.05); opacity: 1; }
            100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
        }

        .modal-animate {
            animation: bounce-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
        }

        .shake {
            animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
        }

        @keyframes shake {
            10%, 90% { transform: translate3d(-2px, 0, 0); }
            20%, 80% { transform: translate3d(4px, 0, 0); }
            30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
            40%, 60% { transform: translate3d(6px, 0, 0); }
        }

        @keyframes float-up {
            0% { transform: translateY(0px); }
            50% { transform: translateY(-15px); }
            100% { transform: translateY(0px); }
        }

        .floating {
            animation: float-up 3s ease-in-out infinite;
        }
    </style>
</head>
<body>

    <!-- Game Canvas -->
    <canvas id="gameCanvas"></canvas>

    <!-- UI Layer -->
    <div id="ui-layer" class="flex flex-col justify-between p-4 md:p-6">
        
        <!-- HUD -->
        <div class="flex justify-between items-start w-full pointer-events-none">
            <div class="bg-white/80 border-4 border-yellow-400 p-3 rounded-2xl shadow-lg">
                <div class="text-sm font-bold text-yellow-500 uppercase tracking-widest text-center">Stars</div>
                <div class="text-4xl font-black text-orange-500 hud-text text-center" id="scoreDisplay">0</div>
            </div>
            
            <div class="flex gap-2 bg-white/80 border-4 border-red-300 p-3 rounded-2xl shadow-lg" id="livesContainer">
                <!-- Hearts injected via JS -->
            </div>
        </div>

        <!-- Question Modal (Hidden by default) -->
        <div id="questionModal" class="hidden absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-11/12 max-w-sm cute-panel p-6 pointer-events-auto">
            <div class="text-center mb-4">
                <h2 class="text-2xl font-bold text-pink-500 mb-2">What's in the bubble?</h2>
                <div class="text-8xl my-4 drop-shadow-md" id="questionEmoji">
                    <!-- Emoji goes here -->
                </div>
            </div>

            <div class="grid grid-cols-1 gap-4" id="optionsGrid">
                <!-- Options injected via JS -->
            </div>
        </div>

        <!-- Start Screen -->
        <div id="startScreen" class="absolute inset-0 bg-sky-300/90 backdrop-blur-sm z-50 flex flex-col items-center justify-center pointer-events-auto">
            <div class="text-center px-6 max-w-lg floating">
                <div class="text-8xl mb-4 drop-shadow-lg">🫧✨</div>
                <h1 class="text-5xl md:text-7xl font-black text-white mb-2 hud-text" style="color: #FF9AA2; -webkit-text-stroke: 2px white;">MEGA BUBBLE<br>PHONICS</h1>
                <div class="bg-yellow-400 text-yellow-900 font-bold px-4 py-1 rounded-full inline-block mb-4 border-2 border-white shadow-sm">50 Words Edition!</div>
                <p class="text-white text-xl md:text-2xl mb-8 font-bold drop-shadow-md">Pop the cute bubbles before they float into the rainbow portal!</p>
                
                <button onclick="startGame()" class="cute-button bg-green-400 hover:bg-green-300 text-white border-green-600 font-bold py-5 px-12 rounded-full shadow-lg text-2xl uppercase tracking-wider w-full md:w-auto">
                    Play Now! 🚀
                </button>
            </div>
        </div>

        <!-- Game Over Screen -->
        <div id="gameOverScreen" class="hidden absolute inset-0 bg-blue-900/90 backdrop-blur-sm z-50 flex flex-col items-center justify-center pointer-events-auto">
            <div class="text-center px-6 floating">
                <div class="text-7xl mb-4">😴✨</div>
                <h2 class="text-5xl md:text-6xl font-black text-yellow-300 mb-4 hud-text">Sleepy Time!</h2>
                <div class="bg-white/20 p-6 rounded-3xl mb-8 border-4 border-white/30">
                    <p class="text-white text-2xl mb-2 font-bold">You collected</p>
                    <p class="text-6xl font-black text-yellow-400 drop-shadow-lg mb-2"><span id="finalScore">0</span></p>
                    <p class="text-white text-2xl font-bold">Stars! ⭐</p>
                </div>
                <button onclick="resetGame()" class="cute-button bg-pink-400 hover:bg-pink-300 text-white border-pink-600 font-bold py-4 px-10 rounded-full shadow-lg text-2xl uppercase tracking-wider">
                    Play Again! 🎈
                </button>
            </div>
        </div>

    </div>

    <script>
        /**
         * CVC DATA REPOSITORY - 50 CVC Words!
         * Includes Short A, E, I, O, U
         */
        const CVC_DATA = [
            // Short A (10 words)
            { word: "cat", emoji: "🐱" }, { word: "bat", emoji: "🦇" }, { word: "map", emoji: "🗺️" }, 
            { word: "dad", emoji: "👨" }, { word: "pan", emoji: "🍳" },
            { word: "hat", emoji: "🎩" }, { word: "mad", emoji: "😠" }, { word: "cap", emoji: "🧢" }, 
            { word: "rat", emoji: "🐀" }, { word: "jam", emoji: "🍓" },
            
            // Short E (10 words)
            { word: "bed", emoji: "🛏️" }, { word: "red", emoji: "🔴" }, { word: "pen", emoji: "🖊️" }, 
            { word: "ten", emoji: "🔟" }, { word: "net", emoji: "🥅" },
            { word: "leg", emoji: "🦵" }, { word: "web", emoji: "🕸️" }, { word: "wet", emoji: "💦" }, 
            { word: "hen", emoji: "🐔" }, { word: "peg", emoji: "📍" },
            
            // Short I (10 words)
            { word: "pig", emoji: "🐷" }, { word: "sit", emoji: "🪑" }, { word: "big", emoji: "🐘" }, 
            { word: "lip", emoji: "👄" }, { word: "win", emoji: "🏆" },
            { word: "fin", emoji: "🦈" }, { word: "hit", emoji: "🎯" }, { word: "zip", emoji: "🤐" }, 
            { word: "kid", emoji: "🧒" }, { word: "dip", emoji: "🥣" },
            
            // Short O (10 words)
            { word: "dog", emoji: "🐶" }, { word: "hot", emoji: "♨️" }, { word: "pot", emoji: "🍲" }, 
            { word: "box", emoji: "📦" }, { word: "top", emoji: "🪀" },
            { word: "fox", emoji: "🦊" }, { word: "log", emoji: "🪵" }, { word: "mop", emoji: "🧹" }, 
            { word: "hop", emoji: "🐇" }, { word: "cot", emoji: "🏕️" },
            
            // Short U (10 words)
            { word: "sun", emoji: "☀️" }, { word: "cup", emoji: "🍵" }, { word: "bug", emoji: "🐛" }, 
            { word: "run", emoji: "🏃" }, { word: "nut", emoji: "🥜" },
            { word: "bus", emoji: "🚌" }, { word: "tub", emoji: "🛁" }, { word: "mud", emoji: "🤎" }, 
            { word: "rug", emoji: "🧶" }, { word: "gum", emoji: "🍬" }
        ];

        // Pastel Colors for Bubbles and Buttons
        const CUTE_COLORS = [
            '#FFB7B2', '#FFDAC1', '#E2F0CB', '#B5EAD7', '#C7CEEA', '#FDFD96'
        ];

        const BUTTON_COLORS = [
            { bg: 'bg-blue-400', border: 'border-blue-600', hover: 'hover:bg-blue-300' },
            { bg: 'bg-pink-400', border: 'border-pink-600', hover: 'hover:bg-pink-300' },
            { bg: 'bg-purple-400', border: 'border-purple-600', hover: 'hover:bg-purple-300' },
            { bg: 'bg-yellow-400', border: 'border-yellow-600', hover: 'hover:bg-yellow-300' },
            { bg: 'bg-orange-400', border: 'border-orange-600', hover: 'hover:bg-orange-300' }
        ];

        /**
         * GAME ENGINE
         */
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        
        let gameState = 'START'; // START, PLAYING, MODAL, GAMEOVER
        let score = 0;
        let lives = 3;
        
        let particles = []; // Background magical dust
        let confettis = []; // Confetti for correct answers
        let bubbles = [];   // The CVC bubbles
        let portalAngle = 0;
        
        let spawnRate = 220; // Slightly slower for kids
        let spawnTimer = 0;
        let gameFrame = 0;
        let activeBubble = null;
        
        let width, height, centerX, centerY;

        function resize() {
            width = canvas.width = window.innerWidth;
            height = canvas.height = window.innerHeight;
            centerX = width / 2;
            centerY = height / 2;
        }

        window.addEventListener('resize', resize);
        resize();

        // --- CLASSES ---

        class Sparkle {
            constructor() {
                this.reset();
                // Randomize initial position so screen isn't empty on load
                this.x = Math.random() * width;
                this.y = Math.random() * height;
            }

            reset() {
                this.x = Math.random() * width;
                this.y = height + Math.random() * 100; // Start below screen
                this.size = Math.random() * 4 + 2;
                this.speedY = Math.random() * -1 - 0.5; // Float up
                this.speedX = (Math.random() - 0.5) * 0.5;
                this.opacity = Math.random() * 0.5 + 0.2;
                this.wobble = Math.random() * Math.PI * 2;
            }

            update() {
                this.y += this.speedY;
                this.x += this.speedX + Math.sin(this.wobble) * 0.5;
                this.wobble += 0.05;

                if (this.y < -10) {
                    this.reset();
                }
            }

            draw() {
                ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                ctx.fill();
            }
        }

        class Confetti {
            constructor(x, y) {
                this.x = x;
                this.y = y;
                this.size = Math.random() * 10 + 5;
                this.color = CUTE_COLORS[Math.floor(Math.random() * CUTE_COLORS.length)];
                
                const angle = Math.random() * Math.PI * 2;
                const velocity = Math.random() * 15 + 5;
                this.vx = Math.cos(angle) * velocity;
                this.vy = Math.sin(angle) * velocity - 5; // Extra upward pop
                
                this.gravity = 0.5;
                this.drag = 0.95;
                this.rotation = Math.random() * 360;
                this.rotationSpeed = (Math.random() - 0.5) * 20;
                this.life = 1.0;
                this.decay = Math.random() * 0.02 + 0.01;
            }

            update() {
                this.vx *= this.drag;
                this.vy *= this.drag;
                this.vy += this.gravity;
                this.x += this.vx;
                this.y += this.vy;
                this.rotation += this.rotationSpeed;
                this.life -= this.decay;
            }

            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation * Math.PI / 180);
                ctx.globalAlpha = Math.max(0, this.life);
                ctx.fillStyle = this.color;
                ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
                ctx.restore();
            }
        }

        class Bubble {
            constructor() {
                // Spawn off-screen
                const edge = Math.floor(Math.random() * 4);
                if (edge === 0) { this.x = Math.random() * width; this.y = -60; } // Top
                else if (edge === 1) { this.x = width + 60; this.y = Math.random() * height; } // Right
                else if (edge === 2) { this.x = Math.random() * width; this.y = height + 60; } // Bottom
                else { this.x = -60; this.y = Math.random() * height; } // Left

                this.speed = 0.3 + (score * 0.01); // Very slow start for kids
                if (this.speed > 1.5) this.speed = 1.5; // Cap speed lower

                this.radius = 45; // Big friendly bubbles
                this.wobble = Math.random() * Math.PI * 2;
                
                // Pick a random CVC word out of the 50
                this.cvcData = CVC_DATA[Math.floor(Math.random() * CVC_DATA.length)];
                this.color = CUTE_COLORS[Math.floor(Math.random() * CUTE_COLORS.length)];
            }

            update() {
                if (gameState !== 'PLAYING') return false;

                // Move towards center portal
                const dx = centerX - this.x;
                const dy = centerY - this.y;
                const dist = Math.sqrt(dx*dx + dy*dy);

                this.x += (dx / dist) * this.speed;
                this.y += (dy / dist) * this.speed;
                
                // Add playful floating wobble
                this.y += Math.sin(this.wobble) * 0.5;
                this.wobble += 0.05;

                // Collision with portal (Missed it!)
                if (dist < 50) {
                    loseLife();
                    return true; // Mark for deletion
                }
                return false;
            }

            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                
                // Bubble Body
                ctx.beginPath();
                ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
                
                // Gradient for 3D bubble effect
                const grad = ctx.createRadialGradient(-10, -10, 5, 0, 0, this.radius);
                grad.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
                grad.addColorStop(0.3, this.color);
                grad.addColorStop(1, 'rgba(255, 255, 255, 0.4)');
                
                ctx.fillStyle = grad;
                ctx.fill();
                
                ctx.lineWidth = 3;
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.stroke();

                // Bubble Shine
                ctx.beginPath();
                ctx.ellipse(-15, -15, 10, 5, Math.PI / 4, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.fill();

                // Emoji
                ctx.font = '40px Arial';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                // Slight shadow for emoji
                ctx.shadowColor = 'rgba(0,0,0,0.2)';
                ctx.shadowBlur = 5;
                ctx.shadowOffsetX = 2;
                ctx.shadowOffsetY = 2;
                ctx.fillText(this.cvcData.emoji, 0, 5); // offset y slightly for visual center

                ctx.restore();
            }
        }

        // --- GAME LOGIC ---

        function init() {
            // Create background sparkles
            for (let i = 0; i < 50; i++) {
                particles.push(new Sparkle());
            }
            updateLivesUI();
            animate();
        }

        function startGame() {
            document.getElementById('startScreen').classList.add('hidden');
            document.getElementById('gameOverScreen').classList.add('hidden');
            resetVars();
            gameState = 'PLAYING';
        }

        function resetVars() {
            score = 0;
            lives = 3;
            spawnRate = 220;
            bubbles = [];
            confettis = [];
            activeBubble = null;
            document.getElementById('scoreDisplay').innerText = score;
            updateLivesUI();
        }

        function loseLife() {
            lives--;
            updateLivesUI();
            
            // Screen Shake Effect
            document.body.classList.add('shake');
            setTimeout(() => document.body.classList.remove('shake'), 500);

            // Pop sound fallback (visual)
            ctx.fillStyle = 'rgba(255, 100, 100, 0.3)';
            ctx.fillRect(0, 0, width, height);

            if (lives <= 0) {
                gameOver();
            }
        }

        function updateLivesUI() {
            const container = document.getElementById('livesContainer');
            container.innerHTML = '';
            for(let i=0; i<3; i++) {
                const heart = document.createElement('div');
                heart.className = `text-3xl transition-all duration-300 ${i < lives ? 'scale-100 opacity-100' : 'scale-75 opacity-30 grayscale'}`;
                heart.innerText = '❤️';
                container.appendChild(heart);
            }
        }

        function gameOver() {
            gameState = 'GAMEOVER';
            document.getElementById('finalScore').innerText = score;
            document.getElementById('gameOverScreen').classList.remove('hidden');
            document.getElementById('questionModal').classList.add('hidden');
        }

        function resetGame() {
            startGame();
        }

        // --- INTERACTION ---

        function handleInput(x, y) {
            if (gameState !== 'PLAYING') return;

            // Check if clicked on a bubble
            for (let i = bubbles.length - 1; i >= 0; i--) { // Check newest top-most first
                const b = bubbles[i];
                const dx = x - b.x;
                const dy = y - b.y;
                const dist = Math.sqrt(dx*dx + dy*dy);

                if (dist < b.radius + 15) { // +15 forgiveness for chubby little fingers
                    activateRescue(b, i);
                    break;
                }
            }
        }

        canvas.addEventListener('mousedown', (e) => handleInput(e.clientX, e.clientY));
        canvas.addEventListener('touchstart', (e) => {
            handleInput(e.touches[0].clientX, e.touches[0].clientY);
        }, {passive: false});

        function activateRescue(bubble, index) {
            gameState = 'MODAL'; // Pauses the game loop updates
            activeBubble = { data: bubble, index: index };
            
            const modal = document.getElementById('questionModal');
            const qEmoji = document.getElementById('questionEmoji');
            const grid = document.getElementById('optionsGrid');

            // Set Emoji
            qEmoji.innerText = bubble.cvcData.emoji;

            // Generate Options
            grid.innerHTML = '';
            const correctWord = bubble.cvcData.word;
            let options = [correctWord];
            
            // Get 2 random wrong answers for 3 total options (from the giant pool of 50!)
            const pool = CVC_DATA.filter(opt => opt.word !== correctWord);
            while (options.length < 3) {
                const randomOpt = pool[Math.floor(Math.random() * pool.length)].word;
                if (!options.includes(randomOpt)) options.push(randomOpt);
            }

            // Shuffle
            options.sort(() => Math.random() - 0.5);

            // Create big cute buttons
            options.forEach((opt, idx) => {
                const btn = document.createElement('button');
                const btnTheme = BUTTON_COLORS[idx % BUTTON_COLORS.length];
                
                btn.className = `cute-button w-full ${btnTheme.bg} ${btnTheme.border} ${btnTheme.hover} text-white font-bold py-4 px-6 rounded-2xl text-3xl tracking-wide text-center uppercase shadow-md`;
                btn.innerText = opt;
                btn.onclick = () => checkAnswer(opt, correctWord);
                grid.appendChild(btn);
            });

            // Trigger bounce animation
            modal.classList.remove('hidden');
            modal.classList.remove('modal-animate');
            void modal.offsetWidth; // Trigger reflow
            modal.classList.add('modal-animate');
        }

        function checkAnswer(selected, correct) {
            const modal = document.getElementById('questionModal');
            
            if (selected === correct) {
                // YAY! CORRECT!
                score += 1; // 1 star per correct
                document.getElementById('scoreDisplay').innerText = score;
                
                if (activeBubble) {
                    // Confetti explosion at bubble location
                    spawnConfetti(activeBubble.data.x, activeBubble.data.y);
                    // Remove bubble
                    bubbles.splice(activeBubble.index, 1);
                }

                // Slightly increase difficulty over time
                if (spawnRate > 80 && score % 3 === 0) spawnRate -= 10;
            } else {
                // OOPS! WRONG!
                // Push bubble closer to the center portal as a slight penalty
                if (activeBubble && bubbles[activeBubble.index]) {
                    const b = bubbles[activeBubble.index];
                    const dx = centerX - b.x;
                    const dy = centerY - b.y;
                    b.x += dx * 0.4; 
                    b.y += dy * 0.4;
                    
                    // Show a little red flash
                    ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
                    ctx.fillRect(0, 0, width, height);
                }
            }

            modal.classList.add('hidden');
            gameState = 'PLAYING';
            activeBubble = null;
        }

        function spawnConfetti(x, y) {
            for(let i=0; i<40; i++) {
                confettis.push(new Confetti(x, y));
            }
        }

        function drawPortal() {
            portalAngle += 0.02;
            ctx.save();
            ctx.translate(centerX, centerY);
            ctx.rotate(portalAngle);

            // Draw a spinning rainbow star/portal
            const numSpokes = 8;
            for(let i=0; i<numSpokes; i++) {
                ctx.rotate((Math.PI * 2) / numSpokes);
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(30, 80);
                ctx.lineTo(-30, 80);
                ctx.fillStyle = `hsla(${(i * 45 + gameFrame) % 360}, 80%, 70%, 0.6)`;
                ctx.fill();
            }

            // Center glowing circle
            ctx.beginPath();
            ctx.arc(0, 0, 40, 0, Math.PI * 2);
            ctx.fillStyle = '#fff';
            ctx.shadowColor = '#FFB7B2';
            ctx.shadowBlur = 20;
            ctx.fill();
            
            ctx.restore();
        }

        // --- MAIN LOOP ---

        function animate() {
            // Soft sky blue background wipe
            ctx.fillStyle = '#87CEEB';
            ctx.fillRect(0, 0, width, height);

            // Draw Background Sparkles
            particles.forEach(p => {
                p.update();
                p.draw();
            });

            // Draw Rainbow Portal at center
            drawPortal();

            // Draw and Update Confetti
            for (let i = confettis.length - 1; i >= 0; i--) {
                confettis[i].update();
                confettis[i].draw();
                if (confettis[i].life <= 0 || confettis[i].y > height + 50) {
                    confettis.splice(i, 1);
                }
            }

            // Handle Bubbles
            if (gameState === 'PLAYING') {
                gameFrame++;
                
                // Spawn Logic
                if (gameFrame % spawnRate === 0) {
                    bubbles.push(new Bubble());
                }

                // Update & Draw Bubbles
                for (let i = bubbles.length - 1; i >= 0; i--) {
                    let shouldRemove = bubbles[i].update();
                    if (shouldRemove) {
                        bubbles.splice(i, 1);
                        // Edge case safeguard
                        if (activeBubble && activeBubble.index === i) {
                            document.getElementById('questionModal').classList.add('hidden');
                            activeBubble = null;
                        }
                    } else {
                        bubbles[i].draw();
                    }
                }
            } else if (gameState === 'MODAL') {
                // Just draw them frozen if modal is open
                bubbles.forEach(b => b.draw());
            }

            requestAnimationFrame(animate);
        }

        // Boot
        init();

    </script>
</body>
</html>