以下是您所需的"飘忽气球"游戏的完整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>