Javascript提高:点击飘忽不定的气球,气球爆炸并增加分数-由Deepseek产生

以下是您所需的"飘忽气球"游戏的完整HTML代码。您需要点击画布中飘忽不定的气球,击中后气球会爆炸并增加分数。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>飘忽气球大冒险 | 点击爆炸得分</title>
    <style>
        * {
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }

        body {
            background: linear-gradient(145deg, #1a4d8c 0%, #0e2a4a 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Poppins', 'Fredoka One', 'Comic Neue', system-ui, sans-serif;
            margin: 0;
            padding: 20px;
        }

        .game-container {
            background: rgba(0, 0, 0, 0.3);
            border-radius: 64px;
            padding: 20px;
            box-shadow: 0 20px 35px rgba(0, 0, 0, 0.4), inset 0 1px 4px rgba(255, 255, 255, 0.2);
        }

        canvas {
            display: block;
            margin: 0 auto;
            border-radius: 40px;
            box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5), inset 0 0 0 2px rgba(255, 255, 255, 0.2);
            cursor: crosshair;
            background: radial-gradient(circle at 20% 30%, #87CEEB, #2a6f9c);
            transition: transform 0.1s ease;
        }

        canvas:active {
            transform: scale(0.99);
        }

        .info-panel {
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            margin-top: 20px;
            margin-bottom: 10px;
            background: #2e2c2ad9;
            backdrop-filter: blur(8px);
            border-radius: 100px;
            padding: 8px 20px;
            gap: 25px;
            box-shadow: 0 5px 12px rgba(0, 0, 0, 0.3);
        }

        .score-box {
            background: #f7e05e;
            padding: 8px 20px;
            border-radius: 60px;
            color: #3b2a1f;
            font-weight: bold;
            font-size: 2rem;
            min-width: 120px;
            text-align: center;
            letter-spacing: 2px;
            box-shadow: inset 0 -2px 0 #b47c2e, 0 5px 12px rgba(0,0,0,0.2);
            font-family: monospace;
        }

        .score-label {
            font-size: 1.2rem;
            background: #261f1a;
            padding: 5px 18px;
            border-radius: 40px;
            color: #ffdd88;
            font-weight: bold;
            letter-spacing: 1px;
        }

        .reset-btn {
            background: #ff8c42;
            border: none;
            font-size: 1.2rem;
            font-weight: bold;
            padding: 8px 25px;
            border-radius: 60px;
            cursor: pointer;
            font-family: inherit;
            color: #2d1b0c;
            transition: 0.2s;
            box-shadow: 0 4px 0 #9b4a1a;
            transform: translateY(-2px);
        }

        .reset-btn:active {
            transform: translateY(3px);
            box-shadow: 0 1px 0 #9b4a1a;
        }

        .tips {
            color: #f9e0a0;
            text-shadow: 0 1px 2px black;
            font-weight: 600;
            font-size: 0.9rem;
            background: #00000066;
            padding: 6px 16px;
            border-radius: 30px;
            backdrop-filter: blur(4px);
        }

        footer {
            margin-top: 18px;
            text-align: center;
            font-size: 0.75rem;
            color: #cfe6ff;
        }

        @media (max-width: 860px) {
            .game-container {
                padding: 10px;
            }
            .score-box {
                font-size: 1.6rem;
                min-width: 90px;
                padding: 4px 15px;
            }
            .score-label {
                font-size: 1rem;
            }
        }
    </style>
</head>
<body>
<div>
    <div class="game-container">
        <canvas id="balloonCanvas" width="900" height="550" style="width:100%; height:auto; max-width:900px; aspect-ratio:900/550"></canvas>
        <div class="info-panel">
            <div class="score-label">🎈 命中数 🎈</div>
            <div class="score-box" id="scoreDisplay">0</div>
            <button class="reset-btn" id="resetGameBtn">✨ 新的一局 ✨</button>
            <div class="tips">💥 点击气球 → 爆炸 +1分</div>
        </div>
        <footer>⚡ 气球飘忽不定,越点越刺激!爆炸后新气球重生~ ⚡</footer>
    </div>
</div>

<script>
    (function(){
        // ----- 画布与上下文 -----
        const canvas = document.getElementById('balloonCanvas');
        const ctx = canvas.getContext('2d');
        
        // ----- 游戏配置参数 -----
        const WIDTH = 900;
        const HEIGHT = 550;
        canvas.width = WIDTH;
        canvas.height = HEIGHT;
        
        // 气球物理参数
        const BALLOON_RADIUS = 23;          // 半径(px)
        const MAX_SPEED = 3.5;              // 最大飘移速度
        const WANDER_FORCE = 0.22;          // 每帧随机加速度幅度 (飘忽不定核心)
        const SPEED_DAMP = 0.995;            // 轻微阻尼,让运动更自然但不会停下来
        
        // 气球数量 (总个数保持不变,击中爆炸后移除并重生,游戏始终维持这个数量)
        const BALLOON_COUNT = 6;
        
        // ----- 全局数据 -----
        let balloons = [];          // 存储气球对象 {x, y, vx, vy, color, radius}
        let explosions = [];        // 爆炸特效 {x, y, life, maxLife}
        let score = 0;              // 击中计数器
        
        // DOM 元素
        const scoreSpan = document.getElementById('scoreDisplay');
        
        // ----- 辅助函数 -----
        function randomRange(min, max) {
            return min + Math.random() * (max - min);
        }
        
        // 生成明亮的鲜艳气球颜色 (看起来欢乐)
        function randomBalloonColor() {
            const hue = Math.random() * 360;
            // 饱和度 65% ~ 90%, 亮度 65% ~ 85% 鲜艳但柔和
            return `hsl(${hue}, 75%, 70%)`;
        }
        
        // 确保新产生气球的位置完全在画布内部(考虑半径)
        function getValidRandomPosition() {
            const margin = BALLOON_RADIUS + 2;
            return {
                x: randomRange(margin, WIDTH - margin),
                y: randomRange(margin, HEIGHT - margin)
            };
        }
        
        // 生成随机速度(飘忽基础速度)
        function getRandomVelocity() {
            return {
                vx: randomRange(-1.8, 1.8),
                vy: randomRange(-1.8, 1.8)
            };
        }
        
        // 创建一个全新的气球对象 (位置合法,随机速度,随机颜色)
        function createBalloon() {
            const { x, y } = getValidRandomPosition();
            const { vx, vy } = getRandomVelocity();
            return {
                x: x,
                y: y,
                vx: vx,
                vy: vy,
                radius: BALLOON_RADIUS,
                color: randomBalloonColor()
            };
        }
        
        // 初始化所有气球 (或者重置游戏时使用)
        function initBalloons() {
            const newBalloons = [];
            for (let i = 0; i < BALLOON_COUNT; i++) {
                newBalloons.push(createBalloon());
            }
            return newBalloons;
        }
        
        // 添加一个新的气球 (替换被击爆的气球),保证总数不变,位置不会瞬间与其他严重重叠也可以,重叠不影响核心玩法
        function addOneBalloon() {
            // 直接返回新气球对象
            return createBalloon();
        }
        
        // ----- 飘忽不定核心更新:每帧随机改变速度 + 边界反弹 + 限速 -----
        function updateBalloonMovement(balloon) {
            // 1. 飘忽不定效果: 随机加减速 (模拟空气乱流)
            balloon.vx += (Math.random() - 0.5) * WANDER_FORCE;
            balloon.vy += (Math.random() - 0.5) * WANDER_FORCE;
            
            // 2. 微小阻尼,避免无限加速,但依然保持飘忽感
            balloon.vx *= SPEED_DAMP;
            balloon.vy *= SPEED_DAMP;
            
            // 3. 速度限幅 (保持可控范围,不会过快飞出视线)
            let speedX = Math.min(MAX_SPEED, Math.max(-MAX_SPEED, balloon.vx));
            let speedY = Math.min(MAX_SPEED, Math.max(-MAX_SPEED, balloon.vy));
            balloon.vx = speedX;
            balloon.vy = speedY;
            
            // 4. 更新位置
            balloon.x += balloon.vx;
            balloon.y += balloon.vy;
            
            // 5. 边界碰撞 (带弹性+随机扰动模拟飘忽反弹)
            const r = balloon.radius;
            // 左边界 & 右边界
            if (balloon.x - r < 0) {
                balloon.x = r;
                balloon.vx = -balloon.vx * 0.95;
                // 额外加一点随机偏移,让反弹更"飘忽"
                balloon.vx += (Math.random() - 0.5) * 0.6;
            }
            if (balloon.x + r > WIDTH) {
                balloon.x = WIDTH - r;
                balloon.vx = -balloon.vx * 0.95;
                balloon.vx += (Math.random() - 0.5) * 0.6;
            }
            // 上边界 & 下边界
            if (balloon.y - r < 0) {
                balloon.y = r;
                balloon.vy = -balloon.vy * 0.95;
                balloon.vy += (Math.random() - 0.5) * 0.6;
            }
            if (balloon.y + r > HEIGHT) {
                balloon.y = HEIGHT - r;
                balloon.vy = -balloon.vy * 0.95;
                balloon.vy += (Math.random() - 0.5) * 0.6;
            }
            
            // 最终二次限速(防止反弹加成过快)
            balloon.vx = Math.min(MAX_SPEED, Math.max(-MAX_SPEED, balloon.vx));
            balloon.vy = Math.min(MAX_SPEED, Math.max(-MAX_SPEED, balloon.vy));
            
            // 边界安全钳: 避免数值误差导致越界
            balloon.x = Math.min(Math.max(balloon.x, r), WIDTH - r);
            balloon.y = Math.min(Math.max(balloon.y, r), HEIGHT - r);
        }
        
        // ----- 绘制一个精致的气球 (带高光和小尾巴) -----
        function drawBalloon(b) {
            ctx.save();
            ctx.shadowBlur = 0;
            // 绘制气球主体 (圆形)
            ctx.beginPath();
            ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
            // 填充主体颜色
            ctx.fillStyle = b.color;
            ctx.fill();
            // 描边轮廓使得更加立体
            ctx.strokeStyle = "rgba(0,0,0,0.2)";
            ctx.lineWidth = 1.2;
            ctx.stroke();
            
            // 高光 (小椭圆形,增加立体感)
            ctx.beginPath();
            ctx.ellipse(b.x - 6, b.y - 7, 5, 7, 0, 0, Math.PI * 2);
            ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
            ctx.fill();
            
            // 第二高光 (更亮的小点)
            ctx.beginPath();
            ctx.arc(b.x - 9, b.y - 10, 2.2, 0, Math.PI * 2);
            ctx.fillStyle = "rgba(255, 250, 210, 0.85)";
            ctx.fill();
            
            // 气球嘴/绳子 (简单趣味)
            ctx.beginPath();
            ctx.moveTo(b.x - 3, b.y + b.radius - 2);
            ctx.lineTo(b.x, b.y + b.radius + 7);
            ctx.lineTo(b.x + 3, b.y + b.radius - 2);
            ctx.fillStyle = "#b87c4f";
            ctx.fill();
            // 绳子短线
            ctx.beginPath();
            ctx.moveTo(b.x, b.y + b.radius + 6);
            ctx.lineTo(b.x, b.y + b.radius + 14);
            ctx.lineWidth = 1.8;
            ctx.strokeStyle = "#6b3f1c";
            ctx.stroke();
            
            // 加上表情?增加萌点: 简单两点小眼睛
            ctx.fillStyle = "#2F2E2E";
            ctx.beginPath();
            ctx.arc(b.x - 7, b.y - 5, 2, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(b.x + 7, b.y - 5, 2, 0, Math.PI*2);
            ctx.fill();
            ctx.fillStyle = "white";
            ctx.beginPath();
            ctx.arc(b.x - 8, b.y - 6, 0.8, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(b.x + 6, b.y - 6, 0.8, 0, Math.PI*2);
            ctx.fill();
            // 微笑
            ctx.beginPath();
            ctx.arc(b.x, b.y - 1, 8, 0.05, Math.PI - 0.05);
            ctx.strokeStyle = "#493421";
            ctx.lineWidth = 1.5;
            ctx.stroke();
            
            ctx.restore();
        }
        
        // ----- 爆炸特效绘制(动态扩散)-----
        function drawExplosions() {
            for (let i = 0; i < explosions.length; i++) {
                const exp = explosions[i];
                const lifeFactor = exp.life / exp.maxLife;  // 从1 -> 0
                const radius = 12 + (1 - lifeFactor) * 18;   // 爆炸逐渐扩大消散
                const alpha = lifeFactor * 0.9;
                
                ctx.save();
                ctx.globalAlpha = alpha;
                // 核心火焰颜色渐变
                const gradient = ctx.createRadialGradient(exp.x, exp.y, 3, exp.x, exp.y, radius);
                gradient.addColorStop(0, '#ff5e00');
                gradient.addColorStop(0.6, '#ffbb33');
                gradient.addColorStop(1, '#ffdd88');
                ctx.beginPath();
                ctx.arc(exp.x, exp.y, radius, 0, Math.PI * 2);
                ctx.fillStyle = gradient;
                ctx.fill();
                // 火星粒子效果
                for (let s = 0; s < 6; s++) {
                    const angle = (s * 60 + exp.life * 15) * Math.PI / 180;
                    const offX = Math.cos(angle) * (radius * 0.7);
                    const offY = Math.sin(angle) * (radius * 0.5);
                    ctx.beginPath();
                    ctx.arc(exp.x + offX, exp.y + offY, radius * 0.22, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255, 100, 0, ${alpha * 0.8})`;
                    ctx.fill();
                }
                ctx.restore();
            }
        }
        
        // 更新爆炸特效应 (减少生命值)
        function updateExplosions() {
            for (let i = explosions.length-1; i >= 0; i--) {
                explosions[i].life -= 1;
                if (explosions[i].life <= 0) {
                    explosions.splice(i,1);
                }
            }
        }
        
        // 添加爆炸特效 (击中气球的位置)
        function addExplosion(x, y) {
            explosions.push({
                x: x,
                y: y,
                life: 12,       // 持续12帧 (~0.2秒)
                maxLife: 12
            });
        }
        
        // 更新UI分数显示
        function updateScoreUI() {
            scoreSpan.innerText = score;
            // 简单的微弹动效果 (css动画无依赖)
            scoreSpan.style.transform = 'scale(1.1)';
            setTimeout(() => { if(scoreSpan) scoreSpan.style.transform = ''; }, 120);
        }
        
        // 处理点击命中 (核心游戏逻辑)
        function handleHit(mouseX, mouseY) {
            // 从后往前遍历,命中第一个就处理 (因为可能会修改数组)
            // 但是要避免同时命中多个气球导致多次加分,一次点击只处理一次击中(最上层感觉)
            for (let i = 0; i < balloons.length; i++) {
                const b = balloons[i];
                const dx = mouseX - b.x;
                const dy = mouseY - b.y;
                const dist = Math.sqrt(dx*dx + dy*dy);
                if (dist <= b.radius) {
                    // 击中气球!加分、爆炸、移除该气球、再生一个新气球
                    score++;
                    updateScoreUI();
                    
                    // 记录爆炸位置 (气球中心)
                    addExplosion(b.x, b.y);
                    
                    // 移除当前气球
                    balloons.splice(i, 1);
                    // 新增一个气球(保持总数不变)
                    const newBalloon = addOneBalloon();
                    balloons.push(newBalloon);
                    
                    // 由于已经处理一次命中,不再继续遍历 (一次点击只引爆一个气球)
                    return true;
                }
            }
            return false;
        }
        
        // ----- 重置整个游戏 -----
        function resetGame() {
            score = 0;
            updateScoreUI();
            // 清空爆炸特效
            explosions = [];
            // 重置所有气球 (全新随机)
            balloons = initBalloons();
            // 可选: 给个重置小特效提示
        }
        
        // ----- 监听canvas上的鼠标点击 / 移动端触摸转换 -----
        function setupCanvasEvents() {
            // 获取相对canvas坐标的方法
            function getCanvasCoords(e) {
                const rect = canvas.getBoundingClientRect();
                const scaleX = canvas.width / rect.width;   // canvas实际像素比
                const scaleY = canvas.height / rect.height;
                let clientX, clientY;
                
                if (e.touches) {
                    // 触摸事件
                    if (e.touches.length === 0) return null;
                    clientX = e.touches[0].clientX;
                    clientY = e.touches[0].clientY;
                    e.preventDefault();   // 避免滚动
                } else {
                    clientX = e.clientX;
                    clientY = e.clientY;
                }
                
                let canvasX = (clientX - rect.left) * scaleX;
                let canvasY = (clientY - rect.top) * scaleY;
                canvasX = Math.min(Math.max(0, canvasX), WIDTH);
                canvasY = Math.min(Math.max(0, canvasY), HEIGHT);
                return { x: canvasX, y: canvasY };
            }
            
            function onClickOrTap(e) {
                // 阻止冒泡避免奇怪行为,但不影响页面
                e.stopPropagation?.();
                const coords = getCanvasCoords(e);
                if (coords) {
                    handleHit(coords.x, coords.y);
                }
            }
            
            // 鼠标事件
            canvas.addEventListener('click', onClickOrTap);
            // 移动端触摸支持
            canvas.addEventListener('touchstart', (e) => {
                e.preventDefault();
                const coords = getCanvasCoords(e);
                if (coords) {
                    handleHit(coords.x, coords.y);
                }
            }, { passive: false });
        }
        
        // ----- 渲染循环 (绘制所有元素) -----
        function drawBackground() {
            // 绘制蓝天草地效果
            const grad = ctx.createLinearGradient(0, 0, 0, HEIGHT);
            grad.addColorStop(0, '#8bc6ec');
            grad.addColorStop(0.65, '#4b9bd6');
            grad.addColorStop(1, '#2c6d9e');
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, WIDTH, HEIGHT);
            // 云朵点缀
            ctx.fillStyle = "rgba(255,255,240,0.75)";
            ctx.shadowBlur = 0;
            ctx.beginPath();
            ctx.ellipse(120, 70, 45, 35, 0, 0, Math.PI*2);
            ctx.ellipse(170, 55, 50, 40, 0, 0, Math.PI*2);
            ctx.ellipse(210, 75, 40, 35, 0, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.ellipse(730, 100, 55, 40, 0, 0, Math.PI*2);
            ctx.ellipse(780, 80, 45, 38, 0, 0, Math.PI*2);
            ctx.ellipse(680, 95, 40, 32, 0, 0, Math.PI*2);
            ctx.fill();
            ctx.fillStyle = "rgba(255,255,210,0.6)";
            ctx.beginPath();
            ctx.ellipse(500, 45, 60, 35, 0, 0, Math.PI*2);
            ctx.fill();
            // 草地
            ctx.fillStyle = '#699e5c';
            ctx.shadowBlur = 0;
            ctx.fillRect(0, HEIGHT-48, WIDTH, 55);
            ctx.fillStyle = '#578b46';
            for(let i=0;i<20;i++) {
                ctx.beginPath();
                ctx.moveTo(i*55, HEIGHT-48);
                ctx.lineTo(i*55+30, HEIGHT-65);
                ctx.lineTo(i*55-30, HEIGHT-65);
                ctx.fill();
            }
        }
        
        // 画所有气球 + 更新它们的位置
        function updateAndDrawBalloons() {
            // 先更新物理 (每一帧所有气球飘移)
            for (let i = 0; i < balloons.length; i++) {
                updateBalloonMovement(balloons[i]);
            }
            // 再绘制
            for (let i = 0; i < balloons.length; i++) {
                drawBalloon(balloons[i]);
            }
        }
        
        // 主渲染动画循环 (requestAnimationFrame)
        function animate() {
            // 1. 更新爆炸特效的生命周期
            updateExplosions();
            
            // 2. 绘制背景
            drawBackground();
            
            // 3. 绘制所有动态气球 (已经包含位置更新)
            updateAndDrawBalloons();
            
            // 4. 绘制爆炸特效 (在最上层)
            drawExplosions();
            
            // 额外显示动态小提示 (飘浮小字)
            ctx.font = "bold 16px 'Segoe UI', system-ui";
            ctx.fillStyle = "#FEF7CF";
            ctx.shadowBlur = 3;
            ctx.shadowColor = "black";
            ctx.fillText("💢 快戳破它们!", WIDTH-140, 40);
            ctx.font = "italic 13px monospace";
            ctx.fillStyle = "#FFFFDD";
            ctx.fillText("飘忽走位·爆炸得分", 18, 45);
            ctx.shadowBlur = 0;
            
            // 继续帧动画
            requestAnimationFrame(animate);
        }
        
        // ----- 游戏启动初始化 -----
        function initGame() {
            balloons = initBalloons();
            score = 0;
            explosions = [];
            updateScoreUI();
            setupCanvasEvents();
            animate();  // 开始动画循环
        }
        
        // 绑定重置按钮
        const resetBtn = document.getElementById('resetGameBtn');
        if (resetBtn) {
            resetBtn.addEventListener('click', () => {
                resetGame();
                // 额外效果:闪烁提示
                const originalText = resetBtn.innerText;
                resetBtn.innerText = '🎈 重置啦 🎈';
                setTimeout(() => {
                    resetBtn.innerText = originalText;
                }, 500);
            });
        }
        
        // 启动一切
        initGame();
    })();
</script>
</body>
</html>
相关推荐
昼猫2 小时前
前端打印分页技术探讨与 PrintomJs 方案
javascript·浏览器
费曼学习法2 小时前
React Hooks 闭包陷阱:为什么 useState 拿不到最新值?
javascript·react.js
ChalesXavier2 小时前
SSE(Server-Sent Events,服务器发送事件):从协议细节到流式处理实战
javascript
用户059540174462 小时前
Playwright 多标签页 IndexedDB 同步测试踩坑实录:折磨我6小时的浏览器沙箱陷阱
前端·css
非凡ghost2 小时前
视频下载神器:直播回放、视频链接一键抓取,还能自动监听!
java·前端·javascript·音视频
镜宇秋霖丶3 小时前
常驻大哥24分法,记得看
前端·javascript·vue.js
小赵同学WoW3 小时前
JS 核心之执行上下文详细解释
前端·javascript
心连欣3 小时前
跨越时代的对话:Vue 2 与 Vue 3 的终极对决与环境搭建指南
前端·javascript·vue.js
HYCS3 小时前
用pixijs实现fabricjs(一):FakeCanvasRenderingContext2D
javascript·webgl·canvas