
🎮 核心玩法
-
拖动投篮:在篮球上拖动鼠标/手指,反向拉长虚线控制力度和方向,松开后篮球飞出
-
篮筐瞬移:每投进一球(+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>