不知名提交
This commit is contained in:
39
InfoGenie-frontend/public/smallgame/躲树叶/index.html
Normal file
39
InfoGenie-frontend/public/smallgame/躲树叶/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
|
||||
<title>清新躲避 · 无尽模式</title>
|
||||
<meta name="theme-color" content="#d8f7c2" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="hud">
|
||||
<div class="score">分数 <span id="score">0</span></div>
|
||||
<button id="pauseBtn" class="btn" aria-label="暂停">Ⅱ</button>
|
||||
</div>
|
||||
|
||||
<canvas id="game" aria-label="清新躲避游戏画布"></canvas>
|
||||
|
||||
<div id="startOverlay" class="overlay show" role="dialog" aria-modal="true">
|
||||
<div class="card">
|
||||
<h1>清新躲避</h1>
|
||||
<p>按住并左右拖动,躲避下落的叶片。</p>
|
||||
<p>无尽模式,难度会随时间提升。</p>
|
||||
<button id="startBtn" class="btn primary">开始游戏</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="gameOverOverlay" class="overlay" role="dialog" aria-modal="true">
|
||||
<div class="card">
|
||||
<h2>游戏结束</h2>
|
||||
<p>本局分数:<strong id="finalScore">0</strong></p>
|
||||
<button id="restartBtn" class="btn primary">再来一次</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rotateOverlay">请将手机竖屏以获得最佳体验</div>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
261
InfoGenie-frontend/public/smallgame/躲树叶/script.js
Normal file
261
InfoGenie-frontend/public/smallgame/躲树叶/script.js
Normal file
@@ -0,0 +1,261 @@
|
||||
(() => {
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const hudScoreEl = document.getElementById('score');
|
||||
const pauseBtn = document.getElementById('pauseBtn');
|
||||
const startOverlay = document.getElementById('startOverlay');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const gameOverOverlay = document.getElementById('gameOverOverlay');
|
||||
const finalScoreEl = document.getElementById('finalScore');
|
||||
const restartBtn = document.getElementById('restartBtn');
|
||||
const rotateOverlay = document.getElementById('rotateOverlay');
|
||||
|
||||
let width = 0, height = 0, DPR = 1;
|
||||
let running = false, paused = false;
|
||||
let lastTime = 0, timeElapsed = 0, score = 0, spawnTimer = 0;
|
||||
|
||||
const player = { x: 0, y: 0, r: 18, vx: 0, targetX: null };
|
||||
const obstacles = [];
|
||||
let pointerActive = false;
|
||||
|
||||
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
|
||||
function rand(min, max){ return Math.random()*(max-min)+min; }
|
||||
|
||||
function updateOrientationOverlay(){
|
||||
const landscape = window.innerWidth > window.innerHeight;
|
||||
rotateOverlay.style.display = landscape ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function resize(){
|
||||
updateOrientationOverlay();
|
||||
DPR = Math.min(2, window.devicePixelRatio || 1);
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
canvas.width = Math.floor(width * DPR);
|
||||
canvas.height = Math.floor(height * DPR);
|
||||
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
||||
if (!running){
|
||||
player.x = width * 0.5;
|
||||
player.y = height - Math.max(80, height * 0.12);
|
||||
} else {
|
||||
player.y = height - Math.max(80, height * 0.12);
|
||||
player.x = clamp(player.x, player.r + 8, width - player.r - 8);
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
function drawBackground(){
|
||||
// 轻微的顶部高光,让画面更通透
|
||||
const g = ctx.createLinearGradient(0,0,0,height);
|
||||
g.addColorStop(0,'rgba(255,255,255,0.10)');
|
||||
g.addColorStop(1,'rgba(255,255,255,0.00)');
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0,0,width,height);
|
||||
}
|
||||
|
||||
function drawPlayer(){
|
||||
ctx.save();
|
||||
ctx.translate(player.x, player.y);
|
||||
const r = player.r;
|
||||
const grad = ctx.createRadialGradient(-r*0.3, -r*0.3, r*0.2, 0, 0, r);
|
||||
grad.addColorStop(0, '#5fca7e');
|
||||
grad.addColorStop(1, '#3a9e5a');
|
||||
ctx.fillStyle = grad;
|
||||
// 圆形带小叶柄的简化“叶子”角色
|
||||
ctx.beginPath();
|
||||
ctx.arc(0,0,r,0,Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(58,158,90,0.7)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -r*0.9);
|
||||
ctx.quadraticCurveTo(r*0.2, -r*1.3, r*0.5, -r*1.0);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function spawnObstacle(){
|
||||
const difficulty = 1 + timeElapsed * 0.08; // 随时间慢慢提升
|
||||
const r = rand(10, 22);
|
||||
const x = rand(r+8, width - r - 8);
|
||||
const speed = rand(90, 140) * (0.9 + difficulty * 0.5);
|
||||
const rot = rand(-Math.PI*0.5, Math.PI*0.5);
|
||||
obstacles.push({ x, y: -r - 20, r, speed, rot, swayPhase: Math.random()*Math.PI*2, swayAmp: rand(6,12) });
|
||||
}
|
||||
|
||||
function drawObstacle(o){
|
||||
ctx.save();
|
||||
ctx.translate(o.x, o.y);
|
||||
ctx.rotate(o.rot);
|
||||
const r = o.r;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, r*0.9, r*0.6, 0, 0, Math.PI*2);
|
||||
const grad = ctx.createLinearGradient(-r, -r, r, r);
|
||||
grad.addColorStop(0, '#d8f7c2');
|
||||
grad.addColorStop(0.5, '#b9ef9f');
|
||||
grad.addColorStop(1, '#9edf77');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(90,150,90,0.5)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r*0.5, 0);
|
||||
ctx.quadraticCurveTo(0, -r*0.3, r*0.5, 0);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function update(dt){
|
||||
const difficulty = 1 + timeElapsed * 0.08;
|
||||
const spawnInterval = Math.max(0.12, 0.9 / difficulty);
|
||||
spawnTimer -= dt;
|
||||
if (spawnTimer <= 0){
|
||||
spawnObstacle();
|
||||
spawnTimer = spawnInterval;
|
||||
}
|
||||
|
||||
// 障碍移动 + 轻微左右摆动
|
||||
for (let i = 0; i < obstacles.length; i++){
|
||||
const o = obstacles[i];
|
||||
o.y += o.speed * dt;
|
||||
o.x += Math.sin(o.swayPhase + timeElapsed * 1.6) * (o.swayAmp * dt);
|
||||
}
|
||||
|
||||
// 清除离开屏幕的障碍
|
||||
for (let i = obstacles.length - 1; i >= 0; i--){
|
||||
const o = obstacles[i];
|
||||
if (o.y > height + o.r + 60){
|
||||
obstacles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘轻推(桌面端备用)
|
||||
if (player.targetX != null){
|
||||
const dir = player.targetX - player.x;
|
||||
player.vx = clamp(dir, -500, 500);
|
||||
player.x += player.vx * dt;
|
||||
if (Math.abs(dir) < 2){
|
||||
player.targetX = null;
|
||||
player.vx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 限制玩家范围
|
||||
player.x = clamp(player.x, player.r + 8, width - player.r - 8);
|
||||
|
||||
// 碰撞检测(近似圆形)
|
||||
for (const o of obstacles){
|
||||
const dx = o.x - player.x;
|
||||
const dy = o.y - player.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist < player.r + o.r * 0.65){
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 计分:按生存时间累计
|
||||
score += dt * 10; // 每秒约10分
|
||||
hudScoreEl.textContent = Math.floor(score);
|
||||
}
|
||||
|
||||
function render(){
|
||||
ctx.clearRect(0,0,width,height);
|
||||
drawBackground();
|
||||
for (const o of obstacles) drawObstacle(o);
|
||||
drawPlayer();
|
||||
}
|
||||
|
||||
function loop(t){
|
||||
if (!running){ return; }
|
||||
if (paused){
|
||||
lastTime = t;
|
||||
requestAnimationFrame(loop);
|
||||
return;
|
||||
}
|
||||
if (!lastTime) lastTime = t;
|
||||
const dt = Math.min(0.033, (t - lastTime)/1000);
|
||||
lastTime = t;
|
||||
timeElapsed += dt;
|
||||
update(dt);
|
||||
render();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function startGame(){
|
||||
obstacles.length = 0;
|
||||
score = 0;
|
||||
timeElapsed = 0;
|
||||
spawnTimer = 0;
|
||||
running = true;
|
||||
paused = false;
|
||||
lastTime = 0;
|
||||
startOverlay.classList.remove('show');
|
||||
gameOverOverlay.classList.remove('show');
|
||||
resize();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function endGame(){
|
||||
running = false;
|
||||
paused = false;
|
||||
finalScoreEl.textContent = Math.floor(score);
|
||||
gameOverOverlay.classList.add('show');
|
||||
}
|
||||
|
||||
function pointerToCanvasX(e){
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return clamp(e.clientX - rect.left, 0, rect.width);
|
||||
}
|
||||
|
||||
// 触控与指针事件:按住并左右拖动
|
||||
canvas.addEventListener('pointerdown', e => {
|
||||
pointerActive = true;
|
||||
player.targetX = null;
|
||||
player.x = pointerToCanvasX(e);
|
||||
});
|
||||
canvas.addEventListener('pointermove', e => {
|
||||
if (!pointerActive) return;
|
||||
player.x = pointerToCanvasX(e);
|
||||
});
|
||||
canvas.addEventListener('pointerup', () => { pointerActive = false; });
|
||||
canvas.addEventListener('pointercancel', () => { pointerActive = false; });
|
||||
|
||||
// 轻点屏幕:向左/右轻推一段距离
|
||||
canvas.addEventListener('click', e => {
|
||||
const x = pointerToCanvasX(e);
|
||||
const center = width / 2;
|
||||
const dir = x < center ? -1 : 1;
|
||||
player.targetX = clamp(player.x + dir * Math.max(50, width * 0.12), player.r + 8, width - player.r - 8);
|
||||
});
|
||||
|
||||
// 键盘备用控制(桌面端)
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a'){
|
||||
player.targetX = clamp(player.x - Math.max(50, width * 0.12), player.r + 8, width - player.r - 8);
|
||||
} else if (e.key === 'ArrowRight' || e.key === 'd'){
|
||||
player.targetX = clamp(player.x + Math.max(50, width * 0.12), player.r + 8, width - player.r - 8);
|
||||
} else if (e.key === ' ') {
|
||||
togglePause();
|
||||
} else if (e.key === 'Enter' && !running){
|
||||
startGame();
|
||||
}
|
||||
});
|
||||
|
||||
// 按钮
|
||||
pauseBtn.addEventListener('click', togglePause);
|
||||
startBtn.addEventListener('click', startGame);
|
||||
restartBtn.addEventListener('click', startGame);
|
||||
|
||||
function togglePause(){
|
||||
if (!running) return;
|
||||
paused = !paused;
|
||||
pauseBtn.textContent = paused ? '▶' : 'Ⅱ';
|
||||
}
|
||||
|
||||
// 避免滚动与系统手势干扰
|
||||
['touchstart','touchmove','touchend'].forEach(type => {
|
||||
window.addEventListener(type, e => { if (pointerActive) e.preventDefault(); }, { passive: false });
|
||||
});
|
||||
})();
|
||||
106
InfoGenie-frontend/public/smallgame/躲树叶/style.css
Normal file
106
InfoGenie-frontend/public/smallgame/躲树叶/style.css
Normal file
@@ -0,0 +1,106 @@
|
||||
/* 主题色:淡绿色到淡黄绿色的清新渐变 */
|
||||
:root {
|
||||
--bg-start: #dff9d3;
|
||||
--bg-mid: #eaffd1;
|
||||
--bg-end: #e9fbb5;
|
||||
--accent: #4fb66d;
|
||||
--accent-dark: #3a9e5a;
|
||||
--text: #2f4f3f;
|
||||
--hud-bg: rgba(255, 255, 255, 0.65);
|
||||
--overlay-bg: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, PingFang SC, Microsoft YaHei, sans-serif;
|
||||
color: var(--text);
|
||||
background: linear-gradient(180deg, var(--bg-start), var(--bg-mid), var(--bg-end));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* 顶部HUD */
|
||||
#hud {
|
||||
position: fixed;
|
||||
top: env(safe-area-inset-top, 12px);
|
||||
left: env(safe-area-inset-left, 12px);
|
||||
right: env(safe-area-inset-right, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
margin: 8px;
|
||||
border-radius: 14px;
|
||||
background: var(--hud-bg);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.08);
|
||||
}
|
||||
#hud .score { font-weight: 600; letter-spacing: 0.5px; }
|
||||
|
||||
/* 画布填充屏幕,适配竖屏 */
|
||||
#game {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
box-shadow: 0 6px 14px rgba(79,182,109,0.35);
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn.primary { font-weight: 600; }
|
||||
|
||||
/* 覆盖层样式 */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.overlay.show { display: flex; }
|
||||
|
||||
.card {
|
||||
width: min(520px, 92vw);
|
||||
padding: 20px 18px;
|
||||
border-radius: 16px;
|
||||
background: var(--overlay-bg);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 10px 22px rgba(0,0,0,0.12);
|
||||
text-align: center;
|
||||
}
|
||||
.card h1, .card h2 { margin: 8px 0 12px; }
|
||||
.card p { margin: 6px 0 12px; }
|
||||
|
||||
/* 横屏提示覆盖层 */
|
||||
#rotateOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-dark);
|
||||
background: rgba(255,255,255,0.6);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
Reference in New Issue
Block a user