<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Star Runner</title>
<style>
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
overflow: hidden;
color: white;
background: #070b20;
font-family: system-ui, sans-serif;
}
#game {
width: min(92vw, 800px);
height: min(82vh, 560px);
border: 2px solid #6474ff;
border-radius: 16px;
background: linear-gradient(#10173c, #080b1d);
box-shadow: 0 0 40px #3949ab66;
touch-action: none;
}
#instructions {
position: fixed;
bottom: 12px;
margin: 0;
padding: 8px 12px;
color: #bec6ff;
text-align: center;
font-size: 14px;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<p id="instructions">
Move with WASD, arrow keys, mouse, or touch. Collect stars. Avoid enemies.
</p>
<script>
"use strict";
const canvas = document.querySelector("#game");
const ctx = canvas.getContext("2d");
const state = {
running: true,
score: 0,
best: Number(localStorage.getItem("starRunnerBest") || 0),
time: 0,
keys: new Set(),
pointer: null,
enemies: [],
particles: []
};
const player = {
x: 100,
y: 100,
radius: 16,
speed: 260
};
const star = {
x: 300,
y: 200,
radius: 12
};
function resize() {
const rect = canvas.getBoundingClientRect();
const scale = window.devicePixelRatio || 1;
canvas.width = Math.round(rect.width * scale);
canvas.height = Math.round(rect.height * scale);
ctx.setTransform(scale, 0, 0, scale, 0, 0);
}
function width() {
return canvas.clientWidth;
}
function height() {
return canvas.clientHeight;
}
function random(min, max) {
return Math.random() * (max - min) + min;
}
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function placeStar() {
star.x = random(30, width() - 30);
star.y = random(60, height() - 30);
}
function addEnemy() {
const side = Math.floor(Math.random() * 4);
let x;
let y;
if (side === 0) {
x = -25;
y = random(0, height());
} else if (side === 1) {
x = width() + 25;
y = random(0, height());
} else if (side === 2) {
x = random(0, width());
y = -25;
} else {
x = random(0, width());
y = height() + 25;
}
state.enemies.push({
x,
y,
radius: random(12, 21),
speed: random(75, 120) + state.score * 2
});
}
function burst(x, y, color, count = 14) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = random(50, 180);
state.particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1,
color
});
}
}
function reset() {
state.running = true;
state.score = 0;
state.time = 0;
state.enemies = [];
state.particles = [];
state.pointer = null;
player.x = width() / 2;
player.y = height() / 2;
placeStar();
addEnemy();
}
function gameOver() {
state.running = false;
state.best = Math.max(state.best, state.score);
localStorage.setItem("starRunnerBest", String(state.best));
burst(player.x, player.y, "#ff496c", 35);
}
function update(dt) {
updateParticles(dt);
if (!state.running) {
return;
}
state.time += dt;
let dx = 0;
let dy = 0;
if (state.keys.has("arrowleft") || state.keys.has("a")) dx -= 1;
if (state.keys.has("arrowright") || state.keys.has("d")) dx += 1;
if (state.keys.has("arrowup") || state.keys.has("w")) dy -= 1;
if (state.keys.has("arrowdown") || state.keys.has("s")) dy += 1;
if (dx || dy) {
const length = Math.hypot(dx, dy);
player.x += (dx / length) * player.speed * dt;
player.y += (dy / length) * player.speed * dt;
} else if (state.pointer) {
const pointerDistance = Math.hypot(
state.pointer.x - player.x,
state.pointer.y - player.y
);
if (pointerDistance > 5) {
const movement = Math.min(player.speed * dt, pointerDistance);
player.x += ((state.pointer.x - player.x) / pointerDistance) * movement;
player.y += ((state.pointer.y - player.y) / pointerDistance) * movement;
}
}
player.x = Math.max(player.radius, Math.min(width() - player.radius, player.x));
player.y = Math.max(45 + player.radius, Math.min(height() - player.radius, player.y));
if (distance(player, star) < player.radius + star.radius) {
state.score++;
burst(star.x, star.y, "#ffd84d");
placeStar();
if (state.score % 2 === 0) {
addEnemy();
}
}
for (const enemy of state.enemies) {
const angle = Math.atan2(player.y - enemy.y, player.x - enemy.x);
enemy.x += Math.cos(angle) * enemy.speed * dt;
enemy.y += Math.sin(angle) * enemy.speed * dt;
if (distance(player, enemy) < player.radius + enemy.radius) {
gameOver();
break;
}
}
}
function updateParticles(dt) {
for (const particle of state.particles) {
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
particle.vx *= 0.97;
particle.vy *= 0.97;
particle.life -= dt * 1.8;
}
state.particles = state.particles.filter(particle => particle.life > 0);
}
function drawBackground() {
ctx.fillStyle = "#090d25";
ctx.fillRect(0, 0, width(), height());
ctx.fillStyle = "#ffffff22";
for (let i = 0; i < 60; i++) {
const x = (i * 97) % width();
const y = (i * 53) % height();
ctx.fillRect(x, y, 2, 2);
}
ctx.strokeStyle = "#6574ff22";
ctx.lineWidth = 1;
for (let x = 0; x < width(); x += 40) {
ctx.beginPath();
ctx.moveTo(x, 45);
ctx.lineTo(x, height());
ctx.stroke();
}
for (let y = 45; y < height(); y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width(), y);
ctx.stroke();
}
}
function drawStar(x, y, outerRadius, innerRadius, color) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(state.time * 2);
ctx.beginPath();
for (let i = 0; i < 10; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const angle = -Math.PI / 2 + i * Math.PI / 5;
const px = Math.cos(angle) * radius;
const py = Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 20;
ctx.fill();
ctx.restore();
}
function draw() {
drawBackground();
ctx.fillStyle = "#131a45dd";
ctx.fillRect(0, 0, width(), 45);
ctx.font = "bold 18px system-ui";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
ctx.fillText(`Score: ${state.score}`, 16, 23);
ctx.textAlign = "right";
ctx.fillStyle = "#aeb8ff";
ctx.fillText(`Best: ${state.best}`, width() - 16, 23);
ctx.textAlign = "left";
drawStar(star.x, star.y, star.radius, 5, "#ffd84d");
for (const enemy of state.enemies) {
ctx.beginPath();
ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI * 2);
ctx.fillStyle = "#ff385f";
ctx.shadowColor = "#ff385f";
ctx.shadowBlur = 15;
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "#5e0b25";
ctx.fillRect(enemy.x - 7, enemy.y - 4, 4, 4);
ctx.fillRect(enemy.x + 3, enemy.y - 4, 4, 4);
}
if (state.running) {
ctx.beginPath();
ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2);
ctx.fillStyle = "#55d9ff";
ctx.shadowColor = "#55d9ff";
ctx.shadowBlur = 20;
ctx.fill();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(player.x - 5, player.y - 4, 3, 0, Math.PI * 2);
ctx.arc(player.x + 5, player.y - 4, 3, 0, Math.PI * 2);
ctx.fillStyle = "#07162b";
ctx.fill();
}
for (const particle of state.particles) {
ctx.globalAlpha = particle.life;
ctx.fillStyle = particle.color;
ctx.fillRect(particle.x - 3, particle.y - 3, 6, 6);
}
ctx.globalAlpha = 1;
if (!state.running) {
ctx.fillStyle = "#050817cc";
ctx.fillRect(0, 45, width(), height() - 45);
ctx.textAlign = "center";
ctx.fillStyle = "white";
ctx.font = "bold 46px system-ui";
ctx.fillText("GAME OVER", width() / 2, height() / 2 - 35);
ctx.font = "22px system-ui";
ctx.fillStyle = "#ffd84d";
ctx.fillText(`Score: ${state.score}`, width() / 2, height() / 2 + 10);
ctx.font = "18px system-ui";
ctx.fillStyle = "#c9ceff";
ctx.fillText(
"Press Space or tap to restart",
width() / 2,
height() / 2 + 52
);
ctx.textAlign = "left";
}
}
function pointerPosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
window.addEventListener("keydown", event => {
const key = event.key.toLowerCase();
state.keys.add(key);
if (key === " " && !state.running) {
reset();
}
if (key.startsWith("arrow") || key === " ") {
event.preventDefault();
}
});
window.addEventListener("keyup", event => {
state.keys.delete(event.key.toLowerCase());
});
canvas.addEventListener("pointerdown", event => {
if (!state.running) {
reset();
return;
}
state.pointer = pointerPosition(event);
canvas.setPointerCapture(event.pointerId);
});
canvas.addEventListener("pointermove", event => {
if (event.buttons || event.pointerType === "touch") {
state.pointer = pointerPosition(event);
}
});
canvas.addEventListener("pointerup", () => {
state.pointer = null;
});
window.addEventListener("resize", resize);
resize();
reset();
let previousTime = performance.now();
function loop(currentTime) {
const dt = Math.min((currentTime - previousTime) / 1000, 0.033);
previousTime = currentTime;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>3 views