CX-7のゲーム・Gemini・擬人化・ガソリン高騰中・ガソリン高騰に関するカスタム事例
2026年03月19日 18時07分
改良版。GeminiのCanvasに、以下をコピペして、しばらく待って、開くを押して、遊んでみてください❤️
CX-7のキャラ・赤狐ナナ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Spring Drive - 赤い狐娘の疾走</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; overflow: hidden; background-color: #1a202c; touch-action: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
canvas { display: block; }
.glass-panel {
background: rgba(20, 20, 20, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.control-btn {
transition: all 0.15s ease-in-out;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.control-btn:active, .control-btn.active {
background: rgba(255, 255, 255, 0.4);
transform: scale(0.95);
}
.text-shadow { text-shadow: 0px 4px 8px rgba(0,0,0,0.8); }
.text-glow { text-shadow: 0 0 10px rgba(255, 105, 180, 0.8), 0 0 20px rgba(255, 105, 180, 0.5); }
@keyframes fall {
0% { transform: translateY(-10vh) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(360deg); opacity: 0; }
}
.sakura-petal {
position: absolute;
border-radius: 150% 0 150% 0;
animation: fall linear infinite;
pointer-events: none;
}
</style>
</head>
<body class="text-white select-none">
<div class="relative w-full h-screen overflow-hidden" id="game-container">
<canvas id="gameCanvas" class="w-full h-full"></canvas>
<!-- UIレイヤー -->
<div class="absolute top-2 left-2 z-10 pointer-events-none flex gap-4">
<div class="glass-panel text-white px-3 py-1.5 rounded-xl text-xs sm:text-sm font-black tracking-wider shadow-lg">
SCORE: <span id="scoreDisplay" class="text-pink-300">0</span>m
</div>
</div>
<div class="absolute top-2 right-2 z-10 pointer-events-none">
<div class="glass-panel text-white px-3 py-1.5 rounded-xl text-xs sm:text-sm font-black tracking-wider flex items-center gap-1.5 shadow-lg">
LIVES:
<div id="livesContainer" class="flex gap-0.5 text-red-500"></div>
</div>
</div>
<div class="absolute top-16 left-1/2 transform -translate-x-1/2 w-11/12 max-w-md z-10 pointer-events-none">
<div class="w-full h-2 bg-gray-800/50 rounded-full overflow-hidden border border-white/20 shadow-inner">
<div id="progressBar" class="h-full bg-gradient-to-r from-pink-500 via-red-500 to-yellow-400 w-0 transition-all duration-300 shadow-[0_0_10px_rgba(255,0,128,0.8)]"></div>
</div>
</div>
<!-- 操作ボタン -->
<div class="absolute bottom-6 left-1/2 transform -translate-x-1/2 w-11/12 max-w-md flex gap-6 px-2 z-10 pointer-events-none">
<button id="btnLeft" class="control-btn pointer-events-auto flex-1 h-12 sm:h-14 rounded-full flex items-center justify-center text-white text-2xl sm:text-3xl shadow-[0_4px_10px_rgba(0,0,0,0.3)] bg-black/20 backdrop-blur-md border border-white/30">◀</button>
<button id="btnRight" class="control-btn pointer-events-auto flex-1 h-12 sm:h-14 rounded-full flex items-center justify-center text-white text-2xl sm:text-3xl shadow-[0_4px_10px_rgba(0,0,0,0.3)] bg-black/20 backdrop-blur-md border border-white/30">▶</button>
</div>
<!-- スタート画面 -->
<div id="startScreen" class="absolute inset-0 bg-gradient-to-b from-black/90 via-black/50 to-transparent flex flex-col items-center justify-start z-20 transition-opacity duration-300 px-4 text-center pt-12 sm:pt-20">
<div class="mb-6">
<h1 class="text-6xl sm:text-8xl font-black mb-2 text-glow text-transparent bg-clip-text bg-gradient-to-r from-red-500 via-pink-500 to-white tracking-tighter">Spring Drive</h1>
<h2 class="text-xl sm:text-3xl font-bold text-pink-200 text-shadow tracking-widest">~ 赤い狐娘の疾走 ~</h2>
</div>
<button id="btnStart" class="px-16 py-6 mb-8 bg-gradient-to-r from-red-600 via-pink-600 to-red-600 hover:from-red-500 hover:to-pink-500 rounded-full text-4xl font-black border-4 border-white/80 shadow-[0_0_40px_rgba(255,50,50,0.8)] transition-all hover:scale-110 active:scale-90 text-white tracking-widest animate-pulse">
スタート
</button>
<div class="glass-panel p-6 sm:p-8 rounded-2xl max-w-lg shadow-2xl bg-black/60 border-red-500/40">
<p class="text-md sm:text-lg mb-4 text-gray-200 leading-relaxed">
CX-7の化身<span class="text-red-400 font-bold mx-1 text-xl">「赤い狐娘」</span>を操作し、<br>障害物を避けて春の道を駆け抜けよう!
</p>
<p class="text-yellow-400 font-black text-2xl mb-4 drop-shadow-md">目標: 5000m先のゴール</p>
<div class="text-sm text-gray-300 text-left bg-black/50 p-4 rounded-xl border border-white/10">
【操作方法】<br>PC: 左右矢印キー [←] [→]<br>スマホ: 画面下の[◀] [▶]ボタンをタップ
</div>
</div>
</div>
<!-- ゲームオーバー画面 -->
<div id="gameOverScreen" class="absolute inset-0 bg-black/90 backdrop-blur-md flex flex-col items-center justify-center z-20 hidden">
<h2 class="text-6xl sm:text-8xl font-black mb-4 text-red-600 text-shadow tracking-widest">CRASH!</h2>
<p class="text-2xl sm:text-3xl mb-12 text-gray-300 font-bold">走破距離: <span id="finalScore" class="text-yellow-400 text-4xl">0</span> m</p>
<div class="flex flex-col sm:flex-row gap-6">
<button id="btnRestart" class="px-10 py-4 bg-gradient-to-r from-red-600 to-pink-600 text-white hover:from-red-500 hover:to-pink-500 rounded-full text-2xl font-bold border-2 border-white/50 transition-all hover:scale-105 active:scale-95 shadow-[0_0_20px_rgba(255,0,0,0.5)]">再挑戦</button>
<button id="btnToTitle1" class="px-10 py-4 bg-gray-800 text-white hover:bg-gray-700 rounded-full text-xl font-bold border-2 border-gray-500 transition-all hover:scale-105 active:scale-95">タイトルへ</button>
</div>
</div>
<!-- クリア画面 -->
<div id="clearScreen" class="absolute inset-0 bg-white/85 backdrop-blur-sm flex flex-col items-center justify-center z-20 hidden px-4 overflow-hidden">
<div id="clearConfetti" class="absolute inset-0 pointer-events-none z-30"></div>
<h2 class="text-4xl sm:text-6xl font-black mb-2 text-pink-500 drop-shadow-md z-40 text-center animate-bounce">おめでとう!</h2>
<h3 class="text-3xl sm:text-5xl font-black mb-4 text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-pink-400 drop-shadow-lg z-40 text-center">GOAL CLEARED!</h3>
<p class="text-xl sm:text-2xl mb-4 text-gray-800 font-bold z-40">見事5000mを走り抜けました!</p>
<div class="bg-red-50/90 p-6 rounded-2xl shadow-xl border-2 border-red-200 mb-8 z-40 max-w-md text-center">
<p class="text-xl text-red-700 font-bold mb-2">赤い狐娘からのメッセージ</p>
<p class="text-lg text-gray-700">「最高のドライブだったね!<br>マツダの走る歓び、感じてくれた?」</p>
</div>
<div class="flex flex-col sm:flex-row gap-6 z-40">
<button id="btnPlayAgain" class="px-10 py-4 bg-gradient-to-r from-red-500 to-pink-500 text-white hover:from-red-400 hover:to-pink-400 rounded-full text-2xl font-bold border-2 border-white shadow-[0_10px_20px_rgba(255,50,50,0.4)] transition-all hover:scale-105 active:scale-95">もう一度遊ぶ</button>
<button id="btnToTitle2" class="px-10 py-4 bg-white text-red-500 hover:bg-gray-100 rounded-full text-xl font-bold border-2 border-red-300 transition-all hover:scale-105 active:scale-95">タイトルへ</button>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const UI = {
score: document.getElementById('scoreDisplay'),
lives: document.getElementById('livesContainer'),
progress: document.getElementById('progressBar'),
screens: {
start: document.getElementById('startScreen'),
over: document.getElementById('gameOverScreen'),
clear: document.getElementById('clearScreen')
},
finalScore: document.getElementById('finalScore')
};
const GOAL_DISTANCE = 5000;
let gameState = 'START';
let lastTime = 0;
let score = 0;
let lives = 3;
let gameSpeed = 350;
let timeAccumulator = 0;
let bgScrollOffset = 0; // 背景スクロール用
let player = { x: 0, y: 0, w: 60, h: 120, speed: 500, invulnerable: 0, tilt: 0 };
let obstacles = [], items = [], particles = [], speedLines = [], popEffects = [], goal = null;
let nextItemScore = 1000;
let nextSlowItemScore = 450;
let nextStarItemScore = 500; // 初期出現スコアを500mに変更
let nextEdgeObstacleScore = 300;
let slowEffectDistance = 0;
let invincibleDistance = 0;
let keys = { ArrowLeft: false, ArrowRight: false };
let touchLeft = false, touchRight = false;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
player.w = Math.max(40, Math.min(70, canvas.width * 0.1));
player.h = player.w * 2;
player.y = canvas.height - player.h * 0.6;
if(gameState === 'START') player.x = canvas.width / 2;
else player.x = Math.max(player.w * 0.5, Math.min(canvas.width - player.w * 0.5, player.x));
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
class PopEffect {
constructor(x, y, text, color) {
this.x = x; this.y = y; this.text = text; this.color = color;
this.life = 1.0; this.vy = -60;
}
update(dt) { this.y += this.vy * (dt/1000); this.life -= dt * 0.0015; }
draw(ctx) {
if(this.life <= 0) return;
ctx.save(); ctx.globalAlpha = this.life; ctx.fillStyle = this.color; ctx.font = '900 24px "Segoe UI", Arial'; ctx.textAlign = 'center';
ctx.shadowColor = 'black'; ctx.shadowBlur = 4; ctx.fillText(this.text, this.x, this.y); ctx.restore();
}
}
class Particle {
constructor(isInit = false, x, y, typeOverride = null) {
this.x = x !== undefined ? x : Math.random() * canvas.width;
this.y = isInit ? Math.random() * canvas.height : (y !== undefined ? y : -20);
this.size = Math.random() * 8 + 4;
this.speedMod = Math.random() * 0.4 + 0.8;
this.life = 1.0;
if(typeOverride) {
this.type = typeOverride; this.vx = (Math.random() - 0.5) * 400; this.vy = (Math.random() - 0.5) * 400;
if(this.type === 'star_sparkle') this.color = '#ffff00'; else if(this.type === 'break') this.color = '#ff5722';
} else {
let colorType = Math.random();
if (colorType > 0.4) {
this.color = Math.random() > 0.5 ? '#ffb7b2' : '#ffdac1'; this.type = 'sakura';
this.rotation = Math.random() * Math.PI; this.rotSpeed = (Math.random() - 0.5) * 5;
} else if (colorType > 0.1) {
this.color = Math.random() > 0.5 ? '#ffdf00' : '#fbc02d'; this.type = 'nanohana';
} else {
this.color = Math.random() > 0.5 ? '#81c784' : '#aed581'; this.type = 'leaf';
}
}
}
update(dt, baseSpeed) {
if (this.type === 'star_sparkle' || this.type === 'break') {
this.x += this.vx * (dt/1000); this.y += (this.vy + baseSpeed) * (dt/1000); this.life -= dt * 0.002;
} else {
this.y += baseSpeed * this.speedMod * (dt / 1000);
if(this.type === 'sakura') this.rotation += this.rotSpeed * (dt / 1000);
}
}
draw(ctx) {
if(this.life <= 0) return;
ctx.save(); ctx.globalAlpha = this.life; ctx.translate(this.x, this.y); ctx.fillStyle = this.color;
if(this.type === 'sakura') {
ctx.rotate(this.rotation); ctx.beginPath(); ctx.moveTo(0, this.size);
ctx.bezierCurveTo(this.size, this.size, this.size, -this.size, 0, -this.size/2); ctx.bezierCurveTo(-this.size, -this.size, -this.size, this.size, 0, this.size); ctx.fill();
} else { ctx.beginPath(); ctx.arc(0, 0, this.size/2, 0, Math.PI*2); ctx.fill(); }
ctx.restore();
}
}
class SpeedLine {
constructor() { this.x = Math.random() * canvas.width; this.y = -100; this.length = Math.random() * 150 + 50; this.opacity = Math.random() * 0.15 + 0.05; }
update(dt, speed) { this.y += speed * 1.5 * (dt / 1000); }
draw(ctx) {
ctx.strokeStyle = `rgba(255, 255, 255, ${this.opacity})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y + this.length); ctx.stroke();
}
}
class Obstacle {
constructor(xPos) {
this.r = 30; let padding = this.r + 20;
this.x = xPos !== undefined ? xPos : padding + Math.random() * (canvas.width - padding * 2);
this.y = -100; this.type = Math.random() > 0.5 ? 'cone' : 'barricade';
}
update(dt, speed) { this.y += speed * (dt / 1000); }
draw(ctx) {
ctx.save(); ctx.translate(this.x, this.y);
ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(0, this.r * 0.8, this.r * 1.2, this.r * 0.4, 0, 0, Math.PI*2); ctx.fill();
if (this.type === 'cone') {
ctx.fillStyle = '#ff5722'; ctx.beginPath(); ctx.moveTo(0, -this.r * 1.5); ctx.lineTo(-this.r * 0.8, this.r * 0.8); ctx.lineTo(this.r * 0.8, this.r * 0.8); ctx.fill();
ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.moveTo(-this.r * 0.4, -this.r * 0.5); ctx.lineTo(this.r * 0.4, -this.r * 0.5); ctx.lineTo(this.r * 0.55, -this.r * 0.1); ctx.lineTo(-this.r * 0.55, -this.r * 0.1); ctx.fill();
ctx.fillStyle = '#e64a19'; ctx.fillRect(-this.r, this.r * 0.6, this.r * 2, this.r * 0.3);
} else {
ctx.fillStyle = '#ffeb3b'; ctx.fillRect(-this.r*1.2, -this.r*0.5, this.r*2.4, this.r);
ctx.fillStyle = '#212121'; ctx.beginPath();
ctx.moveTo(-this.r*0.8, -this.r*0.5); ctx.lineTo(-this.r*0.4, this.r*0.5); ctx.lineTo(-this.r*0.1, this.r*0.5); ctx.lineTo(-this.r*0.5, -this.r*0.5);
ctx.moveTo(this.r*0.1, -this.r*0.5); ctx.lineTo(this.r*0.5, this.r*0.5); ctx.lineTo(this.r*0.8, this.r*0.5); ctx.lineTo(this.r*0.4, -this.r*0.5); ctx.fill();
ctx.fillStyle = '#757575'; ctx.fillRect(-this.r, this.r*0.5, 6, this.r*0.8); ctx.fillRect(this.r-6, this.r*0.5, 6, this.r*0.8);
ctx.fillStyle = '#f44336'; ctx.beginPath(); ctx.arc(-this.r*0.8, -this.r*0.7, 5, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
}
class BaseItem {
constructor() {
let padding = 40; this.x = padding + Math.random() * (canvas.width - padding * 2); this.y = -50; this.r = 25; this.pulse = 0;
}
update(dt, speed) { this.y += speed * (dt / 1000); this.pulse += dt * 0.005; }
drawShadow(ctx) { ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.ellipse(0, this.r, this.r, this.r*0.3, 0, 0, Math.PI*2); ctx.fill(); }
}
class HeartItem extends BaseItem {
constructor() { super(); this.type = 'life'; }
draw(ctx) {
ctx.save(); ctx.translate(this.x, this.y); this.drawShadow(ctx);
let scale = 1 + Math.sin(this.pulse) * 0.15; ctx.scale(scale, scale);
ctx.shadowColor = '#ff3366'; ctx.shadowBlur = 20; ctx.fillStyle = '#ff1493'; ctx.beginPath();
const topCurveHeight = this.r * 0.6; ctx.moveTo(0, topCurveHeight); ctx.bezierCurveTo(0, 0, -this.r, 0, -this.r, topCurveHeight); ctx.bezierCurveTo(-this.r, this.r, 0, this.r*1.3, 0, this.r*1.8); ctx.bezierCurveTo(0, this.r*1.3, this.r, this.r, this.r, topCurveHeight); ctx.bezierCurveTo(this.r, 0, 0, 0, 0, topCurveHeight); ctx.fill();
ctx.shadowBlur = 0; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.beginPath(); ctx.ellipse(-this.r*0.4, topCurveHeight*0.8, this.r*0.15, this.r*0.08, -Math.PI/4, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
}
class SlowItem extends BaseItem {
constructor() { super(); this.type = 'slow'; }
draw(ctx) {
ctx.save(); ctx.translate(this.x, this.y); this.drawShadow(ctx);
let scale = 1 + Math.sin(this.pulse) * 0.15; ctx.scale(scale, scale);
ctx.shadowColor = '#00bfff'; ctx.shadowBlur = 20; ctx.fillStyle = '#1e90ff'; ctx.beginPath();
const topCurveHeight = this.r * 0.6; ctx.moveTo(0, topCurveHeight); ctx.bezierCurveTo(0, 0, -this.r, 0, -this.r, topCurveHeight); ctx.bezierCurveTo(-this.r, this.r, 0, this.r*1.3, 0, this.r*1.8); ctx.bezierCurveTo(0, this.r*1.3, this.r, this.r, this.r, topCurveHeight); ctx.bezierCurveTo(this.r, 0, 0, 0, 0, topCurveHeight); ctx.fill();
ctx.shadowBlur = 0; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.beginPath(); ctx.ellipse(-this.r*0.4, topCurveHeight*0.8, this.r*0.15, this.r*0.08, -Math.PI/4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 16px "Segoe UI"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('S', 0, this.r * 0.8);
ctx.restore();
}
}
class StarItem extends BaseItem {
constructor() { super(); this.type = 'star'; }
update(dt, speed) { super.update(dt, speed); this.pulse += dt * 0.005; }
draw(ctx) {
ctx.save(); ctx.translate(this.x, this.y); this.drawShadow(ctx);
let scale = 1 + Math.sin(this.pulse) * 0.15; ctx.scale(scale, scale);
ctx.rotate(this.pulse);
ctx.shadowColor = '#ffff00'; ctx.shadowBlur = 20; ctx.fillStyle = '#ffd700'; ctx.beginPath();
let rot = Math.PI / 2 * 3; let step = Math.PI / 5; let outerRadius = this.r * 0.9; let innerRadius = this.r * 0.4;
ctx.moveTo(0, -outerRadius);
for (let i = 0; i < 5; i++) { ctx.lineTo(Math.cos(rot) * outerRadius, Math.sin(rot) * outerRadius); rot += step; ctx.lineTo(Math.cos(rot) * innerRadius, Math.sin(rot) * innerRadius); rot += step; }
ctx.lineTo(0, -outerRadius); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#ffffe0'; ctx.beginPath();
rot = Math.PI / 2 * 3; outerRadius = this.r * 0.4; innerRadius = this.r * 0.15;
ctx.moveTo(0, -outerRadius);
for (let i = 0; i < 5; i++) { ctx.lineTo(Math.cos(rot) * outerRadius, Math.sin(rot) * outerRadius); rot += step; ctx.lineTo(Math.cos(rot) * innerRadius, Math.sin(rot) * innerRadius); rot += step; }
ctx.lineTo(0, -outerRadius); ctx.closePath(); ctx.fill();
ctx.shadowBlur = 0; ctx.restore();
}
}
class GoalTape {
constructor() { this.y = -100; this.passed = false; }
update(dt, speed) { this.y += speed * (dt / 1000); }
draw(ctx) {
ctx.save(); ctx.translate(0, this.y);
let leftPoleX = canvas.width * 0.1; let rightPoleX = canvas.width * 0.9;
ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(leftPoleX + 5, 0, 30, 20); ctx.fillRect(rightPoleX + 5, 0, 30, 20);
let pGrad = ctx.createLinearGradient(leftPoleX - 10, 0, leftPoleX + 10, 0); pGrad.addColorStop(0, '#ccc'); pGrad.addColorStop(0.5, '#fff'); pGrad.addColorStop(1, '#999');
ctx.fillStyle = pGrad; ctx.fillRect(leftPoleX - 10, -150, 20, 170); ctx.fillRect(rightPoleX - 10, -150, 20, 170);
ctx.fillStyle = '#ff0033'; ctx.beginPath(); ctx.arc(leftPoleX, -150, 15, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(rightPoleX, -150, 15, 0, Math.PI*2); ctx.fill();
if (!this.passed) {
let tapeGrad = ctx.createLinearGradient(0, -50, 0, -30); tapeGrad.addColorStop(0, '#ff1a1a'); tapeGrad.addColorStop(0.5, '#ff4d4d'); tapeGrad.addColorStop(1, '#cc0000');
ctx.fillStyle = tapeGrad; ctx.fillRect(leftPoleX, -50, rightPoleX - leftPoleX, 20);
ctx.fillStyle = '#ffffff'; ctx.fillRect(leftPoleX, -42, rightPoleX - leftPoleX, 4);
ctx.fillStyle = '#ffffff'; ctx.font = '900 36px "Segoe UI", Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 5; ctx.shadowOffsetY = 2;
ctx.fillText("G O A L !!", canvas.width / 2, -40);
} else {
ctx.lineWidth = 20; ctx.strokeStyle = '#ff1a1a';
ctx.beginPath(); ctx.moveTo(leftPoleX, -40); ctx.quadraticCurveTo(leftPoleX + 80, -10, leftPoleX + 100, 60); ctx.stroke();
ctx.beginPath(); ctx.moveTo(rightPoleX, -40); ctx.quadraticCurveTo(rightPoleX - 80, -10, rightPoleX - 100, 60); ctx.stroke();
}
ctx.restore();
}
}
window.addEventListener('keydown', (e) => { if (e.code === 'ArrowLeft') keys.ArrowLeft = true; if (e.code === 'ArrowRight') keys.ArrowRight = true; });
window.addEventListener('keyup', (e) => { if (e.code === 'ArrowLeft') keys.ArrowLeft = false; if (e.code === 'ArrowRight') keys.ArrowRight = false; });
const handleBtnDown = (dir) => { if (dir === 'left') { touchLeft = true; document.getElementById('btnLeft').classList.add('active'); } if (dir === 'right') { touchRight = true; document.getElementById('btnRight').classList.add('active'); } };
const handleBtnUp = (dir) => { if (dir === 'left') { touchLeft = false; document.getElementById('btnLeft').classList.remove('active'); } if (dir === 'right') { touchRight = false; document.getElementById('btnRight').classList.remove('active'); } };
['mousedown', 'touchstart'].forEach(e => {
document.getElementById('btnLeft').addEventListener(e, (ev) => { ev.preventDefault(); handleBtnDown('left'); });
document.getElementById('btnRight').addEventListener(e, (ev) => { ev.preventDefault(); handleBtnDown('right'); });
});
['mouseup', 'touchend'].forEach(e => {
window.addEventListener(e, () => { handleBtnUp('left'); handleBtnUp('right'); });
});
function initGame() {
score = 0; lives = 3; gameSpeed = 350; obstacles = []; items = []; particles = []; speedLines = []; popEffects = []; goal = null;
nextItemScore = 1000; nextSlowItemScore = 450; nextStarItemScore = 500; nextEdgeObstacleScore = 300; slowEffectDistance = 0; invincibleDistance = 0;
player.x = canvas.width / 2; player.y = canvas.height - player.h * 0.6; player.invulnerable = 0; player.tilt = 0;
for(let i=0; i<120; i++) particles.push(new Particle(true));
updateUI(); gameState = 'PLAYING';
UI.screens.start.classList.add('hidden'); UI.screens.over.classList.add('hidden'); UI.screens.clear.classList.add('hidden');
lastTime = performance.now(); requestAnimationFrame(gameLoop);
}
function showStartScreen() {
gameState = 'START';
UI.screens.start.classList.remove('hidden'); UI.screens.over.classList.add('hidden'); UI.screens.clear.classList.add('hidden');
score = 0; obstacles = []; items = []; goal = null; player.x = canvas.width / 2; updateUI();
}
function updateUI() {
UI.score.innerText = Math.floor(score);
UI.lives.innerHTML = '';
for(let i=0; i<lives; i++) UI.lives.innerHTML += '<span>❤️</span>';
UI.progress.style.width = `${Math.min(100, (score / GOAL_DISTANCE) * 100)}%`;
}
// --- 背景スクロールの描画 ---
function drawBackground() {
// ベースのモスグリーン一色に固定
ctx.fillStyle = '#6b8e23';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// -------------------------------------------------------------
// 【改修】より魅力的で細やかな「赤い狐娘」の後ろ姿描画
// -------------------------------------------------------------
function drawPlayer() {
if (player.invulnerable > 0 && Math.floor(performance.now() / 100) % 2 === 0) return;
ctx.save();
ctx.translate(player.x, player.y);
ctx.rotate(player.tilt);
let t = performance.now() / 150; // アニメーションベース時間
let baseScale = player.w / 60;
ctx.scale(baseScale, baseScale);
// 足元を原点付近に調整
ctx.translate(0, -10);
// 1. 影
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.beginPath();
ctx.ellipse(0, 10, 20 + Math.sin(t*2)*2, 6, 0, 0, Math.PI*2);
ctx.fill();
let walkCycle = t * 1.5;
let bobY = Math.abs(Math.sin(walkCycle)) * 4; // 走る上下動
let legL = Math.sin(walkCycle) * 15;
let legR = Math.sin(walkCycle + Math.PI) * 15;
let armL = Math.sin(walkCycle + Math.PI) * 12;
let armR = Math.sin(walkCycle) * 12;
ctx.translate(0, -bobY);
// --- 腕の描画関数 ---
const drawArm = (dir) => {
// 肌(肩〜二の腕)
ctx.fillStyle = '#ffe0bd';
ctx.beginPath();
ctx.roundRect(-4.5, 0, 9, 15, 4);
ctx.fill();
// 赤いアームウォーマー
let armGrad = ctx.createLinearGradient(-4, 0, 4, 0);
armGrad.addColorStop(0, '#800000');
armGrad.addColorStop(0.3, '#ff4d4d'); // ハイライト
armGrad.addColorStop(0.6, '#d50000');
armGrad.addColorStop(1, '#600000');
ctx.fillStyle = armGrad;
ctx.beginPath();
ctx.roundRect(-4.5, 9, 9, 21, 3);
ctx.fill();
// アームウォーマーの強いハイライト(光沢)
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(-2, 11, 1.5, 17);
// MAZDAマーク (外側側面)
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(dir * 2.5, 13, 2.5, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 0.5;
ctx.beginPath(); // 簡易的なカモメマーク
ctx.moveTo(dir * 2.5 - 1.5, 13); ctx.lineTo(dir * 2.5, 14); ctx.lineTo(dir * 2.5 + 1.5, 13);
ctx.stroke();
// 手
ctx.fillStyle = '#ffe0bd';
ctx.beginPath();
ctx.arc(0, 32, 3.5, 0, Math.PI*2);
ctx.fill();
};
// 左腕 (奥)
ctx.save();
ctx.translate(-16, -65);
ctx.rotate(Math.PI/12 + armL * 0.05);
drawArm(-1);
ctx.restore();
// 右腕 (奥)
ctx.save();
ctx.translate(16, -65);
ctx.rotate(-Math.PI/12 + armR * 0.05);
drawArm(1);
ctx.restore();
// --- 脚の描画関数 ---
const drawLeg = (yOffset) => {
ctx.save();
ctx.translate(0, yOffset);
// 黒タイツ(テカリを強調したグラデーション)
let tightsGrad = ctx.createLinearGradient(-6, 0, 6, 0);
tightsGrad.addColorStop(0, '#0a0a0a');
tightsGrad.addColorStop(0.3, '#111111');
tightsGrad.addColorStop(0.6, '#888888'); // 強い光沢
tightsGrad.addColorStop(0.8, '#222222');
tightsGrad.addColorStop(1, '#050505');
ctx.fillStyle = tightsGrad;
ctx.beginPath();
ctx.moveTo(-7, -40);
ctx.quadraticCurveTo(-8, -20, -4, 0);
ctx.lineTo(4, 0);
ctx.quadraticCurveTo(8, -20, 7, -40);
ctx.fill();
// タイツのハイライト筋
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(3, -35); ctx.lineTo(2, -5); ctx.stroke();
// 未来的な赤いハイカットスニーカー
let shoeGrad = ctx.createLinearGradient(-6, 0, 6, 0);
shoeGrad.addColorStop(0, '#990000');
shoeGrad.addColorStop(0.5, '#ff3333');
shoeGrad.addColorStop(1, '#b30000');
ctx.fillStyle = shoeGrad;
ctx.beginPath();
ctx.moveTo(-6, -5); ctx.lineTo(6, -5); ctx.lineTo(7, 10); ctx.lineTo(-7, 10);
ctx.fill();
// スニーカーのハイライト
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.fillRect(2, -4, 2, 12);
// スニーカーのアクセント
ctx.fillStyle = '#ff1a1a';
ctx.fillRect(-4, -5, 8, 12);
// ソール
ctx.fillStyle = '#f0f0f0';
ctx.beginPath();
ctx.roundRect(-8, 10, 16, 4, 2);
ctx.fill();
// ソールの影
ctx.fillStyle = '#999';
ctx.fillRect(-8, 12, 16, 2);
// 踵の丸いパーツ(黒+銀縁・光沢あり)
ctx.fillStyle = '#e0e0e0'; // 銀縁
ctx.beginPath(); ctx.arc(0, 4, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#111'; // 黒丸
ctx.beginPath(); ctx.arc(0, 4, 3.5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.7)'; // パーツのハイライト
ctx.beginPath(); ctx.arc(-1, 3, 1, 0, Math.PI*2); ctx.fill();
ctx.restore();
};
// 左脚・右脚
ctx.save(); ctx.translate(-8, 0); drawLeg(legL); ctx.restore();
ctx.save(); ctx.translate(8, 0); drawLeg(legR); ctx.restore();
// --- 胴体 (背中と腰のくびれ) ---
ctx.fillStyle = '#ffe0bd';
ctx.beginPath();
ctx.moveTo(-12, -70); // 肩幅
ctx.quadraticCurveTo(-7, -50, -13, -40); // 腰へのライン
ctx.lineTo(13, -40);
ctx.quadraticCurveTo(7, -50, 12, -70);
ctx.fill();
// 背骨と肩甲骨のシャドウ(滑らかに)
let skinShadow = ctx.createLinearGradient(0, -70, 0, -40);
skinShadow.addColorStop(0, 'rgba(200, 130, 100, 0.4)');
skinShadow.addColorStop(1, 'rgba(200, 130, 100, 0.1)');
ctx.strokeStyle = skinShadow;
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(0, -70); ctx.lineTo(0, -45); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-4, -65); ctx.quadraticCurveTo(-2, -55, -5, -50); ctx.stroke();
ctx.beginPath(); ctx.moveTo(4, -65); ctx.quadraticCurveTo(2, -55, 5, -50); ctx.stroke();
// --- 衣装 (ビキニトップ・背中側 & チョーカー) ---
// アンダーバストのバンド(太めにしてMAZDAロゴを入れる)
ctx.fillStyle = '#d50000';
ctx.beginPath();
ctx.moveTo(-12, -54); ctx.quadraticCurveTo(0, -51, 12, -54);
ctx.lineTo(11, -49); ctx.quadraticCurveTo(0, -46, -11, -49);
ctx.fill();
// バンドのハイライト
ctx.strokeStyle = 'rgba(255, 100, 100, 0.8)'; ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(-10, -52); ctx.quadraticCurveTo(0, -49, 10, -52); ctx.stroke();
// MAZDA 文字 (背中のバンド)
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 3px Arial';
ctx.textAlign = 'center';
ctx.fillText('MAZDA', 0, -50);
// 結び目
ctx.fillStyle = '#ff1a1a';
ctx.beginPath(); ctx.arc(0, -51.5, 2.5, 0, Math.PI*2); ctx.fill();
// ホルターネックの紐 (首・チョーカーへ)
ctx.strokeStyle = '#d50000'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(-5, -70); ctx.lineTo(-2, -80); ctx.stroke();
ctx.beginPath(); ctx.moveTo(5, -70); ctx.lineTo(2, -80); ctx.stroke();
// チョーカー
ctx.fillStyle = '#b30000';
ctx.beginPath();
ctx.roundRect(-4.5, -82, 9, 4, 2);
ctx.fill();
// チョーカーのハイライト
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillRect(-3, -81.5, 6, 1);
// --- ミニスカート ---
ctx.save();
ctx.translate(0, -42);
// 走りに合わせてスカートが滑らかに波打つ
let skirtSway = Math.sin(t*2.5) * 2.5 - player.tilt * 10;
let skirtSway2 = Math.cos(t*2.5) * 1.5; // 波打ちの複雑化
let skirtGrad = ctx.createLinearGradient(0, 0, 0, 16);
skirtGrad.addColorStop(0, '#ff3333');
skirtGrad.addColorStop(0.5, '#cc0000');
skirtGrad.addColorStop(1, '#600000');
ctx.fillStyle = skirtGrad;
ctx.beginPath();
ctx.moveTo(-14, 0);
ctx.quadraticCurveTo(0, 3, 14, 0); // ウエストライン
ctx.lineTo(18 + skirtSway*0.6, 12 + skirtSway2); // 右裾
// 裾の波打ち(ベジェ曲線で柔らかく)
ctx.bezierCurveTo(8 + skirtSway*0.4, 16 + skirtSway2, -8 + skirtSway*0.4, 16 - skirtSway2, -18 + skirtSway*0.6, 12 - skirtSway2);
ctx.fill();
// プリーツの線(光沢と影)
ctx.strokeStyle = 'rgba(100, 0, 0, 0.6)'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-9, 1); ctx.lineTo(-12 + skirtSway*0.4, 13 - skirtSway2*0.5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, 2); ctx.lineTo(0 + skirtSway*0.4, 14); ctx.stroke();
ctx.beginPath(); ctx.moveTo(9, 1); ctx.lineTo(12 + skirtSway*0.4, 13 + skirtSway2*0.5); ctx.stroke();
// プリーツのハイライト
ctx.strokeStyle = 'rgba(255, 100, 100, 0.5)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-7, 2); ctx.lineTo(-10 + skirtSway*0.4, 13 - skirtSway2*0.5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(2, 2); ctx.lineTo(2 + skirtSway*0.4, 14); ctx.stroke();
ctx.restore();
// --- 尻尾 (手前のスカートから生える) ---
ctx.save();
ctx.translate(0, -35); // スカートの中間(手前)の位置に変更
// スカートから生えていることを強調するための根元のスリット/影
ctx.fillStyle = 'rgba(60, 0, 0, 0.6)';
ctx.beginPath();
ctx.ellipse(0, 1, 6, 3, 0, 0, Math.PI*2);
ctx.fill();
// しっぽの滑らかな揺れ(付け根と先で位相をずらすオーバーラッピング)
let tailBaseSway = Math.sin(t * 0.6) * 0.15 + player.tilt * 0.4;
let tailTipSway = Math.sin(t * 0.6 - Math.PI/4) * 0.2 + player.tilt * 0.6;
ctx.rotate(tailBaseSway);
let tailGrad = ctx.createLinearGradient(0, 0, -10, 70);
tailGrad.addColorStop(0, '#4d0000'); // 根元の濃い影
tailGrad.addColorStop(0.3, '#ff1a1a'); // 中間の鮮やかな赤
tailGrad.addColorStop(0.8, '#ffffff'); // 先は白
tailGrad.addColorStop(1, '#ffffff');
ctx.fillStyle = tailGrad;
ctx.beginPath();
ctx.moveTo(-5, 0); // 根元に幅を持たせてスカートに接している感を出す
// 右側のライン (下方向へ)
ctx.quadraticCurveTo(15, 20, 5 + tailTipSway*10, 45);
ctx.quadraticCurveTo(0 + tailTipSway*20, 75, -15 + tailTipSway*25, 75); // 先端
// 左側のライン (上へ戻る)
ctx.quadraticCurveTo(-35 + tailTipSway*15, 50, -20, 20);
ctx.quadraticCurveTo(-10, 5, 5, 0); // 根元を閉じる
ctx.fill();
// しっぽの艶感(強いハイライト)
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.beginPath();
// 三日月型のハイライト (下向きに調整)
ctx.moveTo(-10, 25);
ctx.quadraticCurveTo(-20, 45, -8 + tailTipSway*10, 60);
ctx.quadraticCurveTo(-15, 45, -5, 25);
ctx.fill();
// サブハイライト
ctx.fillStyle = 'rgba(255, 200, 200, 0.4)';
ctx.beginPath(); ctx.ellipse(-3, 40, 2, 8, -Math.PI/8 + tailTipSway*0.2, 0, Math.PI*2); ctx.fill();
ctx.restore();
// --- 頭部 ---
ctx.save();
ctx.translate(0, -82);
// 走りの振動で頭全体がわずかに揺れる
let headBob = Math.sin(walkCycle * 2) * 1.5;
ctx.translate(0, headBob * 0.5);
ctx.rotate(player.tilt * -0.2 + Math.sin(walkCycle)*0.05);
// 狐耳 (走るリズムに合わせてピコピコ動く)
let earTwitch = Math.sin(t * 4) * 0.1;
const drawEar = (isRight) => {
let sign = isRight ? 1 : -1;
ctx.save();
ctx.translate(sign * 10, -12);
ctx.rotate(sign * Math.PI / 8 + sign * earTwitch);
// 耳の外側 (赤)グラデーション
let earGrad = ctx.createLinearGradient(0, 0, 0, -20);
earGrad.addColorStop(0, '#990000');
earGrad.addColorStop(1, '#ff1a1a');
ctx.fillStyle = earGrad;
ctx.beginPath();
ctx.moveTo(-6, 0); ctx.lineTo(6, 0); ctx.lineTo(0, -22); ctx.fill();
// 耳の内側の毛並み (ピンク寄りの白)
ctx.fillStyle = '#fff0f0';
ctx.beginPath();
ctx.moveTo(-3, -2); ctx.lineTo(3, -2); ctx.lineTo(0, -17); ctx.fill();
// 耳のハイライト
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(sign*2, -5); ctx.lineTo(0, -20); ctx.stroke();
ctx.restore();
};
drawEar(false); drawEar(true);
// 後頭部の髪 (ショートボブ)
let hairGrad = ctx.createLinearGradient(0, -20, 0, 18);
hairGrad.addColorStop(0, '#ff4d4d'); // 頭頂部を明るく
hairGrad.addColorStop(0.4, '#d50000');
hairGrad.addColorStop(0.8, '#800000');
hairGrad.addColorStop(1, '#4d0000'); // 毛先は暗く
ctx.fillStyle = hairGrad;
ctx.beginPath();
ctx.arc(0, -3, 18, Math.PI * 0.9, Math.PI * 2.1); // 上半分
// 髪の毛先の揺れ(走りに合わせてふわりと動く)
let hairTipSway = Math.cos(walkCycle * 2) * 2;
ctx.bezierCurveTo(20, 5, 16 + hairTipSway, 15, 10 + hairTipSway*0.5, 17);
ctx.quadraticCurveTo(5, 14, 0, 16 + hairTipSway*0.2);
ctx.quadraticCurveTo(-5, 14, -10 - hairTipSway*0.5, 17);
ctx.bezierCurveTo(-16 - hairTipSway, 15, -20, 5, -18, -3);
ctx.fill();
// 艶々感の強調:天使の輪 (マルチレイヤーで輝きを表現)
// レイヤー1:ぼかしを効かせた広い光
ctx.strokeStyle = 'rgba(255, 100, 100, 0.4)';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(0, -7, 14, Math.PI * 1.1, Math.PI * 1.9);
ctx.stroke();
// レイヤー2:シャープなピンクのハイライト
ctx.strokeStyle = 'rgba(255, 180, 180, 0.8)';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(0, -7, 14, Math.PI * 1.15, Math.PI * 1.85);
ctx.stroke();
// レイヤー3:真っ白な芯
ctx.strokeStyle = 'rgba(255, 255, 255, 1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, -7, 14, Math.PI * 1.25, Math.PI * 1.45);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, -7, 14, Math.PI * 1.55, Math.PI * 1.75);
ctx.stroke();
// 光の粒(キラキラ)
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.beginPath(); ctx.arc(-8, -11, 1, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(6, -12, 1.5, 0, Math.PI*2); ctx.fill();
// 髪のディテール線(流れを強調)
ctx.strokeStyle = 'rgba(80, 0, 0, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, -18); ctx.quadraticCurveTo(3, 0, 0 + hairTipSway*0.2, 15); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-8, -14); ctx.quadraticCurveTo(-7, 0, -9 - hairTipSway*0.4, 15); ctx.stroke();
ctx.beginPath(); ctx.moveTo(8, -14); ctx.quadraticCurveTo(7, 0, 9 + hairTipSway*0.4, 15); ctx.stroke();
// 明るい毛束の線
ctx.strokeStyle = 'rgba(255, 100, 100, 0.4)';
ctx.beginPath(); ctx.moveTo(-4, -16); ctx.quadraticCurveTo(-2, 0, -3, 14); ctx.stroke();
ctx.beginPath(); ctx.moveTo(4, -16); ctx.quadraticCurveTo(2, 0, 3, 14); ctx.stroke();
ctx.restore(); // 頭部終了
ctx.restore(); // 全体終了
}
// -------------------------------------------------------------
function checkCollision(rect, circle) {
let hitBoxW = rect.w * 0.4; let hitBoxH = rect.h * 0.6;
let distX = Math.abs(circle.x - rect.x); let distY = Math.abs(circle.y - (rect.y - hitBoxH/2));
if (distX > (hitBoxW / 2 + circle.r) || distY > (hitBoxH / 2 + circle.r)) return false;
if (distX <= (hitBoxW / 2) || distY <= (hitBoxH / 2)) return true;
let dx = distX - hitBoxW / 2; let dy = distY - hitBoxH / 2;
return (dx * dx + dy * dy <= (circle.r * circle.r));
}
function update(dt) {
if (gameState !== 'PLAYING') return;
let dx = 0; if (keys.ArrowLeft || touchLeft) dx = -1; if (keys.ArrowRight || touchRight) dx = 1;
player.tilt += (dx * 0.25 - player.tilt) * 0.2;
player.x += dx * player.speed * (dt / 1000);
let minX = player.w * 0.5; let maxX = canvas.width - player.w * 0.5;
if (player.x < minX) player.x = minX; if (player.x > maxX) player.x = maxX;
if (player.invulnerable > 0) player.invulnerable -= dt;
let previousScore = score;
score += (gameSpeed * dt / 1000) * 0.1;
let baseSpeed = 350;
if (slowEffectDistance > 0) {
gameSpeed = baseSpeed * 0.6;
slowEffectDistance -= (score - previousScore);
if (slowEffectDistance < 0) slowEffectDistance = 0;
} else {
gameSpeed = baseSpeed;
}
if (invincibleDistance > 0) {
invincibleDistance -= (score - previousScore);
if (invincibleDistance < 0) invincibleDistance = 0;
}
player.speed = baseSpeed * 1.1;
if (score >= nextItemScore && !goal) { items.push(new HeartItem()); nextItemScore += 1000; }
if (score >= nextSlowItemScore && !goal) { items.push(new SlowItem()); nextSlowItemScore += 450; }
if (score >= nextStarItemScore && !goal) { items.push(new StarItem()); nextStarItemScore += 500; } // 500mおきに出現するよう固定
if (score >= nextEdgeObstacleScore && !goal) {
let edgePadding = 50;
obstacles.push(new Obstacle(edgePadding));
obstacles.push(new Obstacle(canvas.width - edgePadding));
nextEdgeObstacleScore += 300;
}
if (score >= GOAL_DISTANCE && !goal) goal = new GoalTape();
if (Math.random() < 0.8) particles.push(new Particle(false));
if (invincibleDistance > 0 && Math.random() < 0.5) particles.push(new Particle(false, player.x + (Math.random()-0.5)*player.w, player.y - Math.random()*player.h, 'star_sparkle'));
particles.forEach((p, i) => { p.update(dt, gameSpeed * 0.8); if (p.y > canvas.height + 20 || p.life <= 0) particles.splice(i, 1); });
if (Math.random() < 0.4) speedLines.push(new SpeedLine());
speedLines.forEach((line, i) => { line.update(dt, gameSpeed); if (line.y > canvas.height) speedLines.splice(i, 1); });
popEffects.forEach((pop, i) => { pop.update(dt); if(pop.life <= 0) popEffects.splice(i, 1); });
if (goal) {
goal.update(dt, gameSpeed);
if (!goal.passed && player.y < goal.y + 40) {
goal.passed = true; gameState = 'CLEAR'; UI.screens.clear.classList.remove('hidden');
createSakuraConfetti();
}
} else {
timeAccumulator += dt;
let spawnRate = 1200;
if (timeAccumulator > spawnRate) { obstacles.push(new Obstacle()); timeAccumulator = 0; }
}
// 障害物の処理(ループ内の安全な削除対応)
for (let i = obstacles.length - 1; i >= 0; i--) {
let obs = obstacles[i];
obs.update(dt, gameSpeed);
let isCollided = false;
if (checkCollision(player, obs)) {
if (invincibleDistance > 0) {
popEffects.push(new PopEffect(obs.x, obs.y, "CRASH!", "#ff5722"));
score += 100; // ボーナススコア
popEffects.push(new PopEffect(obs.x, obs.y - 30, "+100m", "#ffd700"));
for(let j=0; j<5; j++) particles.push(new Particle(false, obs.x, obs.y, 'break'));
obstacles.splice(i, 1);
isCollided = true;
} else if (player.invulnerable <= 0) {
lives--; player.invulnerable = 1500;
canvas.style.transform = `translate(${(Math.random()-0.5)*30}px, ${(Math.random()-0.5)*30}px)`;
setTimeout(() => canvas.style.transform = 'translate(0,0)', 150);
slowEffectDistance = 0;
if (lives <= 0) {
gameState = 'GAMEOVER';
UI.finalScore.innerText = Math.floor(score);
UI.screens.over.classList.remove('hidden');
}
}
}
if (!isCollided && obs.y > canvas.height + obs.r * 2) {
obstacles.splice(i, 1);
}
}
// アイテムの処理
for (let i = items.length - 1; i >= 0; i--) {
let item = items[i];
item.update(dt, gameSpeed);
let isItemCollected = false;
if (checkCollision(player, item)) {
if (item.type === 'life') {
lives = Math.min(5, lives + 1); player.invulnerable = 500;
popEffects.push(new PopEffect(item.x, item.y, "+1 LIFE", "#ff1493"));
} else if (item.type === 'slow') {
slowEffectDistance += 200;
popEffects.push(new PopEffect(item.x, item.y, "SLOW", "#1e90ff"));
} else if (item.type === 'star') {
invincibleDistance += 150;
popEffects.push(new PopEffect(item.x, item.y, "FEVER!!", "#ffd700"));
}
items.splice(i, 1);
isItemCollected = true;
}
if (!isItemCollected && item.y > canvas.height + item.r) {
items.splice(i, 1);
}
}
// 背景のスクロール速度の更新
bgScrollOffset += gameSpeed * (dt / 1000);
if (Math.floor(previousScore) !== Math.floor(score) || lives !== UI.lives.children.length) updateUI();
}
function draw() {
drawBackground(); // 追加: 流れる道路の描画
speedLines.forEach(line => line.draw(ctx));
particles.filter(p => p.type !== 'sakura' && p.type !== 'star_sparkle').forEach(p => p.draw(ctx));
if (goal) goal.draw(ctx);
obstacles.forEach(obs => obs.draw(ctx));
items.forEach(item => item.draw(ctx));
drawPlayer();
// 無敵オーラ
if (invincibleDistance > 0) {
ctx.save(); ctx.translate(player.x, player.y - player.h * 0.2);
let t = performance.now() / 100; ctx.rotate(t);
ctx.strokeStyle = `hsl(${(t * 200) % 360}, 100%, 60%)`; ctx.lineWidth = 6;
ctx.beginPath(); ctx.ellipse(0, 0, player.w * 0.8, player.h * 0.6, 0, 0, Math.PI * 2); ctx.stroke();
ctx.rotate(-t * 1.5); ctx.strokeStyle = '#ffffff'; ctx.setLineDash([15, 20]);
ctx.beginPath(); ctx.ellipse(0, 0, player.w * 0.9, player.h * 0.7, 0, 0, Math.PI * 2); ctx.stroke();
ctx.restore();
}
particles.filter(p => p.type === 'sakura' || p.type === 'star_sparkle').forEach(p => p.draw(ctx));
popEffects.forEach(pop => pop.draw(ctx));
let textY = 120;
if (slowEffectDistance > 0) {
ctx.fillStyle = 'rgba(0, 191, 255, 0.15)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#00ffff'; ctx.font = '900 28px "Segoe UI", Arial'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(0, 0, 255, 0.8)'; ctx.shadowBlur = 10;
ctx.fillText('SLOW DOWN', canvas.width / 2, textY);
ctx.font = 'bold 18px "Segoe UI", Arial'; ctx.fillText(`あと ${Math.ceil(slowEffectDistance)}m`, canvas.width / 2, textY + 30);
ctx.shadowBlur = 0; textY += 70;
}
if (invincibleDistance > 0) {
ctx.fillStyle = '#ffff00'; ctx.font = '900 28px "Segoe UI", Arial'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(255, 165, 0, 0.8)'; ctx.shadowBlur = 10;
ctx.fillText('INVINCIBLE', canvas.width / 2, textY);
ctx.font = 'bold 18px "Segoe UI", Arial'; ctx.fillText(`あと ${Math.ceil(invincibleDistance)}m`, canvas.width / 2, textY + 30);
ctx.shadowBlur = 0;
}
}
let lastDt = 0;
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
let dt = timestamp - lastTime; if (dt > 100) dt = 100; lastTime = timestamp; lastDt = dt;
update(dt); draw();
requestAnimationFrame(gameLoop);
}
document.getElementById('btnStart').addEventListener('click', initGame);
document.getElementById('btnRestart').addEventListener('click', initGame);
document.getElementById('btnPlayAgain').addEventListener('click', initGame);
document.getElementById('btnToTitle1').addEventListener('click', showStartScreen);
document.getElementById('btnToTitle2').addEventListener('click', showStartScreen);
function createSakuraConfetti() {
const container = document.getElementById('clearConfetti');
container.innerHTML = '';
for (let i = 0; i < 80; i++) {
let petal = document.createElement('div');
petal.classList.add('sakura-petal');
let size = Math.random() * 12 + 8;
petal.style.width = `${size}px`; petal.style.height = `${size}px`;
petal.style.left = `${Math.random() * 100}vw`; petal.style.top = `-${Math.random() * 20}vh`;
let duration = Math.random() * 3 + 3; let delay = Math.random() * 2;
petal.style.animationDuration = `${duration}s`; petal.style.animationDelay = `${delay}s`;
let isLight = Math.random() > 0.5;
petal.style.background = isLight ? '#ffdac1' : '#ffb7b2';
petal.style.boxShadow = "0 0 5px rgba(255, 183, 178, 0.4)";
container.appendChild(petal);
}
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>

