代码: 全选
> OK let me now produce the final HTML. I'll be comprehensive and make sure everything works.
> Let me also fix a small issue: the original code uses `ctx.roundRect` but that's a method on CanvasRenderingContext2D that might need a polyfill in some browsers. Actually, `roundRect` is supported in Chrome 99+, Firefox 112+, Safari 15.4+. It should be fine for modern browsers. The original code used it so I'll keep it.
> Now I'll write the full HTML.
This is a complete HTML document for a "纸飞机极速下落" (Paper Plane Speed Drop) game. It's a side-scrolling avoidance game where you control a paper plane falling through obstacles, built with Canvas and SVG for a pseudo-3D effect.
```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>紙飛機極速下落</title>
<style>
:root {
--bg-top: #E0F7FA;
--bg-bottom: #C8E6C9;
--primary: #4CAF50;
--text-color: #333;
}
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #333;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
touch-action: none;
}
#game-wrapper {
position: relative;
width: 100%;
max-width: 450px;
height: 100vh;
max-height: 800px;
background: linear-gradient(to bottom, var(--bg-top), var(--bg-bottom));
border: 4px solid #333;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#plane-svg {
position: absolute;
width: 50px;
height: 50px;
top: 0;
left: 0;
transform-origin: center center;
pointer-events: none;
z-index: 10;
transition: fill 0.2s;
}
.ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.85);
z-index: 20;
backdrop-filter: blur(3px);
padding: 20px;
box-sizing: border-box;
}
.hidden {
display: none !important;
}
.hud {
position: absolute;
top: 15px;
left: 15px;
right: 15px;
display: flex;
justify-content: space-between;
font-size: 20px;
font-weight: 900;
color: #333;
text-shadow: 2px 2px 0px #fff;
z-index: 15;
pointer-events: none;
}
button {
padding: 12px 30px;
font-size: 20px;
font-weight: bold;
color: #fff;
background-color: var(--primary);
border: 3px solid #333;
border-radius: 12px;
cursor: pointer;
box-shadow: 4px 4px 0px #333;
margin-bottom: 20px;
transition: transform 0.1s, box-shadow 0.1s;
-webkit-tap-highlight-color: transparent;
}
button:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #333;
}
h2 {
margin-top: 0;
font-size: 32px;
color: #333;
text-shadow: 2px 2px 0px #fff;
text-align: center;
}
.stat-text {
font-size: 20px;
font-weight: bold;
color: #333;
margin: 6px 0;
}
.highlight-score {
color: #FF1744;
font-size: 28px;
font-weight: 900;
}
.highscore-msg {
font-size: 20px;
font-weight: bold;
margin: 10px 0 20px 0;
padding: 10px 20px;
border-radius: 10px;
background: #FFF8E1;
border: 2px dashed #FF9800;
color: #E65100;
text-align: center;
animation: popIn 0.4s ease-out;
}
.highscore-msg.new-record {
background: #FFF3E0;
border-color: #FF5722;
color: #BF360C;
animation: popIn 0.4s ease-out, pulse 0.8s ease-in-out infinite;
}
@keyframes popIn {
0% {
transform: scale(0.7);
opacity: 0;
}
70% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(255, 87, 34, 0.4);
}
50% {
box-shadow: 0 0 0 12px rgba(255, 87, 34, 0);
}
}
.menu-highscore {
font-size: 16px;
font-weight: bold;
color: #FF6D00;
margin-top: 8px;
text-align: center;
min-height: 22px;
}
</style>
</head>
<body>
<div id="game-wrapper">
<!-- HUD -->
<div class="hud" id="hud">
<div id="scoreDisplay">分數: 0</div>
<div id="timeDisplay">時間: 0.0s</div>
</div>
<!-- 遊戲畫布 -->
<canvas id="gameCanvas"></canvas>
<!-- SVG 紙飛機 -->
<svg id="plane-svg" viewBox="-25 -25 50 50">
<polygon id="planeLeft" points="0,20 -20,-15 0,-5" fill="#FFFFFF" stroke="#333" stroke-width="2" stroke-linejoin="round" />
<polygon id="planeRight" points="0,20 0,-5 20,-15" fill="#E0E0E0" stroke="#333" stroke-width="2" stroke-linejoin="round" />
</svg>
<!-- 主選單 -->
<div class="ui-layer" id="menuUI">
<h2>紙飛機極速下落</h2>
<button id="startBtn">開始遊戲</button>
<div id="menuHighScore" class="menu-highscore"></div>
</div>
<!-- 遊戲結束 -->
<div class="ui-layer hidden" id="gameOverUI">
<h2 style="color: #FF1744;">遊戲結束!</h2>
<p class="stat-text">最終分數: <span id="finalScore" class="highlight-score">0</span></p>
<p class="stat-text">用時: <span id="finalTime" style="font-size:22px; font-weight:900;">0.0s</span></p>
<div id="highScoreMsg"></div>
<button id="restartBtn">重新開始</button>
</div>
</div>
<script>
/**
* 遊戲參數配置
*/
const CONFIG = {
blockDensity: 160,
blockMinW: 100,
blockMaxW: 240,
blockHeight: 20,
baseFallSpeed: 9,
turnSpeed: 0.08,
maxAngle: Math.PI / 2.2
};
// ──────────────── DOM 元素 ────────────────
const wrapper = document.getElementById('game-wrapper');
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const planeSvg = document.getElementById('plane-svg');
const planeLeft = document.getElementById('planeLeft');
const planeRight = document.getElementById('planeRight');
const uiMenu = document.getElementById('menuUI');
const uiGameOver = document.getElementById('gameOverUI');
const scoreDisp = document.getElementById('scoreDisplay');
const timeDisp = document.getElementById('timeDisplay');
const menuHighScoreEl = document.getElementById('menuHighScore');
let canvasW, canvasH;
let animationFrameId;
// ──────────────── 遊戲狀態 ────────────────
let state = {
isPlaying: false,
score: 0,
time: 0,
startTime: 0,
cameraY: 0,
lastSpawnY: 0
};
let plane = {
x: 0,
worldY: 0,
screenY: 0,
angle: 0,
isDead: false
};
let blocks = [];
let keys = { left: false, right: false };
// ──────────────── 本輪會話最高分記錄(頁面刷新即重置) ────────────────
var sessionHighScore = -1; // -1 表示尚未有任何遊戲記錄
var sessionHighScoreTime = 0;
var lastGameScore = 0;
var lastGameTime = 0;
var isGameOverState = false;
var lastGameIsNewHighScore = false;
// ──────────────── 外部 API(供 iframe 父頁面調用) ────────────────
window.PaperPlaneGame = {
/**
* 取得完整遊戲狀態
* @returns {Object} 包含 isPlaying, isGameOver, lastScore, lastTime,
* sessionHighScore, sessionHighScoreTime, isNewHighScore
*/
getState: function() {
return {
isPlaying: state.isPlaying,
isGameOver: isGameOverState,
lastScore: lastGameScore,
lastTime: lastGameTime,
sessionHighScore: sessionHighScore,
sessionHighScoreTime: sessionHighScoreTime,
isNewHighScore: lastGameIsNewHighScore
};
},
/** 遊戲是否正在進行中 */
isPlaying: function() { return state.isPlaying; },
/** 遊戲是否已結束(遊戲結束畫面顯示中) */
isGameOver: function() { return isGameOverState; },
/** 取得上一次遊戲的分數 */
getLastScore: function() { return lastGameScore; },
/** 取得上一次遊戲的用時(秒) */
getLastTime: function() { return lastGameTime; },
/** 取得本輪會話最高分 */
getSessionHighScore: function() { return sessionHighScore; },
/** 取得本輪會話最高分的用時 */
getSessionHighScoreTime: function() { return sessionHighScoreTime; },
/** 上一次遊戲是否刷新了最高分 */
isNewHighScore: function() { return lastGameIsNewHighScore; },
/**
* 遊戲結束時的回調(同一來源的父頁面可設置此屬性)
* @type {Function|null} callback(score, time, isNewHighScore)
* @example
* window.PaperPlaneGame.onGameOver = function(score, time, isNew) {
* console.log('Game over! Score:', score);
* };
*/
onGameOver: null
};
/**
* 向父窗口發送 postMessage(適用於跨來源 iframe 嵌入)
*/
function notifyParentGameOver(score, time, isNewHighScore) {
if (window.parent && window.parent !== window) {
try {
window.parent.postMessage({
source: 'paperPlaneGame',
event: 'gameOver',
data: {
score: score,
time: time,
isNewHighScore: isNewHighScore,
sessionHighScore: sessionHighScore,
sessionHighScoreTime: sessionHighScoreTime
}
}, '*');
} catch (e) {
// 忽略跨來源通訊錯誤
}
}
}
// ──────────────── 輔助函式 ────────────────
function resizeCanvas() {
canvasW = wrapper.clientWidth;
canvasH = wrapper.clientHeight;
canvas.width = canvasW;
canvas.height = canvasH;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
/** 更新選單上的本輪最高分顯示 */
function updateMenuHighScore() {
if (sessionHighScore >= 0) {
menuHighScoreEl.textContent =
'本次最高分:' + sessionHighScore + ' 分 (用時 ' + sessionHighScoreTime + 's)';
} else {
menuHighScoreEl.textContent = '';
}
}
// ──────────────── 鍵盤控制 ────────────────
window.addEventListener('keydown', (e) => {
if (e.code === 'ArrowLeft') keys.left = true;
if (e.code === 'ArrowRight') keys.right = true;
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft') keys.left = false;
if (e.code === 'ArrowRight') keys.right = false;
});
// ──────────────── 觸控控制(手機端左右半屏) ────────────────
wrapper.addEventListener('touchstart', (e) => {
if (!state.isPlaying) return;
for (let i = 0; i < e.changedTouches.length; i++) {
let touch = e.changedTouches[i];
if (touch.clientX < window.innerWidth / 2) keys.left = true;
else keys.right = true;
}
});
wrapper.addEventListener('touchend', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
let touch = e.changedTouches[i];
if (touch.clientX < window.innerWidth / 2) keys.left = false;
else keys.right = false;
}
});
wrapper.addEventListener('touchcancel', () => {
keys.left = false;
keys.right = false;
});
// ──────────────── 遊戲流程 ────────────────
function startGame() {
uiMenu.classList.add('hidden');
uiGameOver.classList.add('hidden');
isGameOverState = false;
// 初始化飛機
plane.x = 40;
plane.worldY = 40;
plane.screenY = 40;
plane.angle = CONFIG.maxAngle;
plane.isDead = false;
// 恢復飛機顏色
planeLeft.setAttribute('fill', '#FFFFFF');
planeRight.setAttribute('fill', '#E0E0E0');
state.isPlaying = true;
state.score = 0;
state.time = 0;
state.startTime = performance.now();
state.cameraY = 0;
state.lastSpawnY = canvasH;
blocks = [];
keys.left = false;
keys.right = false;
updateMenuHighScore();
requestAnimationFrame(gameLoop);
}
function gameOver() {
state.isPlaying = false;
plane.isDead = true;
isGameOverState = true;
// 飛機變紅色
planeLeft.setAttribute('fill', '#FF5252');
planeRight.setAttribute('fill', '#D32F2F');
// 記錄本次成績
lastGameScore = state.score;
lastGameTime = state.time;
// 判斷是否刷新本輪會話最高分
let isNewHighScore = (lastGameScore > sessionHighScore);
lastGameIsNewHighScore = isNewHighScore;
if (isNewHighScore) {
sessionHighScore = lastGameScore;
sessionHighScoreTime = lastGameTime;
}
// 更新遊戲結束 UI
document.getElementById('finalScore').innerText = lastGameScore;
document.getElementById('finalTime').innerText = lastGameTime + 's';
const highScoreMsgDiv = document.getElementById('highScoreMsg');
if (isNewHighScore) {
highScoreMsgDiv.className = 'highscore-msg new-record';
highScoreMsgDiv.innerHTML = '🎉 創造歷史最高分:<strong>' + sessionHighScore +
'</strong> 分!<br><small>用時 ' + sessionHighScoreTime + 's</small>';
} else {
highScoreMsgDiv.className = 'highscore-msg';
highScoreMsgDiv.innerHTML = '歷史最高分為 <strong>' + sessionHighScore +
'</strong> 分<br><small>(用時 ' + sessionHighScoreTime + 's)</small>';
}
// 呼叫外部回調(同來源 iframe)
if (typeof window.PaperPlaneGame.onGameOver === 'function') {
try {
window.PaperPlaneGame.onGameOver(lastGameScore, lastGameTime, isNewHighScore);
} catch (e) {
// 忽略回調中的錯誤
}
}
// 向父窗口發送 postMessage(跨來源 iframe)
notifyParentGameOver(lastGameScore, lastGameTime, isNewHighScore);
// 更新選單最高分
updateMenuHighScore();
// 延遲顯示結束畫面
setTimeout(() => {
uiGameOver.classList.remove('hidden');
}, 800);
}
function gameLoop(timestamp) {
if (!state.isPlaying) return;
// 更新時間與分數
state.time = ((timestamp - state.startTime) / 1000).toFixed(1);
state.score = Math.floor(plane.worldY / 100);
timeDisp.innerText = '時間: ' + state.time + 's';
scoreDisp.innerText = '分數: ' + state.score;
// 處理轉向
if (keys.left) plane.angle -= CONFIG.turnSpeed;
if (keys.right) plane.angle += CONFIG.turnSpeed;
plane.angle = Math.max(-CONFIG.maxAngle, Math.min(CONFIG.maxAngle, plane.angle));
// 計算下落速度
let angleRatio = Math.abs(plane.angle) / CONFIG.maxAngle;
let currentSpeed = CONFIG.baseFallSpeed * (1 - (angleRatio * 0.6));
// 更新飛機世界座標
plane.worldY += currentSpeed * Math.cos(plane.angle);
plane.x += currentSpeed * Math.sin(plane.angle);
// 邊界限制
if (plane.x < 20) { plane.x = 20;
plane.angle = 0; }
if (plane.x > canvasW - 20) { plane.x = canvasW - 20;
plane.angle = 0; }
// 攝像機跟隨
if (plane.worldY > canvasH / 2) {
state.cameraY = plane.worldY - canvasH / 2;
plane.screenY = canvasH / 2;
} else {
plane.screenY = plane.worldY;
}
// 生成障礙物
while (state.lastSpawnY < plane.worldY + canvasH) {
state.lastSpawnY += CONFIG.blockDensity;
let w = Math.random() * (CONFIG.blockMaxW - CONFIG.blockMinW) + CONFIG.blockMinW;
let x = Math.random() * (canvasW - w);
blocks.push({
x: x,
worldY: state.lastSpawnY,
w: w,
h: CONFIG.blockHeight,
isHit: false
});
}
// 清理舊方塊
blocks = blocks.filter(b => b.worldY > state.cameraY - 50);
// 更新 SVG 飛機偽三維渲染
let rotateDeg = -plane.angle * (180 / Math.PI);
let rollDeg = plane.angle * 45;
planeSvg.style.transform =
'translate(calc(' + plane.x + 'px - 50%), calc(' + plane.screenY + 'px - 50%)) rotate(' +
rotateDeg + 'deg) rotateY(' + rollDeg + 'deg)';
// 繪製畫面與碰撞檢測
ctx.clearRect(0, 0, canvasW, canvasH);
let planeHitbox = { x: plane.x - 10, y: plane.screenY - 15, w: 20, h: 30 };
blocks.forEach(b => {
let screenY = b.worldY - state.cameraY;
// AABB 碰撞檢測
if (
planeHitbox.x < b.x + b.w &&
planeHitbox.x + planeHitbox.w > b.x &&
planeHitbox.y < screenY + b.h &&
planeHitbox.y + planeHitbox.h > screenY
) {
b.isHit = true;
gameOver();
}
// 繪製方塊
ctx.fillStyle = b.isHit ? '#FF1744' : '#FF80AB';
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(b.x, screenY, b.w, b.h, 6);
} else {
// fallback for older browsers
ctx.rect(b.x, screenY, b.w, b.h);
}
ctx.fill();
ctx.stroke();
});
if (state.isPlaying) {
animationFrameId = requestAnimationFrame(gameLoop);
}
}
// ──────────────── 按鈕事件 ────────────────
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', () => {
startGame();
});
// ──────────────── 初始渲染 ────────────────
updateMenuHighScore();
</script>
</body>
</html>