Neon Runner
by LaserBuilder68581 lines19.6 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#0b1020">
<title>Neon Runner</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: #0b1020;
touch-action: none;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
user-select: none;
overscroll-behavior: none;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
#game {
display: block;
position: fixed;
inset: 0;
width: 100%;
height: 100%;
touch-action: none;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
(function () {
"use strict";
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var W = 0, H = 0, DPR = 1, scale = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
canvas.style.width = W + "px";
canvas.style.height = H + "px";
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
scale = Math.max(0.65, Math.min(1.5, H / 720));
player.w = 42 * scale;
player.x = Math.max(50, Math.min(W * 0.18, 200));
player.h = 44 * scale;
}
function groundY() { return H * 0.78; }
var player = { x: 80, w: 42, h: 44, yOff: 0, vy: 0, onGround: true };
var state = {
running: false, over: false, t: 0, scroll: 0, speed: 0,
score: 0, best: 0, obstacles: [], coins: [], particles: [],
spawnTimer: 0, coinTimer: 0, shake: 0, restartLock: 0
};
try {
var saved = localStorage.getItem("neonRunnerBest");
if (saved) state.best = parseInt(saved, 10) || 0;
} catch (e) {}
function resetGame() {
state.obstacles = [];
state.coins = [];
state.particles = [];
state.t = 0;
state.speed = 400 * scale;
state.score = 0;
state.spawnTimer = 1.1;
state.coinTimer = 1.4;
state.shake = 0;
state.over = false;
state.running = true;
player.yOff = 0;
player.vy = 0;
player.onGround = true;
}
function gameOver() {
state.running = false;
state.over = true;
state.restartLock = 0.6;
state.shake = 16;
explode(player.x + player.w / 2, groundY() - player.yOff - player.h / 2);
var s = Math.floor(state.score);
if (s > state.best) {
state.best = s;
try { localStorage.setItem("neonRunnerBest", String(s)); } catch (e) {}
}
}
function jump() {
if (!state.running) return;
if (player.onGround) {
player.vy = 1080 * scale;
player.onGround = false;
dust(player.x + player.w / 2, groundY(), 6);
}
}
function addParticle(x, y, vx, vy, life, size, hue) {
if (state.particles.length > 260) return;
state.particles.push({ x: x, y: y, vx: vx, vy: vy, life: life, maxLife: life, size: size, hue: hue });
}
function dust(x, y, n) {
for (var i = 0; i < n; i++) {
var a = Math.PI + (Math.random() - 0.5) * Math.PI;
var sp = (80 + Math.random() * 180) * scale;
addParticle(x, y, Math.cos(a) * sp, Math.abs(Math.sin(a)) * sp * 0.8,
0.35 + Math.random() * 0.3, (2 + Math.random() * 3) * scale, 200 + Math.random() * 60);
}
}
function explode(x, y) {
for (var i = 0; i < 44; i++) {
var a = Math.random() * Math.PI * 2;
var sp = (120 + Math.random() * 460) * scale;
addParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
0.5 + Math.random() * 0.7, (2 + Math.random() * 5) * scale, Math.random() * 360);
}
}
function sparkle(x, y, hue) {
for (var i = 0; i < 12; i++) {
var a = Math.random() * Math.PI * 2;
var sp = (60 + Math.random() * 220) * scale;
addParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp,
0.3 + Math.random() * 0.35, (1.5 + Math.random() * 3) * scale, hue);
}
}
function updateParticles(dt) {
for (var i = state.particles.length - 1; i >= 0; i--) {
var p = state.particles[i];
p.life -= dt;
if (p.life <= 0) { state.particles.splice(i, 1); continue; }
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 1400 * scale * dt;
p.vx *= 0.985;
}
}
function spawnObstacle() {
var canFly = state.score > 140;
if (canFly && Math.random() < 0.28) {
var fw = 66 * scale, fh = 26 * scale;
state.obstacles.push({ type: "fly", x: W + fw, w: fw, h: fh, yOff: 32 * scale, hue: Math.random() * 360 });
return;
}
if (Math.random() < 0.55) {
var sw = 32 * scale, sh = (44 + Math.random() * 24) * scale;
state.obstacles.push({ type: "spike", x: W + sw, w: sw, h: sh, yOff: 0, hue: Math.random() * 360 });
} else {
var bw = (32 + Math.random() * 26) * scale, bh = (38 + Math.random() * 46) * scale;
state.obstacles.push({ type: "block", x: W + bw, w: bw, h: bh, yOff: 0, hue: Math.random() * 360 });
}
}
function spawnCoins() {
var n = 3 + Math.floor(Math.random() * 3);
var yOff = (58 + Math.random() * 92) * scale;
var startX = W + 50 * scale;
for (var i = 0; i < n; i++) {
state.coins.push({ x: startX + i * 40 * scale, yOff: yOff, r: 10 * scale, phase: Math.random() * 6.28 });
}
}
function boxOverlap(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
function playerBox() {
var pad = 5 * scale;
return { x: player.x + pad, y: groundY() - player.yOff - player.h + pad,
w: player.w - pad * 2, h: player.h - pad * 2 };
}
function obstacleBox(o) {
var pad = 4 * scale;
var bottom = groundY() - o.yOff;
return { x: o.x + pad, y: bottom - o.h + pad, w: o.w - pad * 2, h: o.h - pad * 2 };
}
function coinHit(c) {
var box = playerBox();
var cy = groundY() - c.yOff;
var nx = Math.max(box.x, Math.min(c.x, box.x + box.w));
var ny = Math.max(box.y, Math.min(cy, box.y + box.h));
var dx = c.x - nx, dy = cy - ny;
return dx * dx + dy * dy < c.r * c.r;
}
function update(dt) {
if (state.restartLock > 0) state.restartLock -= dt;
if (state.shake > 0) state.shake = Math.max(0, state.shake - dt * 34);
if (state.running) {
state.t += dt;
state.speed = Math.min(1050 * scale, state.speed + 14 * scale * dt);
state.scroll += state.speed * dt;
state.score += state.speed * dt * 0.022;
if (!player.onGround) {
player.vy -= 3200 * scale * dt;
player.yOff += player.vy * dt;
if (player.yOff <= 0) {
player.yOff = 0;
player.vy = 0;
player.onGround = true;
dust(player.x + player.w / 2, groundY(), 5);
}
}
state.spawnTimer -= dt;
if (state.spawnTimer <= 0) {
spawnObstacle();
var gap = (340 + Math.random() * 330) * scale;
state.spawnTimer = Math.max(gap / state.speed, 0.5);
}
state.coinTimer -= dt;
if (state.coinTimer <= 0) {
spawnCoins();
state.coinTimer = 1.6 + Math.random() * 2.4;
}
var i;
for (i = 0; i < state.obstacles.length; i++) state.obstacles[i].x -= state.speed * dt;
state.obstacles = state.obstacles.filter(function (o) { return o.x + o.w > -60; });
for (i = 0; i < state.coins.length; i++) {
state.coins[i].x -= state.speed * dt;
state.coins[i].phase += dt * 6;
}
state.coins = state.coins.filter(function (c) { return c.x + c.r > -40; });
var pb = playerBox();
for (i = 0; i < state.obstacles.length; i++) {
if (boxOverlap(pb, obstacleBox(state.obstacles[i]))) { gameOver(); break; }
}
if (state.running) {
for (i = state.coins.length - 1; i >= 0; i--) {
if (coinHit(state.coins[i])) {
var c = state.coins[i];
sparkle(c.x, groundY() - c.yOff, (state.t * 120) % 360);
state.score += 10;
state.coins.splice(i, 1);
}
}
}
} else {
state.t += dt;
state.scroll += 70 * dt;
}
updateParticles(dt);
}
function roundRect(x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawSky() {
var hue = (state.scroll * 0.02 + state.t * 7) % 360;
var g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "hsl(" + hue + ",65%,10%)");
g.addColorStop(0.45, "hsl(" + ((hue + 45) % 360) + ",72%,24%)");
g.addColorStop(1, "hsl(" + ((hue + 90) % 360) + ",78%,46%)");
ctx.fillStyle = g;
ctx.fillRect(-60, -60, W + 120, H + 120);
var sx = W * 0.74, sy = H * 0.26, sr = 62 * scale;
var sh = (hue + 60) % 360;
var sg = ctx.createRadialGradient(sx, sy, 0, sx, sy, sr * 3.2);
sg.addColorStop(0, "hsla(" + sh + ",100%,78%,0.95)");
sg.addColorStop(0.25, "hsla(" + sh + ",100%,62%,0.35)");
sg.addColorStop(1, "hsla(" + sh + ",100%,62%,0)");
ctx.fillStyle = sg;
ctx.beginPath();
ctx.arc(sx, sy, sr * 3.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "hsl(" + sh + ",100%,80%)";
ctx.beginPath();
ctx.arc(sx, sy, sr * 0.72, 0, Math.PI * 2);
ctx.fill();
}
function drawHills(factor, baseY, radius, hue, light, alpha) {
ctx.globalAlpha = alpha;
ctx.fillStyle = "hsl(" + hue + ",65%," + light + "%)";
var spacing = radius * 1.45;
var off = (state.scroll * factor) % spacing;
for (var x = -spacing - off; x < W + spacing * 2; x += spacing) {
ctx.beginPath();
ctx.arc(x, baseY, radius, Math.PI, 0);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawGround() {
var gy = groundY();
var hue = (state.scroll * 0.02 + state.t * 7) % 360;
var g = ctx.createLinearGradient(0, gy, 0, H);
g.addColorStop(0, "hsl(" + ((hue + 200) % 360) + ",55%,16%)");
g.addColorStop(1, "hsl(" + ((hue + 230) % 360) + ",60%,7%)");
ctx.fillStyle = g;
ctx.fillRect(-40, gy, W + 80, H - gy + 60);
ctx.save();
ctx.shadowColor = "hsl(" + ((hue + 90) % 360) + ",100%,65%)";
ctx.shadowBlur = 20 * scale;
ctx.fillStyle = "hsl(" + ((hue + 90) % 360) + ",100%,72%)";
ctx.fillRect(-40, gy - 2 * scale, W + 80, 3 * scale);
ctx.restore();
var spacing = 82 * scale;
var off = state.scroll % spacing;
ctx.fillStyle = "hsla(" + ((hue + 120) % 360) + ",100%,70%,0.35)";
for (var x = -off; x < W + spacing; x += spacing) {
ctx.fillRect(x, gy + 16 * scale, 42 * scale, 4 * scale);
}
}
function drawObstacles() {
var gy = groundY();
for (var i = 0; i < state.obstacles.length; i++) {
var o = state.obstacles[i];
var bottom = gy - o.yOff;
var top = bottom - o.h;
ctx.save();
ctx.shadowColor = "hsl(" + o.hue + ",100%,60%)";
ctx.shadowBlur = 18 * scale;
if (o.type === "spike") {
var g = ctx.createLinearGradient(0, top, 0, bottom);
g.addColorStop(0, "hsl(" + o.hue + ",100%,72%)");
g.addColorStop(1, "hsl(" + ((o.hue + 45) % 360) + ",90%,45%)");
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(o.x, bottom);
ctx.lineTo(o.x + o.w / 2, top);
ctx.lineTo(o.x + o.w, bottom);
ctx.closePath();
ctx.fill();
} else if (o.type === "block") {
var g2 = ctx.createLinearGradient(o.x, top, o.x + o.w, bottom);
g2.addColorStop(0, "hsl(" + o.hue + ",95%,68%)");
g2.addColorStop(1, "hsl(" + ((o.hue + 50) % 360) + ",90%,42%)");
ctx.fillStyle = g2;
roundRect(o.x, top, o.w, o.h, 7 * scale);
ctx.fill();
ctx.fillStyle = "hsla(0,0%,100%,0.35)";
roundRect(o.x + 5 * scale, top + 5 * scale, o.w - 10 * scale, 5 * scale, 3 * scale);
ctx.fill();
} else {
var g3 = ctx.createLinearGradient(0, top, 0, bottom);
g3.addColorStop(0, "hsl(" + o.hue + ",100%,75%)");
g3.addColorStop(1, "hsl(" + ((o.hue + 60) % 360) + ",95%,50%)");
ctx.fillStyle = g3;
roundRect(o.x, top, o.w, o.h, o.h / 2);
ctx.fill();
}
ctx.restore();
}
}
function drawCoins() {
var gy = groundY();
for (var i = 0; i < state.coins.length; i++) {
var c = state.coins[i];
var cy = gy - c.yOff + Math.sin(c.phase) * 5 * scale;
var hue = (state.t * 120 + i * 25) % 360;
ctx.save();
ctx.shadowColor = "hsl(" + hue + ",100%,65%)";
ctx.shadowBlur = 18 * scale;
var cg = ctx.createRadialGradient(c.x - 3 * scale, cy - 3 * scale, 1, c.x, cy, c.r);
cg.addColorStop(0, "hsl(" + hue + ",100%,88%)");
cg.addColorStop(1, "hsl(" + ((hue + 50) % 360) + ",100%,55%)");
ctx.fillStyle = cg;
ctx.beginPath();
ctx.arc(c.x, cy, c.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawPlayer() {
var gy = groundY();
var top = gy - player.yOff - player.h;
var hue = (state.t * 90) % 360;
ctx.save();
ctx.shadowColor = "hsl(" + hue + ",100%,62%)";
ctx.shadowBlur = 26 * scale;
var pg = ctx.createLinearGradient(player.x, top, player.x + player.w, top + player.h);
pg.addColorStop(0, "hsl(" + hue + ",100%,70%)");
pg.addColorStop(1, "hsl(" + ((hue + 70) % 360) + ",100%,55%)");
ctx.fillStyle = pg;
roundRect(player.x, top, player.w, player.h, 12 * scale);
ctx.fill();
ctx.restore();
var eyeY = top + player.h * 0.38;
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(player.x + player.w * 0.62, eyeY, 5 * scale, 0, Math.PI * 2);
ctx.arc(player.x + player.w * 0.88, eyeY, 4.2 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#101025";
ctx.beginPath();
ctx.arc(player.x + player.w * 0.66, eyeY + 1 * scale, 2.4 * scale, 0, Math.PI * 2);
ctx.arc(player.x + player.w * 0.91, eyeY + 1 * scale, 2 * scale, 0, Math.PI * 2);
ctx.fill();
}
function drawParticles() {
for (var i = 0; i < state.particles.length; i++) {
var p = state.particles[i];
var a = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = a;
ctx.fillStyle = "hsl(" + p.hue + ",100%,68%)";
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (0.4 + a * 0.9), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawHUD() {
var pad = Math.max(16, W * 0.05);
var y = pad + 24 * scale;
var fs = Math.max(20, 26 * scale);
ctx.save();
ctx.font = "800 " + fs + "px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.shadowColor = "rgba(0,0,0,0.55)";
ctx.shadowBlur = 8;
ctx.fillStyle = "#fff";
ctx.fillText(String(Math.floor(state.score)), pad, y);
ctx.textAlign = "right";
ctx.font = "600 " + (fs * 0.6) + "px system-ui, sans-serif";
ctx.fillStyle = "rgba(255,255,255,0.75)";
ctx.fillText("BEST " + state.best, W - pad, y);
ctx.restore();
}
function drawOverlay() {
if (state.running) return;
ctx.save();
ctx.fillStyle = "rgba(8,10,26,0.62)";
ctx.fillRect(0, 0, W, H);
var cx = W / 2, cy = H * 0.46;
var hue = (state.t * 60) % 360;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
if (state.over) {
ctx.font = "900 " + Math.max(34, 52 * scale) + "px system-ui, sans-serif";
ctx.fillStyle = "hsl(" + hue + ",100%,68%)";
ctx.shadowColor = "hsl(" + hue + ",100%,60%)";
ctx.shadowBlur = 26 * scale;
ctx.fillText("GAME OVER", cx, cy - 60 * scale);
ctx.shadowBlur = 0;
ctx.fillStyle = "#fff";
ctx.font = "800 " + Math.max(26, 38 * scale) + "px system-ui, sans-serif";
ctx.fillText("Score " + Math.floor(state.score), cx, cy + 4 * scale);
ctx.fillStyle = "rgba(255,255,255,0.72)";
ctx.font = "600 " + Math.max(15, 19 * scale) + "px system-ui, sans-serif";
ctx.fillText("Best " + state.best, cx, cy + 44 * scale);
ctx.fillStyle = "rgba(255,255,255," + (0.55 + 0.45 * Math.sin(state.t * 5)) + ")";
ctx.font = "700 " + Math.max(16, 20 * scale) + "px system-ui, sans-serif";
ctx.fillText("Tap or press Space to play again", cx, cy + 100 * scale);
} else {
ctx.font = "900 " + Math.max(36, 60 * scale) + "px system-ui, sans-serif";
var tg = ctx.createLinearGradient(cx - 180 * scale, 0, cx + 180 * scale, 0);
tg.addColorStop(0, "hsl(" + hue + ",100%,70%)");
tg.addColorStop(1, "hsl(" + ((hue + 90) % 360) + ",100%,68%)");
ctx.fillStyle = tg;
ctx.shadowColor = "hsl(" + hue + ",100%,60%)";
ctx.shadowBlur = 30 * scale;
ctx.fillText("NEON RUNNER", cx, cy - 70 * scale);
ctx.shadowBlur = 0;
ctx.fillStyle = "#fff";
ctx.font = "700 " + Math.max(17, 22 * scale) + "px system-ui, sans-serif";
ctx.fillText("Tap / Space to Start", cx, cy + 10 * scale);
ctx.fillStyle = "rgba(255,255,255,0.7)";
ctx.font = "600 " + Math.max(14, 17 * scale) + "px system-ui, sans-serif";
ctx.fillText("Tap = Jump • Desktop: Space", cx, cy + 56 * scale);
}
ctx.restore();
}
function render() {
ctx.save();
if (state.shake > 0.3) {
ctx.translate((Math.random() - 0.5) * state.shake, (Math.random() - 0.5) * state.shake);
}
var hue = (state.scroll * 0.02 + state.t * 7) % 360;
var gy = groundY();
drawSky();
drawHills(0.10, gy + 30 * scale, 160 * scale, (hue + 210) % 360, 30, 0.85);
drawHills(0.22, gy + 20 * scale, 110 * scale, (hue + 250) % 360, 22, 0.9);
drawHills(0.42, gy + 12 * scale, 70 * scale, (hue + 290) % 360, 15, 0.95);
drawGround();
drawCoins();
drawObstacles();
drawPlayer();
drawParticles();
ctx.restore();
drawHUD();
drawOverlay();
}
canvas.addEventListener("touchstart", function (e) {
e.preventDefault();
if (!state.running) { if (state.restartLock <= 0) resetGame(); return; }
jump();
}, { passive: false });
canvas.addEventListener("mousedown", function (e) {
e.preventDefault();
if (!state.running) { if (state.restartLock <= 0) resetGame(); return; }
jump();
});
window.addEventListener("keydown", function (e) {
if (e.code === "Space" || e.code === "ArrowUp" || e.code === "KeyW") {
e.preventDefault();
if (!state.running) { if (state.restartLock <= 0) resetGame(); return; }
jump();
}
});
window.addEventListener("resize", resize);
window.addEventListener("orientationchange", function () { setTimeout(resize, 120); });
var last = 0;
function loop(now) {
if (!last) last = now;
var dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05;
if (dt < 0) dt = 0;
update(dt);
render();
requestAnimationFrame(loop);
}
resize();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: Neon Runner
Creator: LaserBuilder68
Libraries: none
Complexity: complex (581 lines, 19.6 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: neon-runner-laserbuilder68" to link back to the original. Then publish at arcadelab.ai/publish.