DeepSeek生成的HTML5小游戏 -- 投篮小能手

投篮小游戏

🎮 核心玩法

  • 拖动投篮:在篮球上拖动鼠标/手指,反向拉长虚线控制力度和方向,松开后篮球飞出

  • 篮筐瞬移:每投进一球(+3分),篮筐会随机移动到新位置

  • 得分规则:篮球从篮筐上方下落穿过椭圆区域即得分

🌟 特色效果

  • 花雨粒子:进球时在篮筐位置绽放彩色粒子特效

  • 视觉反馈

    • 拖动时显示当前力度数值

    • 得分时显示浮动"+3"

    • 进球后出现"🏀 恭喜投篮命中!"消息框(渐变消失)

  • 篮板物理:篮球碰到篮板会反弹减速

🎵 音效与音乐

  • 背景音乐:循环播放,需首次点击页面后激活

  • 得分音效:每次进球播放"xiaochu.mp3"

  • 静音开关:可一键关闭所有声音

🕹️ 操作控制

  • 重来按钮:重置分数、篮球位置并随机篮筐位置

  • 自动复位:篮球静止后自动回到手中等待下次投掷

📱 移动端适配

  • 针对iPhone优化:禁止页面滚动/缩放,支持触摸拖动

  • 响应式设计:卡片式界面,Canvas自适应屏幕尺寸

  • 背景图片固定,半透明毛玻璃效果

