🏃 Subway Surfers - HTML
by RocketGlider811136 lines38.4 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>🏃 Subway Surfers - HTML</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a1a;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', Arial, sans-serif;
overflow: hidden;
touch-action: none;
user-select: none;
}
.game-wrapper {
background: linear-gradient(145deg, #1a1a2e, #16213e);
border-radius: 24px;
padding: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(0, 150, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.06);
}
canvas {
display: block;
width: 100%;
max-width: 420px;
height: auto;
aspect-ratio: 9 / 16;
border-radius: 16px;
background: #0f0f23;
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.6);
touch-action: none;
cursor: pointer;
image-rendering: pixelated;
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 8px;
}
.score-display {
color: #fff;
font-size: 18px;
font-weight: 700;
text-shadow: 0 0 20px rgba(0, 200, 255, 0.3);
background: rgba(0, 0, 0, 0.4);
padding: 6px 18px;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(4px);
}
.score-display span {
color: #ffd700;
}
.hint {
color: rgba(255, 255, 255, 0.35);
font-size: 12px;
letter-spacing: 0.5px;
background: rgba(0, 0, 0, 0.3);
padding: 6px 14px;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.04);
}
.hint i {
font-style: normal;
display: inline-block;
margin: 0 4px;
}
@media (max-width: 480px) {
.game-wrapper {
padding: 10px;
border-radius: 16px;
}
.score-display {
font-size: 15px;
padding: 4px 14px;
}
.hint {
font-size: 10px;
padding: 4px 10px;
}
}
/* Mobile touch buttons overlay */
.touch-controls {
display: none;
position: absolute;
bottom: 30px;
left: 0;
right: 0;
justify-content: space-between;
padding: 0 20px;
pointer-events: none;
}
.touch-btn {
width: 64px;
height: 64px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: 2px solid rgba(255, 255, 255, 0.12);
color: #fff;
font-size: 28px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
backdrop-filter: blur(8px);
transition: all 0.1s;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.touch-btn:active {
transform: scale(0.88);
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.3);
}
.touch-btn.left {
margin-right: auto;
}
.touch-btn.right {
margin-left: auto;
}
.touch-btn.jump {
margin: 0 auto;
width: 76px;
height: 76px;
font-size: 32px;
background: rgba(0, 200, 255, 0.12);
border-color: rgba(0, 200, 255, 0.2);
}
@media (pointer: coarse) {
.touch-controls {
display: flex;
}
.hint {
display: none;
}
}
</style>
</head>
<body>
<div class="game-wrapper" style="position:relative;">
<canvas id="gameCanvas" width="450" height="800"></canvas>
<div class="controls">
<div class="score-display">🏆 <span id="scoreDisplay">0</span></div>
<div class="hint">← ↑ → | <i>🖱️ swipe</i></div>
</div>
<!-- Touch controls for mobile -->
<div class="touch-controls" id="touchControls">
<button class="touch-btn left" id="btnLeft">◀</button>
<button class="touch-btn jump" id="btnJump">▲</button>
<button class="touch-btn right" id="btnRight">▶</button>
</div>
</div>
<script>
// ============================================================
// 🚇 SUBWAY SURFERS — HTML5 Canvas Edition
// A 3D endless runner with perspective projection.
// Controls: Arrow Keys / WASD / Swipe / Touch buttons
// ============================================================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
// ---- Sizing ----
const W = 450,
H = 800;
canvas.width = W;
canvas.height = H;
// ---- Game Constants ----
const LANE_COUNT = 3;
const LANE_WIDTH = 100;
const PLAYER_WIDTH = 36;
const PLAYER_DEPTH = 30;
const ROAD_OFFSET_X = (W - LANE_WIDTH * LANE_COUNT) / 2;
// ---- 3D Projection ----
const FOV = 600;
const CAMERA_Y = -120;
const VANISH_Y = 80;
// ---- Game State ----
let player = { x: 1, z: 0, y: 0, targetX: 1, vy: 0, jumping: false, grounded: true };
let obstacles = [];
let coins = [];
let score = 0;
let highScore = parseInt(localStorage.getItem('subwayHighScore')) || 0;
let gameOver = false;
let gameStarted = false;
let frameCount = 0;
let speed = 6.5;
let spawnTimer = 0;
let coinTimer = 0;
let shakeAmount = 0;
let particles = [];
// ---- Input ----
const keys = { left: false, right: false, jump: false };
let swipeStartX = 0;
let swipeStartY = 0;
// ---- DOM refs ----
const btnLeft = document.getElementById('btnLeft');
const btnRight = document.getElementById('btnRight');
const btnJump = document.getElementById('btnJump');
// ============================================================
// HELPERS
// ============================================================
function projectPoint(x, y, z) {
// 3D → 2D with perspective
const scale = FOV / (FOV + z);
const px = W / 2 + (x - W / 2) * scale;
const py = VANISH_Y + (y - VANISH_Y) * scale;
return { x: px, y: py, scale: scale };
}
function laneToX(lane) {
return ROAD_OFFSET_X + lane * LANE_WIDTH + LANE_WIDTH / 2;
}
function randomLane() {
return Math.floor(Math.random() * LANE_COUNT);
}
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
function dist3D(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.z - b.z) ** 2);
}
// ============================================================
// PARTICLE SYSTEM
// ============================================================
function spawnParticles(x, y, z, color, count = 15) {
for (let i = 0; i < count; i++) {
particles.push({
x,
y,
z,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8 - 2,
vz: (Math.random() - 0.5) * 6,
life: 1,
decay: 0.015 + Math.random() * 0.025,
size: 4 + Math.random() * 8,
color: color || '#ffd700',
});
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.z += p.vz;
p.vy += 0.15;
p.life -= p.decay;
if (p.life <= 0 || p.z > 300) {
particles.splice(i, 1);
}
}
}
function drawParticles() {
for (const p of particles) {
const proj = projectPoint(p.x, p.y, p.z);
const size = p.size * proj.scale;
const alpha = p.life * 0.9;
ctx.globalAlpha = alpha;
ctx.shadowColor = p.color;
ctx.shadowBlur = 12;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(proj.x, proj.y, Math.max(size, 1), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
}
// ============================================================
// OBSTACLES
// ============================================================
function spawnObstacle() {
const lane = randomLane();
const type = Math.random() < 0.35 ? 'train' : 'barrier';
const x = laneToX(lane);
obstacles.push({
x: x,
z: -250 - Math.random() * 100,
lane: lane,
type: type,
width: type === 'train' ? 80 : 55,
depth: type === 'train' ? 60 : 40,
height: type === 'train' ? 70 : 45,
color: type === 'train' ? '#e74c3c' : '#f39c12',
hit: false,
});
}
function spawnCoin() {
const lane = randomLane();
const x = laneToX(lane);
coins.push({
x: x + (Math.random() - 0.5) * 40,
z: -200 - Math.random() * 150,
y: -10 + Math.random() * 20,
radius: 14,
collected: false,
bobPhase: Math.random() * Math.PI * 2,
});
}
// ============================================================
// PLAYER
// ============================================================
function resetPlayer() {
player.x = laneToX(1);
player.targetX = laneToX(1);
player.z = 0;
player.y = 0;
player.vy = 0;
player.jumping = false;
player.grounded = true;
}
function jumpPlayer() {
if (player.grounded && !gameOver) {
player.vy = -11;
player.jumping = true;
player.grounded = false;
spawnParticles(player.x, 20, player.z, '#88ddff', 8);
}
}
// ============================================================
// COLLISION
// ============================================================
function checkCollisions() {
const pX = player.x;
const pZ = player.z;
const pY = player.y;
// Obstacles
for (const obs of obstacles) {
if (obs.hit) continue;
const halfW = obs.width / 2;
const halfD = obs.depth / 2;
const dx = Math.abs(pX - obs.x);
const dz = Math.abs(pZ - obs.z);
if (dx < halfW + PLAYER_WIDTH / 2 && dz < halfD + PLAYER_DEPTH / 2) {
// Check if player is above (jumping over)
if (pY > obs.height - 10) {
// Jumped over! (bonus points)
if (!obs.hit) {
obs.hit = true;
score += 5;
spawnParticles(obs.x, obs.height / 2, obs.z, '#4ecdc4', 10);
updateScore();
}
continue;
}
// Collision!
gameOver = true;
shakeAmount = 12;
spawnParticles(pX, pY, pZ, '#ff4444', 30);
return;
}
}
// Coins
for (const coin of coins) {
if (coin.collected) continue;
const dx = Math.abs(pX - coin.x);
const dz = Math.abs(pZ - coin.z);
const dy = Math.abs(pY - coin.y);
if (dx < 30 && dz < 30 && dy < 35) {
coin.collected = true;
score += 10;
spawnParticles(coin.x, coin.y, coin.z, '#ffd700', 12);
updateScore();
}
}
}
// ============================================================
// SCORE
// ============================================================
function updateScore() {
scoreSpan.textContent = Math.floor(score);
}
// ============================================================
// DRAWING
// ============================================================
function drawRoad() {
// Road with perspective
const segments = 40;
const segLen = 18;
const roadLeft = ROAD_OFFSET_X;
const roadRight = ROAD_OFFSET_X + LANE_WIDTH * LANE_COUNT;
for (let i = 0; i < segments; i++) {
const z1 = -i * segLen;
const z2 = -(i + 1) * segLen;
const p1 = projectPoint(roadLeft, 0, z1);
const p2 = projectPoint(roadRight, 0, z1);
const p3 = projectPoint(roadRight, 0, z2);
const p4 = projectPoint(roadLeft, 0, z2);
const brightness = 0.25 + 0.75 * (1 - i / segments);
const isEven = i % 2 === 0;
ctx.fillStyle = isEven ?
`rgba(60, 70, 90, ${brightness * 0.5})` :
`rgba(80, 90, 110, ${brightness * 0.4})`;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineTo(p3.x, p3.y);
ctx.lineTo(p4.x, p4.y);
ctx.closePath();
ctx.fill();
// Lane markers
if (i % 2 === 0) {
for (let lane = 1; lane < LANE_COUNT; lane++) {
const lx = roadLeft + lane * LANE_WIDTH;
const lp1 = projectPoint(lx, 0, z1);
const lp2 = projectPoint(lx, 0, z2);
ctx.strokeStyle = `rgba(255,255,255,${0.08 * brightness})`;
ctx.lineWidth = 2 * p1.scale;
ctx.beginPath();
ctx.moveTo(lp1.x, lp1.y);
ctx.lineTo(lp2.x, lp2.y);
ctx.stroke();
}
}
}
// Road edges glow
const grad = ctx.createLinearGradient(0, 0, W, 0);
grad.addColorStop(0, 'rgba(0,0,0,0.4)');
grad.addColorStop(0.15, 'rgba(0,0,0,0)');
grad.addColorStop(0.85, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.4)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawObstacle(obs) {
const z = obs.z;
const x = obs.x;
const w = obs.width;
const d = obs.depth;
const h = obs.height;
// Bottom
const p1 = projectPoint(x - w / 2, 0, z - d / 2);
const p2 = projectPoint(x + w / 2, 0, z - d / 2);
const p3 = projectPoint(x + w / 2, 0, z + d / 2);
const p4 = projectPoint(x - w / 2, 0, z + d / 2);
// Top
const p1t = projectPoint(x - w / 2, h, z - d / 2);
const p2t = projectPoint(x + w / 2, h, z - d / 2);
const p3t = projectPoint(x + w / 2, h, z + d / 2);
const p4t = projectPoint(x - w / 2, h, z + d / 2);
const color = obs.type === 'train' ? '#e74c3c' : '#f39c12';
const darkColor = obs.type === 'train' ? '#a93226' : '#d68910';
ctx.shadowColor = color;
ctx.shadowBlur = 15 * Math.min(1, 1 + z / 100);
// Draw as 3D box
const drawFace = (pts, fill) => {
ctx.fillStyle = fill;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) {
ctx.lineTo(pts[i].x, pts[i].y);
}
ctx.closePath();
ctx.fill();
};
// Sides
drawFace([p1, p2, p2t, p1t], darkColor);
drawFace([p2, p3, p3t, p2t], color);
drawFace([p3, p4, p4t, p3t], darkColor);
drawFace([p4, p1, p1t, p4t], color);
// Top
drawFace([p1t, p2t, p3t, p4t], '#fff5e6');
// Detail: windows for train
if (obs.type === 'train') {
const winColor = 'rgba(200,230,255,0.4)';
const winW = w * 0.35;
const winH = h * 0.25;
const wy = h * 0.4;
for (let side = -1; side <= 1; side += 2) {
const wx = x + side * w * 0.2;
const pWin1 = projectPoint(wx - winW / 2, wy, z - d / 2 + 2);
const pWin2 = projectPoint(wx + winW / 2, wy, z - d / 2 + 2);
const pWin3 = projectPoint(wx + winW / 2, wy + winH, z - d / 2 + 2);
const pWin4 = projectPoint(wx - winW / 2, wy + winH, z - d / 2 + 2);
drawFace([pWin1, pWin2, pWin3, pWin4], winColor);
}
}
ctx.shadowBlur = 0;
}
function drawCoin(coin) {
const bob = Math.sin(coin.bobPhase + frameCount * 0.04) * 5;
const y = coin.y + bob;
const proj = projectPoint(coin.x, y, coin.z);
const radius = coin.radius * proj.scale;
if (radius < 1) return;
const alpha = 0.8 + 0.2 * Math.sin(frameCount * 0.06 + coin.bobPhase);
ctx.shadowColor = '#ffd700';
ctx.shadowBlur = 20 * proj.scale;
// Glow
const grad = ctx.createRadialGradient(proj.x, proj.y, 0, proj.x, proj.y, radius * 1.8);
grad.addColorStop(0, `rgba(255,215,0,${alpha * 0.3})`);
grad.addColorStop(1, 'rgba(255,215,0,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(proj.x, proj.y, radius * 1.8, 0, Math.PI * 2);
ctx.fill();
// Coin body
const grad2 = ctx.createRadialGradient(
proj.x - radius * 0.3, proj.y - radius * 0.3, 0,
proj.x, proj.y, radius
);
grad2.addColorStop(0, '#ffe066');
grad2.addColorStop(0.6, '#ffd700');
grad2.addColorStop(1, '#b8860b');
ctx.fillStyle = grad2;
ctx.beginPath();
ctx.arc(proj.x, proj.y, radius, 0, Math.PI * 2);
ctx.fill();
// Inner ring
ctx.strokeStyle = `rgba(255,255,200,${alpha * 0.5})`;
ctx.lineWidth = 2 * proj.scale;
ctx.beginPath();
ctx.arc(proj.x, proj.y, radius * 0.55, 0, Math.PI * 2);
ctx.stroke();
// Star symbol
ctx.fillStyle = `rgba(255,255,200,${alpha * 0.7})`;
ctx.font = `${radius * 0.7}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('★', proj.x, proj.y + 1);
ctx.shadowBlur = 0;
}
function drawPlayer() {
const p = player;
const proj = projectPoint(p.x, p.y, p.z);
const scale = proj.scale;
const w = PLAYER_WIDTH * scale;
const h = 50 * scale;
// Shadow
const shadowProj = projectPoint(p.x, -2, p.z);
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 20 * scale;
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(shadowProj.x, shadowProj.y + 2, w * 0.5, w * 0.15, 0, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Body
const bodyColor = '#3498db';
const bodyDark = '#2171a5';
// Torso
ctx.shadowColor = '#3498db';
ctx.shadowBlur = 20 * scale;
// Main body (rounded rect)
const rx = proj.x - w / 2;
const ry = proj.y - h;
const rw = w;
const rh = h * 0.6;
const corner = 6 * scale;
ctx.fillStyle = bodyColor;
ctx.beginPath();
ctx.moveTo(rx + corner, ry);
ctx.lineTo(rx + rw - corner, ry);
ctx.quadraticCurveTo(rx + rw, ry, rx + rw, ry + corner);
ctx.lineTo(rx + rw, ry + rh - corner);
ctx.quadraticCurveTo(rx + rw, ry + rh, rx + rw - corner, ry + rh);
ctx.lineTo(rx + corner, ry + rh);
ctx.quadraticCurveTo(rx, ry + rh, rx, ry + rh - corner);
ctx.lineTo(rx, ry + corner);
ctx.quadraticCurveTo(rx, ry, rx + corner, ry);
ctx.closePath();
ctx.fill();
// Head
const headR = w * 0.38;
const headY = ry - headR * 0.5;
ctx.fillStyle = '#f5cba7';
ctx.beginPath();
ctx.arc(proj.x, headY, headR, 0, Math.PI * 2);
ctx.fill();
// Hair / cap
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.ellipse(proj.x, headY - headR * 0.2, headR * 0.9, headR * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
// Cap brim
ctx.fillStyle = '#c0392b';
ctx.fillRect(proj.x - headR * 0.7, headY - headR * 0.1, headR * 1.4, headR * 0.15);
// Eyes
ctx.fillStyle = '#2c3e50';
const eyeOff = headR * 0.3;
const eyeR = headR * 0.15;
ctx.beginPath();
ctx.arc(proj.x - eyeOff, headY - headR * 0.05, eyeR, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(proj.x + eyeOff, headY - headR * 0.05, eyeR, 0, Math.PI * 2);
ctx.fill();
// Eye shine
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(proj.x - eyeOff + eyeR * 0.4, headY - headR * 0.15, eyeR * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(proj.x + eyeOff + eyeR * 0.4, headY - headR * 0.15, eyeR * 0.4, 0, Math.PI * 2);
ctx.fill();
// Legs
ctx.shadowBlur = 0;
ctx.fillStyle = '#2c3e50';
const legW = w * 0.25;
const legH = h * 0.35;
const legOffset = w * 0.2;
const bob = player.jumping ? Math.sin(frameCount * 0.3) * 4 * scale : 0;
ctx.fillRect(proj.x - legOffset - legW / 2, proj.y - legH + bob, legW, legH);
ctx.fillRect(proj.x + legOffset - legW / 2, proj.y - legH - bob, legW, legH);
// Shoes
ctx.fillStyle = '#e74c3c';
ctx.fillRect(proj.x - legOffset - legW / 2 - 2 * scale, proj.y - 2 * scale + bob, legW + 4 * scale, 4 * scale);
ctx.fillRect(proj.x + legOffset - legW / 2 - 2 * scale, proj.y - 2 * scale - bob, legW + 4 * scale, 4 * scale);
ctx.shadowBlur = 0;
}
function drawUI() {
// Game Over overlay
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#ff4444';
ctx.shadowBlur = 40;
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 52px Arial';
ctx.fillText('💥 GAME OVER', W / 2, H / 2 - 60);
ctx.shadowBlur = 0;
ctx.fillStyle = '#fff';
ctx.font = '24px Arial';
ctx.fillText(`Score: ${Math.floor(score)}`, W / 2, H / 2 + 20);
if (score > highScore) {
highScore = Math.floor(score);
localStorage.setItem('subwayHighScore', highScore);
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 22px Arial';
ctx.fillText('🏆 NEW BEST!', W / 2, H / 2 + 70);
} else {
ctx.fillStyle = '#aaa';
ctx.font = '18px Arial';
ctx.fillText(`Best: ${highScore}`, W / 2, H / 2 + 70);
}
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = '20px Arial';
ctx.fillText('🔄 Tap or press Space to restart', W / 2, H / 2 + 140);
// Glow pulse
const pulse = 0.5 + 0.5 * Math.sin(frameCount * 0.04);
ctx.shadowColor = `rgba(255,255,255,${pulse * 0.3})`;
ctx.shadowBlur = 30;
ctx.strokeStyle = `rgba(255,255,255,${pulse * 0.15})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(W / 2, H / 2 + 140, 60 + pulse * 10, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 0;
return;
}
// High score display
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillStyle = 'rgba(255,215,0,0.5)';
ctx.font = '14px Arial';
ctx.fillText(`🏆 ${highScore}`, W - 16, 12);
// Speed indicator
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillStyle = 'rgba(255,255,255,0.25)';
ctx.font = '12px Arial';
const speedKmh = Math.floor(speed * 3.5 + 20);
ctx.fillText(`⚡ ${speedKmh} km/h`, 12, 12);
}
// ============================================================
// GAME LOOP
// ============================================================
function update() {
if (gameOver) {
updateParticles();
if (shakeAmount > 0) shakeAmount *= 0.92;
if (shakeAmount < 0.1) shakeAmount = 0;
return;
}
frameCount++;
// ---- Speed increase ----
speed += 0.002;
speed = Math.min(speed, 14);
// ---- Player movement ----
if (keys.left && player.targetX > laneToX(0)) {
player.targetX -= LANE_WIDTH;
keys.left = false;
}
if (keys.right && player.targetX < laneToX(LANE_COUNT - 1)) {
player.targetX += LANE_WIDTH;
keys.right = false;
}
// Smooth lane movement
player.x = lerp(player.x, player.targetX, 0.18);
// Jump physics
if (player.jumping) {
player.vy += 0.55;
player.y += player.vy;
if (player.y >= 0) {
player.y = 0;
player.vy = 0;
player.jumping = false;
player.grounded = true;
spawnParticles(player.x, 2, player.z, '#88ddff', 6);
}
}
// ---- Spawn obstacles ----
spawnTimer++;
const spawnInterval = Math.max(20, 55 - speed * 1.5);
if (spawnTimer > spawnInterval + Math.random() * 30) {
spawnObstacle();
// Sometimes spawn two at once
if (Math.random() < 0.2 && speed > 8) {
spawnObstacle();
}
spawnTimer = 0;
}
// ---- Spawn coins ----
coinTimer++;
if (coinTimer > 12 + Math.random() * 20) {
spawnCoin();
if (Math.random() < 0.3) spawnCoin();
coinTimer = 0;
}
// ---- Move obstacles ----
for (let i = obstacles.length - 1; i >= 0; i--) {
obstacles[i].z += speed;
if (obstacles[i].z > 350) {
obstacles.splice(i, 1);
}
}
// ---- Move coins ----
for (let i = coins.length - 1; i >= 0; i--) {
coins[i].z += speed;
if (coins[i].z > 350) {
coins.splice(i, 1);
}
}
// ---- Collisions ----
checkCollisions();
// ---- Score ----
score += 0.15;
updateScore();
// ---- Particles ----
updateParticles();
// ---- Shake decay ----
if (shakeAmount > 0) shakeAmount *= 0.9;
if (shakeAmount < 0.05) shakeAmount = 0;
}
function draw() {
ctx.save();
// Screen shake
if (shakeAmount > 0.5) {
const sx = (Math.random() - 0.5) * shakeAmount * 1.2;
const sy = (Math.random() - 0.5) * shakeAmount * 1.2;
ctx.translate(sx, sy);
}
// Clear
ctx.clearRect(-10, -10, W + 20, H + 20);
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#0a0e27');
skyGrad.addColorStop(0.3, '#141c3a');
skyGrad.addColorStop(0.6, '#1a2a4a');
skyGrad.addColorStop(1, '#0f1a2e');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
ctx.fillStyle = 'rgba(255,255,255,0.15)';
const seed = 12345;
for (let i = 0; i < 40; i++) {
const sx = ((i * 137 + 31) % W);
const sy = ((i * 251 + 71) % (H * 0.6));
const size = ((i * 7) % 3) + 1;
const twinkle = 0.5 + 0.5 * Math.sin(frameCount * 0.02 + i * 1.7);
ctx.globalAlpha = 0.2 * twinkle;
ctx.fillRect(sx, sy, size, size);
}
ctx.globalAlpha = 1;
// ---- Draw road ----
drawRoad();
// ---- Draw coins ----
// Sort by z for proper depth
const sortedCoins = [...coins].filter(c => !c.collected).sort((a, b) => a.z - b.z);
for (const coin of sortedCoins) {
if (coin.z > -50) drawCoin(coin);
}
// ---- Draw obstacles ----
const sortedObs = [...obstacles].sort((a, b) => a.z - b.z);
for (const obs of sortedObs) {
if (obs.z > -30) drawObstacle(obs);
}
// ---- Draw particles ----
drawParticles();
// ---- Draw player ----
drawPlayer();
// ---- UI ----
drawUI();
ctx.restore();
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ============================================================
// RESTART
// ============================================================
function restartGame() {
obstacles = [];
coins = [];
particles = [];
score = 0;
speed = 6.5;
spawnTimer = 0;
coinTimer = 0;
frameCount = 0;
shakeAmount = 0;
gameOver = false;
resetPlayer();
updateScore();
}
// ============================================================
// INPUT HANDLING
// ============================================================
// ---- Keyboard ----
document.addEventListener('keydown', (e) => {
if (gameOver) {
if (e.key === ' ' || e.key === 'Space' || e.key === 'Enter') {
e.preventDefault();
restartGame();
}
return;
}
switch (e.key) {
case 'ArrowLeft':
case 'a':
case 'A':
e.preventDefault();
keys.left = true;
break;
case 'ArrowRight':
case 'd':
case 'D':
e.preventDefault();
keys.right = true;
break;
case 'ArrowUp':
case 'w':
case 'W':
case ' ':
case 'Space':
e.preventDefault();
jumpPlayer();
break;
}
});
// ---- Touch / Swipe ----
let touchStartX = 0,
touchStartY = 0;
let isTouching = false;
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = (touch.clientX - rect.left) / rect.width * W;
const y = (touch.clientY - rect.top) / rect.height * H;
touchStartX = x;
touchStartY = y;
isTouching = true;
if (gameOver) {
restartGame();
return;
}
// Check if tap is in lower half → jump
if (y > H * 0.6) {
jumpPlayer();
}
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!isTouching || gameOver) return;
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = (touch.clientX - rect.left) / rect.width * W;
const dx = x - touchStartX;
if (dx > 35 && player.targetX < laneToX(LANE_COUNT - 1)) {
player.targetX += LANE_WIDTH;
touchStartX = x;
} else if (dx < -35 && player.targetX > laneToX(0)) {
player.targetX -= LANE_WIDTH;
touchStartX = x;
}
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
isTouching = false;
}, { passive: false });
// ---- Mouse click (for desktop) ----
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width * W;
const y = (e.clientY - rect.top) / rect.height * H;
if (gameOver) {
restartGame();
return;
}
// Click lower half → jump
if (y > H * 0.55) {
jumpPlayer();
} else {
// Click left/right half → move
if (x < W / 2 && player.targetX > laneToX(0)) {
player.targetX -= LANE_WIDTH;
} else if (x >= W / 2 && player.targetX < laneToX(LANE_COUNT - 1)) {
player.targetX += LANE_WIDTH;
}
}
});
// ---- Mobile buttons ----
btnLeft.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameOver) { restartGame(); return; }
if (player.targetX > laneToX(0)) player.targetX -= LANE_WIDTH;
}, { passive: false });
btnLeft.addEventListener('mousedown', (e) => {
e.preventDefault();
if (gameOver) { restartGame(); return; }
if (player.targetX > laneToX(0)) player.targetX -= LANE_WIDTH;
});
btnRight.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameOver) { restartGame(); return; }
if (player.targetX < laneToX(LANE_COUNT - 1)) player.targetX += LANE_WIDTH;
}, { passive: false });
btnRight.addEventListener('mousedown', (e) => {
e.preventDefault();
if (gameOver) { restartGame(); return; }
if (player.targetX < laneToX(LANE_COUNT - 1)) player.targetX += LANE_WIDTH;
});
btnJump.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameOver) { restartGame(); return; }
jumpPlayer();
}, { passive: false });
btnJump.addEventListener('mousedown', (e) => {
e.preventDefault();
if (gameOver) { restartGame(); return; }
jumpPlayer();
});
// ---- Prevent page scroll ----
document.addEventListener('touchmove', (e) => {
if (e.target.closest('.game-wrapper')) e.preventDefault();
}, { passive: false });
// ---- Handle resize ----
function resizeCanvas() {
const wrapper = canvas.parentElement;
const maxWidth = Math.min(window.innerWidth - 32, 420);
const ratio = 9 / 16;
let w = maxWidth;
let h = w / ratio;
if (h > window.innerHeight - 120) {
h = window.innerHeight - 120;
w = h * ratio;
}
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// ============================================================
// INIT
// ============================================================
resetPlayer();
updateScore();
// Spawn initial coins
for (let i = 0; i < 6; i++) {
setTimeout(() => spawnCoin(), i * 200);
}
gameLoop();
console.log('🏃 Subway Surfers loaded!');
console.log('Controls: Arrow keys / WASD / Swipe / Click');
console.log(`🏆 High Score: ${highScore}`);
</script>
</body>
</html>Game Source: 🏃 Subway Surfers - HTML
Creator: RocketGlider81
Libraries: none
Complexity: complex (1136 lines, 38.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: subway-surfers-html-rocketglider81" to link back to the original. Then publish at arcadelab.ai/publish.