Neon Snake
by SolarScout64773 lines23.7 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Snake</title>
<style>
:root{
--bg:#0b0f1a;
--text:#e8eefc;
--muted:#9fb0d6;
--accent:#7cf7ff;
--accent2:#8b7bff;
--danger:#ff4d6d;
}
html,body{height:100%;margin:0;background:radial-gradient(1200px 800px at 50% 30%, #121b33 0%, var(--bg) 55%, #070a12 100%); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;}
body{display:grid; place-items:center; padding:16px;}
```
#app{width:min(900px, 100%); display:grid; gap:14px; grid-template-columns: 1fr; align-items:start;}
@media (min-width: 860px){ #app{grid-template-columns: 1fr 290px;} }
.card{
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border:1px solid rgba(255,255,255,0.10);
border-radius:18px;
box-shadow: 0 12px 40px rgba(0,0,0,0.35);
overflow:hidden;
}
.topbar{
display:flex; gap:10px; align-items:center; justify-content:space-between;
padding:12px 14px; background:rgba(0,0,0,0.20); border-bottom:1px solid rgba(255,255,255,0.08);
}
.brand{display:flex; gap:10px; align-items:center;}
.dot{width:10px;height:10px;border-radius:999px;background:var(--accent); box-shadow:0 0 14px rgba(124,247,255,0.9);}
.title{font-weight:700; letter-spacing:0.2px;}
.stats{display:flex; gap:14px; font-size:13px; color:var(--muted);}
.stats b{color:var(--text); font-weight:700;}
.main{padding:14px; display:grid; gap:12px; justify-items:center;}
canvas{
width: min(640px, 100%);
aspect-ratio: 1 / 1;
border-radius:16px;
background: linear-gradient(180deg, rgba(10,15,28,0.95), rgba(9,12,20,0.95));
border:1px solid rgba(255,255,255,0.10);
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35), 0 18px 60px rgba(0,0,0,0.40);
touch-action: none;
}
.side{padding:14px; display:grid; gap:12px;}
.section{background:rgba(0,0,0,0.16); border:1px solid rgba(255,255,255,0.08); border-radius:16px; padding:12px;}
.section h3{margin:0 0 8px 0; font-size:13px; color:var(--muted); font-weight:700; letter-spacing:0.3px; text-transform:uppercase;}
.row{display:flex; gap:10px; align-items:center; flex-wrap:wrap; justify-content:center;}
button{
font: inherit;
border-radius:12px;
border:1px solid rgba(255,255,255,0.12);
color: var(--text);
padding:9px 10px;
outline:none;
cursor:pointer;
background: linear-gradient(180deg, rgba(124,247,255,0.22), rgba(124,247,255,0.06));
border-color: rgba(124,247,255,0.28);
}
button.secondary{
background: rgba(0,0,0,0.22);
border-color: rgba(255,255,255,0.12);
}
button:active{transform: translateY(1px);}
.pill{
display:inline-flex; gap:8px; align-items:center;
border-radius:999px; padding:8px 10px;
border:1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.20);
font-size:13px; color:var(--muted);
}
.pill b{color:var(--text);}
.hint{font-size:12.5px; color:var(--muted); line-height:1.35;}
.kbd{display:inline-block; padding:2px 6px; border-radius:8px; border:1px solid rgba(255,255,255,0.18); background:rgba(0,0,0,0.20); color:var(--text); font-size:12px;}
#dpad{
display:grid; grid-template-columns: 58px 58px 58px; grid-template-rows: 58px 58px 58px;
gap:10px; justify-content:center; align-content:center; margin-top:6px;
user-select:none;
}
.padbtn{
border-radius:16px;
border:1px solid rgba(255,255,255,0.12);
background: rgba(0,0,0,0.22);
display:grid; place-items:center;
font-size:18px;
cursor:pointer;
touch-action: manipulation;
}
.padbtn:active{transform: translateY(1px); border-color: rgba(124,247,255,0.35);}
.padbtn.blank{opacity:0; pointer-events:none;}
#overlay{
position:absolute; inset:0;
display:grid; place-items:center;
background: radial-gradient(900px 600px at 50% 40%, rgba(17,26,46,0.78), rgba(5,7,12,0.88));
backdrop-filter: blur(6px);
}
#overlay.hidden{display:none;}
.modal{
width:min(520px, calc(100% - 28px));
border-radius:20px;
background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
border:1px solid rgba(255,255,255,0.14);
box-shadow: 0 22px 70px rgba(0,0,0,0.55);
padding:16px;
display:grid; gap:12px;
}
.modal h2{margin:0; font-size:20px;}
.modal p{margin:0; color:var(--muted); line-height:1.35;}
.modal .actions{display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end;}
.tag{
display:inline-flex; align-items:center; gap:8px;
border-radius:999px;
padding:6px 10px;
border:1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.18);
color: var(--muted);
font-size:12px;
}
.tag .warn{color:#ffd1d8;}
.tag .dot2{
width:8px;height:8px;border-radius:999px;background:var(--danger);
box-shadow:0 0 14px rgba(255,77,109,0.7);
}
```
</style>
</head>
<body>
<div id="app">
<div class="card" style="position:relative;">
<div class="topbar">
<div class="brand">
<div class="dot"></div>
<div class="title">Neon Snake</div>
</div>
<div class="stats">
<div><b>Score</b> <span id="score">0</span></div>
<div><b>Best</b> <span id="best">0</span></div>
<div><b>Level</b> <span id="level">1</span></div>
</div>
</div>
```
<div class="main">
<canvas id="c" width="600" height="600"></canvas>
<div class="row">
<span class="pill"><b>Controls:</b> <span class="kbd">Arrows</span> / <span class="kbd">WASD</span> • <span class="kbd">Space</span> pause</span>
<button id="btnPause" class="secondary">Pause</button>
<button id="btnRestart">Restart</button>
</div>
</div>
<div id="overlay">
<div class="modal">
<h2 id="overlayTitle">Neon Snake</h2>
<p id="overlayText">Eat the glowing food, grow longer, and don't crash into yourself.</p>
<div class="row" style="justify-content:flex-start;">
<span class="tag"><span class="dot2"></span><span class="warn"><b>Classic walls:</b> hitting the edge ends the game</span></span>
<span class="tag"><b>Sound:</b> on</span>
<span class="tag"><b>Difficulty:</b> medium</span>
</div>
<div class="actions">
<button id="btnStart">Start</button>
</div>
<p class="hint">On phones/tablets: swipe on the board or use the D-pad on the right.</p>
</div>
</div>
</div>
<div class="card">
<div class="side">
<div class="section">
<h3>Touch controls</h3>
<div class="hint">Use the D-pad below, or swipe on the board.</div>
<div id="dpad" aria-label="Touch controls">
<div class="padbtn blank"></div>
<div class="padbtn" data-dir="up">▲</div>
<div class="padbtn blank"></div>
<div class="padbtn" data-dir="left">◀</div>
<div class="padbtn" data-dir="pause">⏸</div>
<div class="padbtn" data-dir="right">▶</div>
<div class="padbtn blank"></div>
<div class="padbtn" data-dir="down">▼</div>
<div class="padbtn blank"></div>
</div>
</div>
<div class="section">
<h3>Shortcuts</h3>
<div class="hint">
<span class="kbd">R</span> restart • <span class="kbd">Space</span> pause
</div>
</div>
</div>
</div>
```
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const levelEl = document.getElementById('level');
const btnPause = document.getElementById('btnPause');
const btnRestart = document.getElementById('btnRestart');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlayTitle');
const overlayText = document.getElementById('overlayText');
const btnStart = document.getElementById('btnStart');
const GRID = 24;
const W = canvas.width;
const CELL = W / GRID;
const key = (x,y)=> `${x},${y}`;
const clamp = (n,min,max)=> Math.max(min, Math.min(max,n));
const difficulty = 'normal';
const walls = true;
let soundOn = true;
const BASE_SPEED = { easy: 200, normal: 165, hard: 130 };
let best = Number(localStorage.getItem('neon_snake_best') || 0);
bestEl.textContent = best;
let snake, dir, nextDir, food, score, level, tickMs, timer;
let paused = true;
let gameOver = false;
let audioCtx = null;
function ensureAudio() {
if (!soundOn) return;
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume().catch(()=>{});
}
function beep({freq=440, dur=0.08, type='sine', gain=0.035}={}) {
if (!soundOn) return;
ensureAudio();
if (!audioCtx) return;
const t0 = audioCtx.currentTime;
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, t0);
g.gain.setValueAtTime(0, t0);
g.gain.linearRampToValueAtTime(gain, t0 + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
o.connect(g).connect(audioCtx.destination);
o.start(t0);
o.stop(t0 + dur + 0.02);
}
function sfxEat(){ beep({freq: 640, dur:0.07, type:'triangle', gain:0.04}); }
function sfxTurn(){ beep({freq: 300, dur:0.03, type:'sine', gain:0.02}); }
function sfxLose(){
if (!soundOn) return;
ensureAudio();
beep({freq: 210, dur:0.10, type:'sawtooth', gain:0.05});
setTimeout(()=> beep({freq: 160, dur:0.14, type:'sawtooth', gain:0.05}), 90);
}
function sfxLevel(){
beep({freq: 780, dur:0.06, type:'square', gain:0.03});
setTimeout(()=> beep({freq: 980, dur:0.06, type:'square', gain:0.03}), 70);
}
function resetGame() {
const cx = Math.floor(GRID/2);
const cy = Math.floor(GRID/2);
snake = [
{ x: cx, y: cy+2 },
{ x: cx, y: cy+3 },
{ x: cx, y: cy+4 }
];
dir = { x: 0, y: -1 };
nextDir = { ...dir };
score = 0;
level = 1;
gameOver = false;
tickMs = BASE_SPEED[difficulty];
scoreEl.textContent = score;
levelEl.textContent = level;
spawnFood();
startLoop();
draw();
}
function startLoop(){
if (timer) clearInterval(timer);
timer = setInterval(step, tickMs);
}
function setSpeed(ms){
tickMs = clamp(ms, 55, 220);
startLoop();
}
function spawnFood(){
const occupied = new Set(snake.map(s => key(s.x,s.y)));
const head = snake[0];
for (let tries=0; tries<999; tries++){
const x = Math.floor(Math.random()*GRID);
const y = Math.floor(Math.random()*GRID);
if (occupied.has(key(x,y))) continue;
const d = Math.abs(x-head.x) + Math.abs(y-head.y);
if (d < 4 && tries < 80) continue;
food = { x, y, pulse: 0 };
return;
}
for (let y=0; y<GRID; y++){
for (let x=0; x<GRID; x++){
if (!occupied.has(key(x,y))) { food = { x,y,pulse:0 }; return; }
}
}
}
function step(){
if (paused || gameOver) return;
if (!(nextDir.x === -dir.x && nextDir.y === -dir.y)){
if (nextDir.x !== dir.x || nextDir.y !== dir.y) sfxTurn();
dir = nextDir;
}
const head = snake[0];
const nx = head.x + dir.x;
const ny = head.y + dir.y;
if (nx < 0 || nx >= GRID || ny < 0 || ny >= GRID){
endGame();
return;
}
for (let i=0; i<snake.length; i++){
if (snake[i].x === nx && snake[i].y === ny){
endGame();
return;
}
}
snake.unshift({ x:nx, y:ny });
if (nx === food.x && ny === food.y){
score++;
scoreEl.textContent = score;
sfxEat();
if (score > best){
best = score;
localStorage.setItem('neon_snake_best', String(best));
bestEl.textContent = best;
}
const newLevel = 1 + Math.floor(score / 5);
if (newLevel !== level){
level = newLevel;
levelEl.textContent = level;
sfxLevel();
setSpeed(tickMs - 8);
}
spawnFood();
} else {
snake.pop();
}
food.pulse += 0.22;
draw();
}
function endGame(){
gameOver = true;
paused = true;
sfxLose();
draw();
showOverlay("Game Over", `Score ${score}. Press Start to play again.`);
btnStart.textContent = "Play again";
}
function roundRect(x, y, w, h, r){
const rr = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x+rr, y);
ctx.arcTo(x+w, y, x+w, y+h, rr);
ctx.arcTo(x+w, y+h, x, y+h, rr);
ctx.arcTo(x, y+h, x, y, rr);
ctx.arcTo(x, y, x+w, y, rr);
ctx.closePath();
}
// Tongue flicker timer
let tongueOut = true;
setInterval(()=>{ tongueOut = !tongueOut; }, 300);
function drawSnake(){
if (!snake || snake.length === 0) return;
const R = CELL * 0.40;
ctx.save();
// --- Body tube: draw from tail to just behind head ---
for (let i = snake.length - 1; i >= 1; i--) {
const curr = snake[i];
const prev = snake[i - 1];
const cx = (curr.x + 0.5) * CELL;
const cy = (curr.y + 0.5) * CELL;
const px = (prev.x + 0.5) * CELL;
const py = (prev.y + 0.5) * CELL;
const t = i / Math.max(1, snake.length - 1);
// Taper tail
const segR = i === snake.length - 1 ? R * 0.55 : R;
// Outer green body
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(px, py);
ctx.lineWidth = segR * 2;
ctx.lineCap = 'round';
const g = Math.round(130 + 70 * (1 - t));
ctx.strokeStyle = `rgb(10, ${g}, 15)`;
ctx.shadowColor = 'rgba(30,160,30,0.25)';
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
// Belly stripe – lighter centre line
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(px, py);
ctx.lineWidth = segR * 0.55;
ctx.strokeStyle = `rgba(160, 220, 100, ${0.28 - 0.15 * t})`;
ctx.stroke();
}
// --- Scales: small arc on each body segment ---
for (let i = snake.length - 1; i >= 1; i--) {
const s = snake[i];
const cx = (s.x + 0.5) * CELL;
const cy = (s.y + 0.5) * CELL;
// Angle perpendicular to movement direction for this segment
const nx = snake[i - 1];
const angle = Math.atan2(nx.y - s.y, nx.x - s.x) + Math.PI / 2;
ctx.beginPath();
ctx.arc(cx, cy, R * 0.62, angle - 0.9, angle + 0.9);
ctx.strokeStyle = 'rgba(0, 60, 0, 0.55)';
ctx.lineWidth = 1.8;
ctx.lineCap = 'round';
ctx.stroke();
}
// --- Head ---
const head = snake[0];
const hx = (head.x + 0.5) * CELL;
const hy = (head.y + 0.5) * CELL;
// Head blob – slightly wider than body
ctx.shadowColor = 'rgba(60,220,60,0.55)';
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.arc(hx, hy, R * 1.15, 0, Math.PI * 2);
const hg = ctx.createRadialGradient(hx - R*0.3, hy - R*0.3, R*0.05, hx, hy, R*1.15);
hg.addColorStop(0, '#5ef55e');
hg.addColorStop(0.45, '#1daa1d');
hg.addColorStop(1, '#085508');
ctx.fillStyle = hg;
ctx.fill();
ctx.shadowBlur = 0;
// Head outline
ctx.beginPath();
ctx.arc(hx, hy, R * 1.15, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(0,80,0,0.6)';
ctx.lineWidth = 1.5;
ctx.stroke();
// Snout highlight
let snoutX = hx + dir.x * R * 0.6;
let snoutY = hy + dir.y * R * 0.6;
ctx.beginPath();
ctx.arc(snoutX, snoutY, R * 0.38, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(120,255,120,0.18)';
ctx.fill();
// Eyes – offset to sides of direction
const eyeDist = R * 0.52;
// perpendicular to direction
const px = -dir.y, py = dir.x;
const eyeFwd = R * 0.28;
const eyeR = R * 0.24;
[1, -1].forEach(side => {
const ex = hx + dir.x * eyeFwd + px * side * eyeDist;
const ey = hy + dir.y * eyeFwd + py * side * eyeDist;
// Iris
ctx.beginPath();
ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
ctx.fillStyle = '#f5e500';
ctx.fill();
// Slit pupil – rotated to match direction
const pupilAngle = Math.atan2(dir.y, dir.x) + Math.PI / 2;
ctx.save();
ctx.translate(ex, ey);
ctx.rotate(pupilAngle);
ctx.beginPath();
ctx.ellipse(0, 0, eyeR * 0.28, eyeR * 0.82, 0, 0, Math.PI * 2);
ctx.fillStyle = '#111';
ctx.fill();
ctx.restore();
// Eye shine
ctx.beginPath();
ctx.arc(ex - eyeR * 0.28, ey - eyeR * 0.28, eyeR * 0.22, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.75)';
ctx.fill();
});
// Tongue
if (tongueOut) {
const tBaseX = hx + dir.x * R * 1.05;
const tBaseY = hy + dir.y * R * 1.05;
const tTipX = hx + dir.x * R * 1.80;
const tTipY = hy + dir.y * R * 1.80;
const forkSize = R * 0.40;
ctx.strokeStyle = '#ff2222';
ctx.lineWidth = 1.6;
ctx.lineCap = 'round';
// Stem
ctx.beginPath();
ctx.moveTo(tBaseX, tBaseY);
ctx.lineTo(tTipX, tTipY);
ctx.stroke();
// Fork A
ctx.beginPath();
ctx.moveTo(tTipX, tTipY);
ctx.lineTo(tTipX + dir.x * forkSize * 0.6 + px * forkSize * 0.55,
tTipY + dir.y * forkSize * 0.6 + py * forkSize * 0.55);
ctx.stroke();
// Fork B
ctx.beginPath();
ctx.moveTo(tTipX, tTipY);
ctx.lineTo(tTipX + dir.x * forkSize * 0.6 - px * forkSize * 0.55,
tTipY + dir.y * forkSize * 0.6 - py * forkSize * 0.55);
ctx.stroke();
}
ctx.restore();
}
function draw(){
ctx.clearRect(0,0,W,W);
const g = ctx.createLinearGradient(0,0,0,W);
g.addColorStop(0, 'rgba(18,26,46,0.55)');
g.addColorStop(1, 'rgba(6,8,14,0.65)');
ctx.fillStyle = g;
ctx.fillRect(0,0,W,W);
ctx.globalAlpha = 0.10;
ctx.strokeStyle = '#ffffff';
ctx.beginPath();
for (let i=1;i<GRID;i++){
ctx.moveTo(i*CELL, 0); ctx.lineTo(i*CELL, W);
ctx.moveTo(0, i*CELL); ctx.lineTo(W, i*CELL);
}
ctx.stroke();
ctx.globalAlpha = 1;
if (!food || !snake) return;
const fx = food.x * CELL + CELL / 2;
const fy = food.y * CELL + CELL / 2;
const pulse = 0.9 + 0.1 * Math.sin(food.pulse || 0);
const ar = CELL * 0.36 * pulse;
ctx.save();
ctx.shadowColor = 'rgba(255,80,80,0.85)';
ctx.shadowBlur = 18;
// Apple body
ctx.beginPath();
ctx.arc(fx - ar*0.08, fy + ar*0.08, ar, 0, Math.PI*2);
ctx.closePath();
const appleGrad = ctx.createRadialGradient(fx - ar*0.25, fy - ar*0.25, ar*0.05, fx, fy, ar);
appleGrad.addColorStop(0, '#ff6e6e');
appleGrad.addColorStop(0.5, '#e8001c');
appleGrad.addColorStop(1, '#8b0010');
ctx.fillStyle = appleGrad;
ctx.fill();
// Indent at top
ctx.beginPath();
ctx.arc(fx - ar*0.08, fy - ar*0.82, ar*0.28, 0, Math.PI*2);
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fill();
// Shine highlight
ctx.beginPath();
ctx.ellipse(fx - ar*0.32, fy - ar*0.30, ar*0.18, ar*0.11, -0.6, 0, Math.PI*2);
ctx.fillStyle = 'rgba(255,255,255,0.55)';
ctx.fill();
// Stem
ctx.beginPath();
ctx.moveTo(fx - ar*0.05, fy - ar*0.78);
ctx.bezierCurveTo(fx - ar*0.05, fy - ar*1.18, fx + ar*0.38, fy - ar*1.22, fx + ar*0.30, fy - ar*1.05);
ctx.strokeStyle = '#5a3010';
ctx.lineWidth = CELL * 0.07;
ctx.lineCap = 'round';
ctx.stroke();
// Leaf
ctx.beginPath();
ctx.ellipse(fx + ar*0.18, fy - ar*1.10, ar*0.22, ar*0.11, -0.8, 0, Math.PI*2);
ctx.fillStyle = '#3ecf3e';
ctx.fill();
ctx.restore();
drawSnake();
ctx.save();
ctx.globalAlpha = 0.45;
ctx.strokeStyle = 'rgba(255,77,109,0.55)';
ctx.lineWidth = 6;
ctx.strokeRect(3,3,W-6,W-6);
ctx.restore();
if (paused && overlay.classList.contains('hidden')){
drawCenterToast("Paused");
}
}
function drawCenterToast(text){
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.45)';
const w = 260, h = 56;
const x = (W-w)/2, y = (W-h)/2;
roundRect(x,y,w,h,16);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.90)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '700 18px system-ui, -apple-system, Segoe UI, Roboto, Arial';
ctx.fillText(text, W/2, W/2);
ctx.restore();
}
function showOverlay(title, text) {
overlayTitle.textContent = title;
overlayText.textContent = text;
overlay.classList.remove('hidden');
paused = true;
draw();
}
function hideOverlay() {
overlay.classList.add('hidden');
paused = false;
}
function setDir(dx,dy){ nextDir = { x:dx, y:dy }; }
function togglePause(){
if (!overlay.classList.contains('hidden')) return;
paused = !paused;
btnPause.textContent = paused ? "Resume" : "Pause";
draw();
}
function onKey(e){
const k = e.key.toLowerCase();
if (k === ' ' || k === 'spacebar'){ togglePause(); e.preventDefault(); return; }
if (k === 'r'){ ensureAudio(); hideOverlay(); resetGame(); return; }
if (k === 'arrowup' || k === 'w') setDir(0,-1);
else if (k === 'arrowdown' || k === 's') setDir(0,1);
else if (k === 'arrowleft' || k === 'a') setDir(-1,0);
else if (k === 'arrowright' || k === 'd') setDir(1,0);
}
window.addEventListener('keydown', onKey, { passive:false });
btnPause.addEventListener('click', ()=>{ ensureAudio(); togglePause(); });
btnRestart.addEventListener('click', ()=>{ ensureAudio(); hideOverlay(); resetGame(); btnStart.textContent="Start"; btnPause.textContent="Pause"; });
document.getElementById('dpad').addEventListener('click', (e)=>{
const btn = e.target.closest('.padbtn');
if (!btn || btn.classList.contains('blank')) return;
ensureAudio();
const d = btn.dataset.dir;
if (d === 'up') setDir(0,-1);
else if (d === 'down') setDir(0,1);
else if (d === 'left') setDir(-1,0);
else if (d === 'right') setDir(1,0);
else if (d === 'pause') togglePause();
});
let touchStart = null;
canvas.addEventListener('pointerdown', (e)=>{
ensureAudio();
canvas.setPointerCapture(e.pointerId);
touchStart = { x:e.clientX, y:e.clientY, t:performance.now() };
});
canvas.addEventListener('pointerup', (e)=>{
if (!touchStart) return;
const tapX = e.clientX;
const tapY = e.clientY;
touchStart = null;
// Convert tap to canvas coordinates
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = W / rect.height;
const canvasTapX = (tapX - rect.left) * scaleX;
const canvasTapY = (tapY - rect.top) * scaleY;
// Head center in canvas pixels
const head = snake[0];
const headPx = (head.x + 0.5) * CELL;
const headPy = (head.y + 0.5) * CELL;
const dx = canvasTapX - headPx;
const dy = canvasTapY - headPy;
const adx = Math.abs(dx);
const ady = Math.abs(dy);
// Tiny tap near head = pause
if (adx < CELL && ady < CELL){ togglePause(); return; }
if (adx > ady){
setDir(dx > 0 ? 1 : -1, 0);
} else {
setDir(0, dy > 0 ? 1 : -1);
}
});
btnStart.addEventListener('click', ()=>{
ensureAudio();
hideOverlay();
btnPause.textContent = "Pause";
resetGame();
});
showOverlay("Neon Snake", "Eat the glowing food, grow longer, and don't crash into yourself.");
</script>
</body>
</html>Game Source: Neon Snake
Creator: SolarScout64
Libraries: none
Complexity: complex (773 lines, 23.7 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 KIDHUBB header at the top with "remix_of: neon-snake-solarscout64" to link back to the original. Then publish at kidhubb.com/publish.