🔧 技术特点

  • 纯HTML/CSS/JavaScript实现,无外部依赖

  • 基于Canvas的物理模拟(重力、碰撞、反弹)

  • 粒子系统实现花雨效果

  • 统一触摸/鼠标事件处理

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <title>投篮小游戏 · 花雨命中</title>
    <style>
        * {
            box-sizing: border-box;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            margin: 0;
            padding: 0;
        }
        body {
            min-height: 100vh;
            background-image: url('https://amitofoicu.github.io/home/lianchi6.jpg');
            background-size: cover;
            background-position: center;
            background-repeat: no-repeat;
            background-attachment: fixed;
            font-family: 'Segoe UI', Roboto, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 8px;
        }
        /* 最外层卡片,半透明背景让底图透出来 */
        .game-outer {
            background: rgba(0, 0, 0, 0.3);
            backdrop-filter: blur(5px);
            -webkit-backdrop-filter: blur(5px);
            border-radius: 3rem 3rem 2.5rem 2.5rem;
            padding: 1.2rem 1.2rem 1.8rem;
            border: 2px solid #e5cca5;
            box-shadow: 0 30px 40px rgba(0, 0, 0, 0.6);
            width: fit-content;
            max-width: 100%;
        }
        /* 头部区域:角标 + 标题 + 控制按钮集成 */
        .game-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 1rem;
            padding: 0 0.2rem;
            gap: 6px;
            flex-wrap: wrap;
            justify-content: center;
        }
        .header-left {
            display: flex;
            gap: 12px;
            align-items: center;
            flex-wrap: wrap;
            justify-content: center;
        }
        .more-games {
            background: rgba(20, 30, 35, 0.85);
            backdrop-filter: blur(4px);
            -webkit-backdrop-filter: blur(4px);
            padding: 0.4rem 1rem;
            border-radius: 40px;
            border: 2px solid #fad275;
            color: #ffefc0;
            font-weight: bold;
            font-size: 1.1rem;
            box-shadow: 0 4px 0 #6b4f2e;
            transition: 0.1s;
            white-space: nowrap;
        }
        .more-games a {
            color: #fff2c9;
            text-decoration: none;
            display: flex;
            align-items: center;
            gap: 5px;
        }
        .more-games a span {
            font-size: 1rem;
        }
        .game-title {
            background: rgba(25, 45, 55, 0.9);
            backdrop-filter: blur(4px);
            -webkit-backdrop-filter: blur(4px);
            padding: 0.4rem 1.2rem;
            border-radius: 50px;
            border: 2px solid #fbc96e;
            color: #ffebc0;
            font-size: 1.8rem;
            font-weight: 800;
            letter-spacing: 2px;
            text-shadow: 2px 2px 0 #1a4a5e;
            box-shadow: 0 4px 0 #6b4f2e;
            white-space: nowrap;
        }
        .header-right {
            display: flex;
            gap: 10px;
            align-items: center;
            flex-wrap: wrap;
            justify-content: center;
        }
        .mute-btn, .reset-btn {
            background: #f5c281;
            border: none;
            border-radius: 40px;
            padding: 0.4rem 1.2rem;
            font-size: 1.1rem;
            font-weight: bold;
            color: #2a4733;
            border-bottom: 4px solid #b27634;
            cursor: pointer;
            transition: all 0.07s;
            box-shadow: 0 4px 8px black;
            display: inline-flex;
            align-items: center;
            gap: 4px;
            -webkit-touch-callout: none;
        }
        .mute-btn {
            background: #5d7c8a;
            border-bottom-color: #2f4a55;
            color: #f0edd0;
        }
        .mute-btn:active, .reset-btn:active {
            transform: translateY(4px);
            border-bottom-width: 2px;
        }

        /* Canvas 容器:完全适配iPhone尺寸 */
        .canvas-wrapper {
            display: flex;
            justify-content: center;
            margin-bottom: 0.8rem;
            width: 100%;
        }
        canvas {
            display: block;
            background: radial-gradient(circle at 30% 20%, #b7e1f0, #7bb3cc);
            border-radius: 28px;
            box-shadow: inset 0 -6px 14px #3f6b7a, 0 12px 18px rgba(0,0,0,0.6);
            cursor: crosshair;
            border: 4px solid #f5eacb;
            width: 100%;
            height: auto;
            max-width: 900px;
            aspect-ratio: 900 / 520;
            touch-action: none;   /* 禁止iPhone默认滚动/缩放 */
        }

        /* 底部信息栏 */
        .panel {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.5rem 1rem;
            background: #ece2d0;
            border-radius: 50px;
            border: 3px solid #c29a6b;
            box-shadow: inset 0 2px 5px #baa077, 0 5px 0 #7d5535;
            color: #3d2b17;
            font-weight: bold;
            font-size: 1.2rem;
            gap: 10px;
        }
        .score-box {
            background: #1e2f3a;
            color: #f9e07c;
            padding: 0.2rem 1.2rem;
            border-radius: 40px;
            font-size: 2rem;
            letter-spacing: 2px;
            text-shadow: 0 2px 0 #0f1c24;
            box-shadow: inset 0 -3px 0 #4a6a78;
            border: 1px solid #cfb27c;
            font-family: 'Courier New', monospace;
            min-width: 85px;
            text-align: center;
        }
        .hint {
            display: flex;
            gap: 1rem;
            align-items: center;
            color: #2b3a3f;
            text-shadow: 1px 1px 0 #bba87e;
            font-size: 1rem;
            font-weight: 600;
            flex-wrap: wrap;
            justify-content: center;
        }
        .hint span {
            background: #e9dbc1;
            padding: 0.2rem 1rem;
            border-radius: 30px;
            border: 1px solid #b4925a;
            white-space: nowrap;
        }
        /* iPhone 小屏适配 */
        @media (max-width: 500px) {
            .game-title { font-size: 1.3rem; padding: 0.2rem 0.8rem; }
            .more-games { font-size: 0.9rem; padding: 0.2rem 0.8rem; }
            .mute-btn, .reset-btn { font-size: 0.9rem; padding: 0.2rem 0.8rem; }
            .hint { font-size: 0.8rem; gap: 0.5rem; }
            .score-box { font-size: 1.5rem; min-width: 65px; }
        }
    </style>
</head>
<body>
<div class="game-outer">
    <div class="game-header">
        <div class="header-left">
            <div class="more-games">
                <a href="https://amitofoicu.github.io/home/main.html" target="_blank" rel="noopener">🎮 <span>更多游戏</span></a>
            </div>
            <div class="game-title">🏀 投篮小游戏</div>
        </div>
        <div class="header-right">
            <button id="muteToggleBtn" class="mute-btn">🔊 静音</button>
            <button id="resetBtn" class="reset-btn">🔄 重来</button>
        </div>
    </div>

    <div class="canvas-wrapper">
        <canvas id="gameCanvas" width="900" height="520"></canvas>
    </div>

    <div class="panel">
        <div class="score-box" id="scoreDisplay">0</div>
        <div class="hint">
            <span>⬇️ 拖动投篮</span>
            <span>🎉 +3 花雨</span>
        </div>
        <div style="width:40px;"></div>
    </div>
</div>

<script>
    (function() {
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const scoreSpan = document.getElementById('scoreDisplay');

        const W = 900, H = 520;

        // ---------- 音频 ----------
        const backgroundMusic = new Audio('https://amitofoicu.github.io/home/beijing.ogg');
        backgroundMusic.loop = true;
        backgroundMusic.volume = 0.5;

        const scoreSound = new Audio('https://amitofoicu.github.io/home/xiaochu.mp3');
        scoreSound.volume = 0.8;

        let isMuted = false;
        let musicStarted = false;

        function toggleMute() {
            isMuted = !isMuted;
            const muteBtn = document.getElementById('muteToggleBtn');
            if (isMuted) {
                backgroundMusic.pause();
                muteBtn.innerHTML = '🔇 静音';
            } else {
                if (musicStarted) {
                    backgroundMusic.play().catch(e => console.log(e));
                }
                muteBtn.innerHTML = '🔊 静音';
            }
        }

        function tryPlayMusic() {
            if (!musicStarted && !isMuted) {
                backgroundMusic.play().then(() => {
                    musicStarted = true;
                }).catch(e => console.log('音乐等待交互', e));
            }
        }

        function playScoreSound() {
            if (isMuted) return;
            const soundClone = new Audio('https://amitofoicu.github.io/home/xiaochu.mp3');
            soundClone.volume = 0.7;
            soundClone.play().catch(e => console.log('音效播放失败', e));
        }

        // ---------- 篮筐 ----------
        let hoop = {
            x: 670, y: 210,
            rimRadiusX: 24,
            rimRadiusY: 7,
        };

        function getBackboardX() {
            return hoop.x + 42;
        }

        function randomizeHoopPosition() {
            const minX = 320, maxX = 820;
            const minY = 140, maxY = 320;
            hoop.x = Math.floor(Math.random() * (maxX - minX) + minX);
            hoop.y = Math.floor(Math.random() * (maxY - minY) + minY);
        }

        // 篮球
        let ball = {
            x: 140, y: 430,
            vx: 0, vy: 0,
            radius: 16,
        };

        let score = 0;
        let isDragging = false;
        let dragStart = { x: 0, y: 0 };
        let dragEnd = { x: 0, y: 0 };
        let isBallMoving = false;
        let inHoopLastFrame = false;

        // 粒子 & 得分浮层 & 新增:恭喜消息框
        let particles = [];
        let floatingScore = null;     
        let congratulationMessage = null; // { text, x, y, life }

        const GRAVITY = 0.26;
        const MIN_POWER = 3;

        function resetBallToHand() {
            ball.x = 140;
            ball.y = 430;
            ball.vx = 0;
            ball.vy = 0;
            isBallMoving = false;
            inHoopLastFrame = false;
        }

        function updateScoreDisplay() {
            scoreSpan.textContent = score;
        }

        function resetGame() {
            score = 0;
            updateScoreDisplay();
            resetBallToHand();
            dragStart = { x: 0, y: 0 };
            dragEnd = { x: 0, y: 0 };
            isDragging = false;
            particles = [];
            floatingScore = null;
            congratulationMessage = null; // 清除消息
            randomizeHoopPosition();
            tryPlayMusic();
        }

        // 花雨
        function createConfetti(explodeX, explodeY) {
            for (let i = 0; i < 40; i++) {
                const angle = Math.random() * Math.PI * 2;
                const speed = 4 + Math.random() * 9;
                const vx = Math.cos(angle) * speed * (0.6 + Math.random() * 0.8);
                const vy = Math.sin(angle) * speed * (0.6 + Math.random() * 0.8) - 2.8;
                particles.push({
                    x: explodeX, y: explodeY,
                    vx: vx, vy: vy,
                    size: 6 + Math.random() * 12,
                    color: `hsl(${Math.random() * 360}, 90%, 68%)`,
                    life: 1.0,
                    decay: 0.01 + Math.random() * 0.018,
                });
            }
        }

        function updateParticles() {
            for (let i = particles.length - 1; i >= 0; i--) {
                const p = particles[i];
                p.x += p.vx;
                p.y += p.vy;
                p.vy += 0.13;
                p.life -= p.decay;
                if (p.life <= 0 || p.y > H + 70) {
                    particles.splice(i, 1);
                }
            }
        }

        function drawParticles() {
            for (let p of particles) {
                ctx.globalAlpha = p.life * 0.9;
                ctx.fillStyle = p.color;
                ctx.beginPath();
                if (Math.random() > 0.4) {
                    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
                } else {
                    ctx.arc(p.x, p.y, p.size/2, 0, Math.PI*2);
                    ctx.fill();
                }
            }
            ctx.globalAlpha = 1.0;
        }

        // 物理更新
        function updateBall() {
            if (!isBallMoving) return;

            ball.vy += GRAVITY;
            ball.x += ball.vx;
            ball.y += ball.vy;

            // 边界
            if (ball.y + ball.radius > H) {
                ball.y = H - ball.radius;
                ball.vy *= -0.45;
                ball.vx *= 0.98;
                if (Math.abs(ball.vy) < 0.5 && Math.abs(ball.vx) < 0.3) resetBallToHand();
            }
            if (ball.y - ball.radius < 0) {
                ball.y = ball.radius;
                ball.vy *= -0.4;
            }
            if (ball.x - ball.radius < 0) {
                ball.x = ball.radius;
                ball.vx *= -0.5;
            }
            if (ball.x + ball.radius > W) {
                ball.x = W - ball.radius;
                ball.vx *= -0.5;
            }

            // 篮板碰撞
            const boardLeft = getBackboardX() - 22;
            const boardRight = getBackboardX() + 12;
            const boardTop = hoop.y - 90;
            const boardBottom = hoop.y + 90;
            if (ball.x + ball.radius > boardLeft && ball.x - ball.radius < boardRight &&
                ball.y + ball.radius > boardTop && ball.y - ball.radius < boardBottom) {
                if (ball.vx > 0 && ball.x < boardLeft) {
                    ball.x = boardLeft - ball.radius;
                    ball.vx *= -0.35;
                } else if (ball.vx < 0 && ball.x > boardRight) {
                    ball.x = boardRight + ball.radius;
                    ball.vx *= -0.35;
                } else {
                    if (ball.vy > 0 && ball.y < boardTop) {
                        ball.y = boardTop - ball.radius;
                        ball.vy *= -0.3;
                    } else if (ball.vy < 0 && ball.y > boardBottom) {
                        ball.y = boardBottom + ball.radius;
                        ball.vy *= -0.3;
                    } else {
                        ball.x = boardLeft - ball.radius;
                        ball.vx *= -0.3;
                    }
                }
            }

            // 得分检测 (椭圆)
            const dx = ball.x - hoop.x;
            const dy = ball.y - hoop.y;
            const rx = hoop.rimRadiusX + ball.radius * 0.7;
            const ry = hoop.rimRadiusY + ball.radius * 0.7;
            const isInEllipse = (dx*dx) / (rx*rx) + (dy*dy) / (ry*ry) <= 1.0;
            const isDescending = ball.vy > 0.8;

            if (isBallMoving && !inHoopLastFrame && isInEllipse && isDescending) {
                score += 3;
                updateScoreDisplay();

                createConfetti(hoop.x, hoop.y - 15);
                floatingScore = { value: 3, x: hoop.x, y: hoop.y - 45, life: 1.2 };

                // ===== 新增:恭喜投篮命中!消息框 =====
                congratulationMessage = {
                    text: '🏀 恭喜投篮命中!',
                    x: hoop.x,
                    y: hoop.y - 85,
                    life: 1.5      // 持续1.5秒左右
                };

                playScoreSound();

                randomizeHoopPosition();   // 进球后篮筐瞬移

                ball.vy += 0.2;
                inHoopLastFrame = true;
            }

            if (Math.hypot(ball.x - hoop.x, ball.y - hoop.y) > 60) {
                inHoopLastFrame = false;
            }

            if (Math.abs(ball.vy) < 0.3 && Math.abs(ball.vx) < 0.2 && ball.y > H - 40) {
                resetBallToHand();
            }
        }

        // 绘制得分浮层 + 恭喜消息框
        function drawFloatingElements() {
            // 得分浮层 (+3)
            if (floatingScore) {
                const fs = floatingScore;
                fs.life -= 0.01;
                if (fs.life <= 0) {
                    floatingScore = null;
                } else {
                    ctx.shadowBlur = 20;
                    ctx.shadowColor = '#ffaa00';
                    ctx.font = 'bold 48px "Courier New", monospace';
                    ctx.fillStyle = `rgba(255, 240, 100, ${fs.life})`;
                    ctx.fillText(`+${fs.value}`, fs.x - 50, fs.y - 10);
                    ctx.shadowBlur = 0;
                }
            }

            // 恭喜消息框 (渐变消失)
            if (congratulationMessage) {
                const msg = congratulationMessage;
                msg.life -= 0.012;  // 缓慢消失
                if (msg.life <= 0) {
                    congratulationMessage = null;
                } else {
                    ctx.shadowBlur = 25;
                    ctx.shadowColor = '#ffd966';
                    ctx.font = 'bold 38px "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif';
                    ctx.fillStyle = `rgba(255, 255, 180, ${msg.life})`;
                    ctx.strokeStyle = `rgba(200, 100, 20, ${msg.life*0.8})`;
                    ctx.lineWidth = 3;
                    ctx.textAlign = 'center';
                    // 背景框 (增加可读性)
                    const metrics = ctx.measureText(msg.text);
                    const textWidth = metrics.width;
                    const boxX = msg.x - textWidth/2 - 16;
                    const boxY = msg.y - 42;
                    ctx.fillStyle = `rgba(30, 30, 30, ${msg.life * 0.4})`;
            ctx.shadowBlur = 0;
                    ctx.fillRect(boxX, boxY, textWidth + 32, 52);
                    // 描边文字
                    ctx.shadowBlur = 20;
                    ctx.shadowColor = '#ffb347';
                    ctx.strokeText(msg.text, msg.x, msg.y);
                    ctx.fillStyle = `rgba(255, 245, 150, ${msg.life})`;
                    ctx.fillText(msg.text, msg.x, msg.y);
                    ctx.textAlign = 'left';
                    ctx.shadowBlur = 0;
                }
            }
        }

        // 绘制场景
        function drawScene() {
            ctx.clearRect(0, 0, W, H);

            // 地板
            ctx.fillStyle = '#b5906b';
            ctx.fillRect(0, H-45, W, 45);
            ctx.fillStyle = '#9c7a5a';
            for (let i=0;i<W;i+=60) ctx.fillRect(i, H-43, 30, 6);

            const boardX = getBackboardX();
            // 支架
            ctx.fillStyle = '#6b4f3a';
            ctx.fillRect(boardX-12, hoop.y+30, 22, 180);
            ctx.fillStyle = '#8b6f4f';
            ctx.fillRect(boardX-22, hoop.y+20, 44, 22);
            // 篮板
            ctx.fillStyle = '#d4b28c';
            ctx.shadowColor = '#3e2e1f';
            ctx.shadowBlur = 12;
            ctx.fillRect(boardX-22, hoop.y-95, 34, 190);
            ctx.shadowBlur = 0;
            ctx.strokeStyle = '#5b3f28';
            ctx.lineWidth = 3;
            ctx.strokeRect(boardX-22, hoop.y-95, 34, 190);

            // 平行篮筐
            ctx.shadowBlur = 14;
            ctx.shadowColor = '#c99f33';
            ctx.beginPath();
            ctx.ellipse(hoop.x, hoop.y, hoop.rimRadiusX, hoop.rimRadiusY, 0, 0, Math.PI*2);
            ctx.strokeStyle = '#f2c744';
            ctx.lineWidth = 6;
            ctx.stroke();
            ctx.beginPath();
            ctx.ellipse(hoop.x, hoop.y, hoop.rimRadiusX-3, hoop.rimRadiusY-2, 0, 0, Math.PI*2);
            ctx.strokeStyle = '#f9df92';
            ctx.lineWidth = 3;
            ctx.stroke();

            ctx.beginPath();
            for (let i = -1; i <= 1; i+=2) {
                ctx.moveTo(hoop.x + i*12, hoop.y + 6);
                ctx.lineTo(hoop.x + i*12 - 4*i, hoop.y + 32);
            }
            ctx.strokeStyle = '#bcb09b';
            ctx.lineWidth = 2;
            ctx.stroke();

            ctx.shadowBlur = 0;

            // 篮球
            ctx.shadowBlur = 14;
            ctx.shadowColor = '#2e2e2e';
            ctx.beginPath();
            ctx.arc(ball.x, ball.y, ball.radius, 0, 2 * Math.PI);
            ctx.fillStyle = '#c96e28';
            ctx.fill();
            ctx.strokeStyle = '#2c1e0e';
            ctx.lineWidth = 2.5;
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(ball.x - ball.radius, ball.y);
            ctx.lineTo(ball.x + ball.radius, ball.y);
            ctx.moveTo(ball.x, ball.y - ball.radius);
            ctx.lineTo(ball.x, ball.y + ball.radius);
            ctx.strokeStyle = '#2c1e0e';
            ctx.stroke();

            // 拖动虚线
            if (isDragging && !isBallMoving) {
                ctx.beginPath();
                ctx.moveTo(ball.x, ball.y);
                ctx.lineTo(dragEnd.x, dragEnd.y);
                ctx.strokeStyle = '#fff9d7';
                ctx.lineWidth = 3;
                ctx.setLineDash([9, 9]);
                ctx.stroke();
                const dragDist = Math.hypot(dragEnd.x - ball.x, dragEnd.y - ball.y);
                const power = Math.min(dragDist * 0.2, 16).toFixed(1);
                ctx.font = 'bold 20px "Segoe UI"';
                ctx.fillStyle = '#fef7e0';
                ctx.shadowBlur = 10;
                ctx.fillText(`💪 ${power}`, ball.x - 15, ball.y - 40);
                ctx.setLineDash([]);
            } else {
                ctx.setLineDash([]);
            }

            drawParticles();
            // 绘制所有文字浮层(得分+恭喜消息)
            drawFloatingElements();
        }

        // 动画循环
        function gameLoop() {
            updateBall();
            updateParticles();
            drawScene();
            requestAnimationFrame(gameLoop);
        }

        // ---------- 触摸事件 (针对iPhone优化) + 鼠标兼容 ----------
        function getCanvasCoords(e) {
            const rect = canvas.getBoundingClientRect();
            const scaleX = canvas.width / rect.width;
            const scaleY = canvas.height / rect.height;
            let clientX, clientY;
            if (e.touches) {
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
                e.preventDefault();
            } else {
                clientX = e.clientX;
                clientY = e.clientY;
            }
            const x = (clientX - rect.left) * scaleX;
            const y = (clientY - rect.top) * scaleY;
            return { x: Math.min(W, Math.max(0, x)), y: Math.min(H, Math.max(0, y)) };
        }

        function startDrag(e) {
            e.preventDefault();
            tryPlayMusic();
            if (isBallMoving) return;
            const { x, y } = getCanvasCoords(e);
            isDragging = true;
            dragStart.x = ball.x;
            dragStart.y = ball.y;
            dragEnd.x = x;
            dragEnd.y = y;
        }

        function moveDrag(e) {
            if (!isDragging || isBallMoving) return;
            e.preventDefault();
            const { x, y } = getCanvasCoords(e);
            dragEnd.x = x;
            dragEnd.y = y;
        }

        function endDrag(e) {
            e.preventDefault();
            if (!isDragging || isBallMoving) {
                isDragging = false;
                return;
            }
            let dx = dragEnd.x - dragStart.x;
            let dy = dragEnd.y - dragStart.y;
            const speed = Math.sqrt(dx*dx + dy*dy) * 0.2;
            if (speed < MIN_POWER) {
                isDragging = false;
                return;
            }
            const angle = Math.atan2(dy, dx);
            ball.vx = -Math.cos(angle) * speed * 1.25;
            ball.vy = -Math.sin(angle) * speed * 1.25;
            const maxSpeed = 18;
            if (Math.abs(ball.vx) > maxSpeed) ball.vx = (ball.vx > 0 ? maxSpeed : -maxSpeed);
            if (Math.abs(ball.vy) > maxSpeed) ball.vy = (ball.vy > 0 ? maxSpeed : -maxSpeed);
            isBallMoving = true;
            isDragging = false;
        }

        function cancelDrag() {
            isDragging = false;
        }

        // 统一事件监听 (支持触摸与鼠标)
        canvas.addEventListener('mousedown', startDrag);
        canvas.addEventListener('mousemove', moveDrag);
        canvas.addEventListener('mouseup', endDrag);
        canvas.addEventListener('mouseleave', cancelDrag);

        // 触摸事件
        canvas.addEventListener('touchstart', startDrag, { passive: false });
        canvas.addEventListener('touchmove', moveDrag, { passive: false });
        canvas.addEventListener('touchend', endDrag, { passive: false });
        canvas.addEventListener('touchcancel', cancelDrag, { passive: false });

        canvas.addEventListener('dragstart', (e) => e.preventDefault());

        // 按钮
        document.getElementById('resetBtn').addEventListener('click', () => {
            resetGame();
            tryPlayMusic();
        });
        document.getElementById('muteToggleBtn').addEventListener('click', toggleMute);

        // 尝试启动音乐 (一次交互)
        document.addEventListener('click', function once() {
            if (!musicStarted && !isMuted) {
                backgroundMusic.play().then(() => musicStarted = true).catch(() => {});
            }
        }, { once: true });

        // 启动
        randomizeHoopPosition();
        resetGame();
        gameLoop();
    })();
</script>
</body>
</html>
相关推荐
一只理智恩1 小时前
基于 CesiumJS + React + Go 实现三维无人机编队实时巡航可视化系统
前端·人工智能·算法·golang·无人机
Zhu_S W1 小时前
EasyExcel:让Excel操作变得简单优雅
java·前端
GISer_Jing2 小时前
从零到架构师:Taro 全链路学习与实战指南
前端·react.js·taro
phltxy2 小时前
快速上手 ElementPlus:核心用法精讲
前端·javascript·vue.js
SuperEugene2 小时前
数组的 10 个常用操作:map / filter / reduce 实战套路
前端·javascript
晓得迷路了2 小时前
栗子前端技术周刊第 117 期 - TypeScript 6.0 Beta、webpack 2026 年路线图、React 最新生态调查报告结果...
前端·javascript·react.js
摇滚侠2 小时前
bootstrap 框架讲解-快速上手,最适合后端开发人员的bootstrap 保姆级使用教程
前端·bootstrap·html
德育处主任Pro2 小时前
『NAS』在飞牛部署一个积木塔游戏-TowerBlocks
游戏
lzhdim2 小时前
CSS实现毛玻璃模糊效果
前端·css