Neon Catch
by AtomicGlider60457 lines12.0 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Neon Catch</title>
<style>
html { height: 100%; }
body {
margin: 0; height: 100%; overflow: hidden;
background: #080616; color: #fff;
font-family: "Trebuchet MS", system-ui, sans-serif;
user-select: none; touch-action: manipulation;
}
#game { position: fixed; inset: 0; width: 100%; height: 100%; cursor: crosshair; }
#hud {
position: fixed; top: 0; left: 0; right: 0;
display: flex; justify-content: space-between; align-items: center;
padding: 14px 24px; letter-spacing: 3px; pointer-events: none;
text-shadow: 0 0 12px #ff2d75;
}
#hud b { font-size: 1.9rem; margin-left: 6px; }
#combo { font-size: 1.4rem; font-weight: bold; color: #ffd60a; text-shadow: 0 0 14px #ffd60a; }
#combo.pulse { animation: pulse 0.25s ease-out; }
#time.low { color: #ff2d75; animation: blink 0.5s infinite; }
@keyframes pulse { 0% { transform: scale(1.6); } 100% { transform: scale(1); } }
@keyframes blink { 50% { opacity: 0.3; } }
#overlay {
position: fixed; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 16px; padding: 20px; text-align: center;
background: radial-gradient(circle, rgba(25, 12, 60, 0.88), rgba(5, 3, 15, 0.96));
transition: opacity 0.3s;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
h1 {
margin: 0; font-size: clamp(2.5rem, 10vw, 5rem); letter-spacing: 8px; color: #fff;
text-shadow: 0 0 20px #ff2d75, 0 0 50px #ff2d75;
}
h1 span { color: #22d3ee; text-shadow: 0 0 20px #22d3ee, 0 0 50px #22d3ee; }
#msg { max-width: 460px; margin: 0; font-size: 1.1rem; line-height: 1.5; color: #cbd5e1; }
.legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.legend span {
padding: 6px 14px; border: 1px solid var(--c); border-radius: 999px;
color: var(--c); box-shadow: 0 0 12px var(--c); font-weight: bold;
}
#start {
margin-top: 6px; padding: 14px 48px; font-size: 1.3rem; font-weight: bold;
letter-spacing: 4px; color: #fff; background: transparent;
border: 2px solid #22d3ee; border-radius: 10px;
box-shadow: 0 0 20px #22d3ee, inset 0 0 20px rgba(34, 211, 238, 0.3);
cursor: pointer; transition: transform 0.15s, background 0.15s;
}
#start:hover { background: rgba(34, 211, 238, 0.25); transform: scale(1.06); }
#best { margin: 0; color: #ffd60a; letter-spacing: 2px; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
<div>SCORE <b id="score">0</b></div>
<div id="combo">x1</div>
<div>TIME <b id="time">30</b></div>
</div>
<div id="overlay">
<h1>NEON<span>CATCH</span></h1>
<p id="msg">Tap the glowing orbs before they fade. Build combos for a bigger multiplier!</p>
<div class="legend">
<span style="--c:#ff2d75">โ +1</span>
<span style="--c:#ffd60a">โ
+5</span>
<span style="--c:#22ff9a">+ +2 sec</span>
<span style="--c:#8b5cf6">โ AVOID</span>
</div>
<button id="start">START</button>
<p id="best"></p>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const $ = (id) => document.getElementById(id);
const scoreEl = $("score");
const timeEl = $("time");
const comboEl = $("combo");
const overlay = $("overlay");
const msg = $("msg");
const bestEl = $("best");
const startBtn = $("start");
let W, H, dpr;
function resize() {
dpr = window.devicePixelRatio || 1;
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();
const TYPES = {
normal: { color: "#ff2d75", life: 2.2, r: 34, label: "" },
gold: { color: "#ffd60a", life: 1.1, r: 26, label: "โ
" },
time: { color: "#22ff9a", life: 1.8, r: 30, label: "+" },
bomb: { color: "#8b5cf6", life: 2.6, r: 36, label: "โ" },
};
function pickType() {
const n = Math.random();
if (n < 0.08) return "gold";
if (n < 0.15) return "time";
if (n < 0.33) return "bomb";
return "normal";
}
let running = false;
let score = 0;
let time = 30;
let elapsed = 0;
let combo = 0;
let spawnT = 0;
let shake = 0;
let flash = 0;
let last = 0;
let targets = [];
let parts = [];
let pops = [];
let trail = [];
let best = 0;
try {
best = +localStorage.getItem("neon-best") || 0;
} catch (e) {}
if (best) bestEl.textContent = "BEST: " + best;
const stars = Array.from({ length: 130 }, () => ({
x: Math.random(),
y: Math.random(),
z: Math.random() * 0.9 + 0.1,
}));
const mult = () => Math.min(8, 1 + Math.floor(combo / 5));
let ac;
function beep(freq, dur, type = "sine", vol = 0.07, slide = 0) {
if (!ac) return;
const o = ac.createOscillator();
const g = ac.createGain();
const t = ac.currentTime;
o.type = type;
o.frequency.setValueAtTime(freq, t);
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), t + dur);
g.gain.setValueAtTime(vol, t);
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
o.connect(g).connect(ac.destination);
o.start();
o.stop(t + dur);
}
function spawn() {
const type = pickType();
const def = TYPES[type];
const r = def.r;
const a = Math.random() * Math.PI * 2;
const sp = 30 + Math.random() * 50 + elapsed * 2;
targets.push({
type,
r,
x: r + Math.random() * Math.max(1, W - 2 * r),
y: 80 + r + Math.random() * Math.max(1, H - 80 - 2 * r),
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp,
age: 0,
life: def.life * Math.max(0.55, 1 - elapsed * 0.012),
});
}
function burst(x, y, color, n = 22) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const s = 80 + Math.random() * 300;
parts.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s,
life: 0.5 + Math.random() * 0.5,
age: 0,
color,
});
}
}
function pop(x, y, text, color) {
pops.push({ x, y, text, color, age: 0 });
}
function updateHud() {
scoreEl.textContent = score;
timeEl.textContent = Math.ceil(time);
timeEl.classList.toggle("low", time <= 5);
}
function updateCombo() {
comboEl.textContent = combo > 1 ? `x${mult()} ยท ${combo} STREAK` : "x1";
comboEl.classList.remove("pulse");
void comboEl.offsetWidth;
comboEl.classList.add("pulse");
}
function startGame() {
if (!ac) {
try { ac = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
}
score = 0;
time = 30;
elapsed = 0;
combo = 0;
spawnT = 0;
targets = [];
parts = [];
pops = [];
running = true;
updateHud();
updateCombo();
overlay.classList.add("hidden");
beep(330, 0.25, "triangle", 0.08, 400);
}
function endGame() {
running = false;
const newBest = score > best;
if (newBest) {
best = score;
try { localStorage.setItem("neon-best", best); } catch (e) {}
}
const rank =
score < 20 ? "Rookie" : score < 50 ? "Gamer" : score < 90 ? "Pro" : "Legend";
msg.innerHTML =
`TIME'S UP!<br><span style="font-size:2.2rem;color:#ffd60a">${score}</span><br>` +
`Rank: <b>${rank}</b>${newBest ? " ยท NEW BEST!" : ""}`;
bestEl.textContent = "BEST: " + best;
startBtn.textContent = "PLAY AGAIN";
overlay.classList.remove("hidden");
beep(220, 0.6, "sawtooth", 0.08, -150);
}
function collect(t, i) {
targets.splice(i, 1);
const c = TYPES[t.type].color;
if (t.type === "bomb") {
time -= 3;
combo = 0;
shake = 18;
flash = 0.35;
burst(t.x, t.y, c, 45);
pop(t.x, t.y, "-3s", c);
beep(90, 0.35, "sawtooth", 0.1, -50);
} else if (t.type === "time") {
time += 2;
combo++;
burst(t.x, t.y, c, 26);
pop(t.x, t.y, "+2s", c);
beep(660, 0.2, "sine", 0.08, 400);
} else {
const pts = (t.type === "gold" ? 5 : 1) * mult();
score += pts;
combo++;
shake = t.type === "gold" ? 8 : 3;
burst(t.x, t.y, c, t.type === "gold" ? 40 : 22);
pop(t.x, t.y, "+" + pts, c);
beep(440 + Math.min(combo, 20) * 30, 0.12, "triangle", 0.08);
}
updateCombo();
updateHud();
}
canvas.addEventListener("pointerdown", (e) => {
if (!running) return;
for (let i = targets.length - 1; i >= 0; i--) {
const t = targets[i];
if (Math.hypot(e.clientX - t.x, e.clientY - t.y) <= t.r * 1.2) {
collect(t, i);
return;
}
}
if (combo > 0) {
combo = 0;
updateCombo();
}
beep(120, 0.1, "square", 0.04);
});
window.addEventListener("pointermove", (e) => {
trail.push({ x: e.clientX, y: e.clientY, t: performance.now() });
if (trail.length > 25) trail.shift();
});
startBtn.addEventListener("click", startGame);
function update(dt) {
elapsed += dt;
time -= dt;
spawnT -= dt;
if (spawnT <= 0) {
spawn();
spawnT = Math.max(0.3, 0.85 - elapsed * 0.014);
}
for (let i = targets.length - 1; i >= 0; i--) {
const t = targets[i];
t.age += dt;
t.x += t.vx * dt;
t.y += t.vy * dt;
if (t.x < t.r || t.x > W - t.r) t.vx *= -1;
if (t.y < 70 + t.r || t.y > H - t.r) t.vy *= -1;
if (t.age >= t.life) {
if (t.type === "normal" && combo > 0) {
combo = 0;
updateCombo();
}
burst(t.x, t.y, "#556", 6);
targets.splice(i, 1);
}
}
updateHud();
if (time <= 0) {
time = 0;
updateHud();
endGame();
}
}
function updateFx(dt) {
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
p.age += dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.97;
p.vy *= 0.97;
if (p.age >= p.life) parts.splice(i, 1);
}
for (let i = pops.length - 1; i >= 0; i--) {
pops[i].age += dt;
pops[i].y -= 60 * dt;
if (pops[i].age > 0.8) pops.splice(i, 1);
}
stars.forEach((s) => {
s.x -= s.z * dt * 0.02;
if (s.x < 0) s.x = 1;
});
shake *= 0.88;
flash *= 0.9;
}
function draw() {
ctx.fillStyle = "rgba(8, 6, 22, 0.35)";
ctx.fillRect(0, 0, W, H);
stars.forEach((s) => {
ctx.fillStyle = `rgba(180, 200, 255, ${s.z * 0.8})`;
ctx.fillRect(s.x * W, s.y * H, s.z * 2, s.z * 2);
});
ctx.save();
if (shake > 0.5) ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
targets.forEach((t) => {
const def = TYPES[t.type];
const k = Math.min(1, t.age / 0.15);
const r = t.r * (1 - Math.pow(1 - k, 3));
const left = 1 - t.age / t.life;
ctx.shadowColor = def.color;
ctx.shadowBlur = 25;
ctx.fillStyle = def.color;
ctx.beginPath();
ctx.arc(t.x, t.y, r * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 4;
ctx.strokeStyle = def.color;
ctx.beginPath();
ctx.arc(t.x, t.y, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * left);
ctx.stroke();
ctx.shadowBlur = 0;
if (def.label) {
ctx.fillStyle = "#0b0820";
ctx.font = `bold ${r * 0.8}px sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(def.label, t.x, t.y + 1);
}
});
ctx.globalCompositeOperation = "lighter";
parts.forEach((p) => {
const a = 1 - p.age / p.life;
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, 3 * a + 1, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
pops.forEach((p) => {
ctx.globalAlpha = 1 - p.age / 0.8;
ctx.fillStyle = p.color;
ctx.shadowColor = p.color;
ctx.shadowBlur = 12;
ctx.font = "bold 26px sans-serif";
ctx.textAlign = "center";
ctx.fillText(p.text, p.x, p.y - 30);
});
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
ctx.restore();
const now = performance.now();
ctx.lineCap = "round";
for (let i = 1; i < trail.length; i++) {
const age = now - trail[i].t;
if (age > 250) continue;
ctx.strokeStyle = `rgba(34, 211, 238, ${1 - age / 250})`;
ctx.lineWidth = 4 * (1 - age / 250);
ctx.beginPath();
ctx.moveTo(trail[i - 1].x, trail[i - 1].y);
ctx.lineTo(trail[i].x, trail[i].y);
ctx.stroke();
}
if (flash > 0.01) {
ctx.fillStyle = `rgba(139, 92, 246, ${flash})`;
ctx.fillRect(0, 0, W, H);
}
}
function loop(ts) {
const dt = Math.min(0.05, (ts - last) / 1000 || 0);
last = ts;
if (running) update(dt);
updateFx(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>Game Source: Neon Catch
Creator: AtomicGlider60
Libraries: none
Complexity: complex (457 lines, 12.0 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-catch-atomicglider60" to link back to the original. Then publish at arcadelab.ai/publish.