Chotto.app
Login
Sign Up
ES
App Name
kuyawa / snake
App Icon in SVG
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"> <!-- Definitions for Gradients and Shadows --> <defs> <!-- Background Gradient (Dark Theme) --> <linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#2D3748" /> <stop offset="100%" stop-color="#1A202C" /> </linearGradient> <!-- Snake Gradient (Vibrant Green) --> <linearGradient id="snakeGradient" x1="0%" y1="100%" x2="100%" y2="0%"> <stop offset="0%" stop-color="#10B981" /> <stop offset="100%" stop-color="#34D399" /> </linearGradient> <!-- Drop Shadow for Depth --> <filter id="shadow" x="-10%" y="-10%" width="120%" height="120%"> <feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#000000" flood-opacity="0.3"/> </filter> </defs> <!-- App Icon Rounded Background --> <rect width="512" height="512" rx="115" fill="url(#bgGradient)" /> <!-- Subtle Inner Retro Grid --> <path d="M100 0 v512 M200 0 v512 M300 0 v512 M400 0 v512 M0 100 h512 M0 200 h512 M0 300 h512 M0 400 h512" stroke="#4A5568" stroke-opacity="0.15" stroke-width="4"/> <!-- Snake Body --> <path d="M 150 380 L 362 380 L 362 256 L 150 256 L 150 132 L 280 132" fill="none" stroke="url(#snakeGradient)" stroke-width="56" stroke-linejoin="round" stroke-linecap="round" filter="url(#shadow)" /> <!-- Snake Eye --> <circle cx="270" cy="115" r="8" fill="#1A202C" /> <!-- Snake Forked Tongue --> <path d="M 308 132 L 332 132 L 342 124 M 332 132 L 342 140" fill="none" stroke="#F87171" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/> <!-- Food (Red Apple) --> <circle cx="380" cy="132" r="22" fill="#EF4444" filter="url(#shadow)" /> <!-- Apple Leaf --> <path d="M 380 110 C 380 100, 393 100, 393 110 C 393 120, 380 120, 380 110" fill="#10B981" /> </svg>
HTML Content
<!DOCTYPE html> <html lang="en" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Snake</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> <style> :root{--speed:0.3s} [data-theme="dark"]{ --bg:#0c0c0c;--bg2:#161616;--fg:#ece8df;--muted:#7a7468; --accent:#e8a838;--accent-glow:rgba(232,168,56,0.12); --card:#181818;--border:#262626;--danger:#e84848;--success:#48c878; --key-bg:#1e1e1e;--key-hover:#2a2a2a;--key-border:#333; --shadow:0 4px 30px rgba(0,0,0,0.5);--overlay:rgba(0,0,0,0.65); } [data-theme="light"]{ --bg:#f4efe5;--bg2:#eae4d6;--fg:#1a1612;--muted:#8a7e6e; --accent:#c07e18;--accent-glow:rgba(192,126,24,0.08); --card:#fff;--border:#ddd4c4;--danger:#c82828;--success:#2a9858; --key-bg:#ede7db;--key-hover:#e0d9cb;--key-border:#ccc3b3; --shadow:0 4px 30px rgba(0,0,0,0.06);--overlay:rgba(0,0,0,0.35); } *{margin:0;padding:0;box-sizing:border-box} body{ font-family:'Space Grotesk',sans-serif;background:var(--bg);color:var(--fg); min-height:100vh;overflow-x:hidden;transition:background var(--speed),color var(--speed); } body::before{ content:'';position:fixed;inset:0;pointer-events:none;z-index:0; background: radial-gradient(ellipse at 20% 50%,var(--accent-glow) 0%,transparent 55%), radial-gradient(ellipse at 80% 20%,var(--accent-glow) 0%,transparent 45%), radial-gradient(ellipse at 50% 85%,var(--accent-glow) 0%,transparent 50%); } body::after{ content:'';position:fixed;inset:0;pointer-events:none;z-index:0;opacity:0.07; background-image: linear-gradient(var(--border) 1px,transparent 1px), linear-gradient(90deg,var(--border) 1px,transparent 1px); background-size:60px 60px; } .container{ position:relative;z-index:1;max-width:680px;margin:0 auto;padding:10px 16px; min-height:100vh;display:flex;flex-direction:column;align-items:center; } header{ width:100%;display:flex;justify-content:space-between;align-items:center; padding:6px 0 14px; } .logo{ font-size:1.35rem;font-weight:700;letter-spacing:-0.5px; display:flex;align-items:center;gap:8px; } .logo span{color:var(--accent)} .logo svg{width:22px;height:22px;flex-shrink:0} .header-right{display:flex;align-items:center;gap:8px} .stats{ display:flex;gap:12px;font-size:0.75rem;color:var(--muted); font-family:'JetBrains Mono',monospace; } .stat-val{color:var(--accent);font-weight:700} .stat-val.rec{color:var(--success)} .btn-icon{ width:34px;height:34px;display:flex;align-items:center;justify-content:center; background:var(--key-bg);border:1px solid var(--border);border-radius:9px; cursor:pointer;color:var(--fg);transition:all 0.2s;flex-shrink:0; } .btn-icon:hover{background:var(--key-hover);transform:translateY(-1px)} .btn-icon svg{width:15px;height:15px} .theme-toggle{ position:relative;width:46px;height:25px;background:var(--key-bg); border:1px solid var(--border);border-radius:13px;cursor:pointer; transition:all var(--speed);flex-shrink:0; } .theme-toggle::after{ content:'';position:absolute;top:2.5px;left:2.5px;width:18px;height:18px; background:var(--accent);border-radius:50%;transition:transform var(--speed); box-shadow:0 2px 5px rgba(0,0,0,0.2); } [data-theme="light"] .theme-toggle::after{transform:translateX(21px)} .theme-icons{ position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between; padding:0 6px;font-size:9px;pointer-events:none; } .game-wrapper{ flex:1;display:flex;flex-direction:column;align-items:center; gap:14px;width:100%; } .game-board{ position:relative;border-radius:14px;overflow:hidden; border:1px solid var(--border); box-shadow:0 0 50px var(--accent-glow),var(--shadow); transition:border-color 0.3s,box-shadow 0.3s; } .game-board.wall-hit{ border-color:var(--danger)!important; box-shadow:0 0 40px rgba(232,72,72,0.25),var(--shadow)!important; } .game-board.self-hit{animation:bShake 0.4s ease} canvas{display:block} .dpad{ display:none; grid-template-areas:". up ." "left center right" ". down ."; grid-template-columns:54px 54px 54px;grid-template-rows:54px 54px 54px; gap:4px;margin-top:2px; } .dpad-btn{ display:flex;align-items:center;justify-content:center; background:var(--key-bg);border:1px solid var(--border);border-radius:13px; color:var(--fg);cursor:pointer;transition:all 0.12s; -webkit-tap-highlight-color:transparent;outline:none; } .dpad-btn:active,.dpad-btn.pressed{ background:var(--accent);color:#0c0c0c;transform:scale(0.9); border-color:var(--accent); } .dpad-btn svg{width:18px;height:18px;pointer-events:none} .dpad-up{grid-area:up}.dpad-down{grid-area:down} .dpad-left{grid-area:left}.dpad-right{grid-area:right} .dpad-center{grid-area:center;background:var(--border);border-radius:50%;border:none} @media(max-width:720px){.dpad{display:grid}} @media(max-width:400px){ .dpad{grid-template-columns:46px 46px 46px;grid-template-rows:46px 46px 46px} .stats{gap:8px;font-size:0.68rem} } .hint{font-size:0.76rem;color:var(--muted);text-align:center;padding:2px 0} @media(max-width:720px){.hint{display:none}} /* Modal */ .ov{ position:fixed;inset:0;background:var(--overlay);backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);z-index:100;display:flex;align-items:center; justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.3s; } .ov.open{opacity:1;pointer-events:all} .modal{ background:var(--card);border:1px solid var(--border);border-radius:22px; padding:34px 30px 26px;max-width:340px;width:90%;text-align:center; box-shadow:0 24px 80px rgba(0,0,0,0.35); transform:scale(0.92) translateY(16px); transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1); } .ov.open .modal{transform:scale(1) translateY(0)} .m-badge{ width:60px;height:60px;margin:0 auto 14px;border-radius:50%; display:flex;align-items:center;justify-content:center; } .m-badge.lose{background:rgba(232,72,72,0.1);border:2px solid var(--danger)} .m-badge.won{background:rgba(72,200,120,0.1);border:2px solid var(--success)} .modal h2{font-size:1.35rem;font-weight:700;margin-bottom:16px} .m-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:20px} .m-stat{ background:var(--key-bg);border:1px solid var(--border);border-radius:12px; padding:10px 6px; } .m-stat-l{font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.7px;margin-bottom:3px} .m-stat-v{font-family:'JetBrains Mono',monospace;font-size:1.2rem;font-weight:700;color:var(--accent)} .m-stat-v.rec{color:var(--success)} .btn{ display:inline-flex;align-items:center;justify-content:center;gap:8px; padding:12px 28px;font-family:'Space Grotesk',sans-serif;font-size:0.9rem; font-weight:600;border:none;border-radius:12px;cursor:pointer;transition:all 0.2s; } .btn-a{background:var(--accent);color:#0c0c0c} .btn-a:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(232,168,56,0.25)} /* Particles */ .particles{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden} .dot{ position:absolute;border-radius:50%;background:var(--accent);opacity:0; animation:drift linear infinite; } @keyframes drift{ 0%{transform:translateY(105vh) scale(0);opacity:0} 8%{opacity:0.09;transform:translateY(95vh) scale(1)} 92%{opacity:0.09} 100%{transform:translateY(-5vh) scale(0.5);opacity:0} } @keyframes bShake{ 0%,100%{transform:translateX(0)} 15%{transform:translateX(-5px) rotate(-0.3deg)} 30%{transform:translateX(5px) rotate(0.3deg)} 45%{transform:translateX(-3px)} 60%{transform:translateX(3px)} 75%{transform:translateX(-1px)} } @media(prefers-reduced-motion:reduce){ *,*::before,*::after{animation-duration:0.01ms!important;transition-duration:0.01ms!important} } </style> </head> <body> <div class="particles" id="ptcl"></div> <div class="ov" id="ov"> <div class="modal"> <div class="m-badge" id="mBadge"></div> <h2 id="mTitle"></h2> <div class="m-stats"> <div class="m-stat"><div class="m-stat-l">Score</div><div class="m-stat-v" id="mScore">0</div></div> <div class="m-stat"><div class="m-stat-l">Best</div><div class="m-stat-v" id="mBest">0</div></div> <div class="m-stat"><div class="m-stat-l">Length</div><div class="m-stat-v" id="mLen">0</div></div> <div class="m-stat"><div class="m-stat-l">Level</div><div class="m-stat-v" id="mLvl">0</div></div> </div> <button class="btn btn-a" id="btnAgain">Play Again</button> </div> </div> <div class="container"> <header> <div class="logo"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M3 18Q3 12 8 12Q13 12 13 7Q13 4 17 4" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round"/> <circle cx="17.5" cy="4" r="2.2" fill="var(--accent)"/> <circle cx="18.3" cy="3.3" r="0.7" fill="var(--bg)"/> </svg> <span>snake</span> </div> <div class="header-right"> <div class="stats"> <span><span class="stat-val" id="hScore">0</span></span> <span>Best <span class="stat-val" id="hBest">0</span></span> <span>Lv <span class="stat-val" id="hLvl">1</span></span> </div> <button class="btn-icon" id="btnPause" aria-label="Pause" title="Pause (P)"> <svg id="pauseIco" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg> </button> <button class="btn-icon" id="btnNew" aria-label="New game" title="New game"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg> </button> <div class="theme-toggle" id="themeTgl" role="button" tabindex="0" aria-label="Toggle theme"> <div class="theme-icons"><span>☽</span><span>☼</span></div> </div> </div> </header> <main class="game-wrapper"> <div class="game-board" id="board"> <canvas id="cv"></canvas> </div> <div class="dpad" id="dpad"> <button class="dpad-btn dpad-up" data-dir="up" aria-label="Up"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg> </button> <button class="dpad-btn dpad-left" data-dir="left" aria-label="Left"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> </button> <div class="dpad-center"></div> <button class="dpad-btn dpad-right" data-dir="right" aria-label="Right"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg> </button> <button class="dpad-btn dpad-down" data-dir="down" aria-label="Down"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg> </button> </div> <p class="hint">Arrow keys / WASD to move · P to pause · Space to start</p> </main> </div> <script> /* ── Constants ── */ const G = 20; // grid cells const INIT_SPEED = 145; // ms per tick const MIN_SPEED = 58; const SPEED_DEC = 2.2; // ms faster per food /* Theme-aware canvas colors */ const C = { dark: { gridA:'rgba(255,255,255,0.018)', gridB:'rgba(255,255,255,0.035)', ovlBg:'rgba(12,12,12,0.78)', ovlTxt:'#ece8df', ovlSub:'#7a7468', hudBg:'rgba(12,12,12,0.55)', hudTxt:'#ece8df', headR:[232,168,56], tailR:[90,58,12], food:'#e84848', foodGlow:'rgba(232,72,72,0.45)', foodHi:'rgba(255,255,255,0.28)', stem:'#48c878', eyeW:'#ffffff', eyeP:'#1a1612', parts:['#e8a838','#e84848','#f0c060','#48c878'], gridLine:'rgba(255,255,255,0.04)', }, light: { gridA:'rgba(0,0,0,0.012)', gridB:'rgba(0,0,0,0.025)', ovlBg:'rgba(244,239,229,0.82)', ovlTxt:'#1a1612', ovlSub:'#8a7e6e', hudBg:'rgba(255,255,255,0.6)', hudTxt:'#1a1612', headR:[192,126,24], tailR:[90,58,12], food:'#c82828', foodGlow:'rgba(200,40,40,0.35)', foodHi:'rgba(255,255,255,0.4)', stem:'#2a9858', eyeW:'#ffffff', eyeP:'#1a1612', parts:['#c07e18','#c82828','#d8a848','#2a9858'], gridLine:'rgba(0,0,0,0.05)', } }; /* ── State ── */ let snake, dir, dirQueue, food, score, highScore, speed, level; let state = 'idle'; // idle | playing | paused | over | won let lastTick = 0, particles = [], eatFlash = 0, rafId; let canvas, ctx, cellSz, cvSz, dpr; /* ── DOM ── */ const $=id=>document.getElementById(id); const board=$('board'), cv=$('cv'), ov=$('ov'); const hScore=$('hScore'), hBest=$('hBest'), hLvl=$('hLvl'); const mBadge=$('mBadge'), mTitle=$('mTitle'); const mScore=$('mScore'), mBest=$('mBest'), mLen=$('mLen'), mLvl=$('mLvl'); /* ── Particles (CSS) ── */ (function(){ const c=$('ptcl'); for(let i=0;i<16;i++){ const d=document.createElement('div');d.className='dot'; const s=2+Math.random()*3; d.style.cssText=`left:${Math.random()*100}%;width:${s}px;height:${s}px;animation-duration:${20+Math.random()*30}s;animation-delay:${-Math.random()*30}s;`; c.appendChild(d); } })(); /* ── Canvas resize ── */ function resize(){ const pad=32; const maxW=Math.min(560, window.innerWidth-pad); const maxH=window.innerHeight-(window.innerWidth<=720?340:260); const sz=Math.max(200, Math.min(maxW, maxH)); cvSz=Math.floor(sz/G)*G; cellSz=cvSz/G; dpr=window.devicePixelRatio||1; cv.width=cvSz*dpr; cv.height=cvSz*dpr; cv.style.width=cvSz+'px'; cv.style.height=cvSz+'px'; ctx=cv.getContext('2d'); ctx.setTransform(dpr,0,0,dpr,0,0); } window.addEventListener('resize',()=>{ resize(); }); /* ── Helpers ── */ function theme(){ return document.documentElement.getAttribute('data-theme')||'dark'; } function tc(){ return C[theme()]; } function lerp(a,b,t){ return a+(b-a)*t; } function lerpC(a,b,t){ return [Math.round(lerp(a[0],b[0],t)),Math.round(lerp(a[1],b[1],t)),Math.round(lerp(a[2],b[2],t))]; } function rgb(c){ return `rgb(${c[0]},${c[1]},${c[2]})`; } function rrect(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(); } /* ── Game init ── */ function init(){ snake=[{x:7,y:10},{x:6,y:10},{x:5,y:10}]; dir='right'; dirQueue=[]; score=0; speed=INIT_SPEED; level=1; particles=[]; eatFlash=0; highScore=parseInt(localStorage.getItem('snk-hi'))||0; placeFood(); updateHUD(); } function placeFood(){ const occ=new Set(snake.map(s=>s.x+','+s.y)); const avail=[]; for(let x=0;x<G;x++) for(let y=0;y<G;y++) if(!occ.has(x+','+y)) avail.push({x,y}); if(!avail.length){ state='won'; showEnd(true); return; } food=avail[Math.floor(Math.random()*avail.length)]; } function updateHUD(){ hScore.textContent=score; hBest.textContent=highScore; hLvl.textContent=level; hBest.classList.toggle('rec', score>0 && score>=highScore); } /* ── Direction queue ── */ const OPP={up:'down',down:'up',left:'right',right:'left'}; function qDir(d){ const last=dirQueue.length?dirQueue[dirQueue.length-1]:dir; if(d!==last && d!==OPP[last] && dirQueue.length<3) dirQueue.push(d); } /* ── Update ── */ function update(){ if(dirQueue.length) dir=dirQueue.shift(); const head={...snake[0]}; switch(dir){ case 'up': head.y--; break; case 'down': head.y++; break; case 'left': head.x--; break; case 'right': head.x++; break; } /* Wall collision */ if(head.x<0||head.x>=G||head.y<0||head.y>=G){ die('wall'); return; } /* Self collision */ if(snake.some(s=>s.x===head.x&&s.y===head.y)){ die('self'); return; } snake.unshift(head); /* Eat food? */ if(head.x===food.x && head.y===food.y){ score++; speed=Math.max(MIN_SPEED, INIT_SPEED-score*SPEED_DEC); level=Math.min(10, Math.floor(score/4)+1); eatFlash=1; spawnParts(food.x*cellSz+cellSz/2, food.y*cellSz+cellSz/2); placeFood(); updateHUD(); } else { snake.pop(); } } function die(type){ state='over'; board.classList.add(type==='wall'?'wall-hit':'self-hit'); setTimeout(()=>board.classList.remove('wall-hit','self-hit'),500); let isNew=false; if(score>highScore){ highScore=score; localStorage.setItem('snk-hi',highScore); isNew=true; } updateHUD(); setTimeout(()=>showEnd(false,isNew),600); } /* ── Particles (canvas) ── */ function spawnParts(x,y){ const c=tc(); for(let i=0;i<14;i++){ const a=Math.random()*Math.PI*2; const sp=1.5+Math.random()*3.5; particles.push({ x,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp, life:1, decay:0.025+Math.random()*0.025, sz:2+Math.random()*3, color:c.parts[Math.floor(Math.random()*c.parts.length)] }); } } function updateParts(){ for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.x+=p.vx; p.y+=p.vy; p.vx*=0.95; p.vy*=0.95; p.life-=p.decay; if(p.life<=0) particles.splice(i,1); } } /* ── Draw ── */ function draw(){ const c=tc(); ctx.clearRect(0,0,cvSz,cvSz); /* Grid */ for(let x=0;x<G;x++) for(let y=0;y<G;y++){ ctx.fillStyle=(x+y)%2===0?c.gridA:c.gridB; ctx.fillRect(x*cellSz,y*cellSz,cellSz,cellSz); } /* Subtle grid lines */ ctx.strokeStyle=c.gridLine; ctx.lineWidth=0.5; for(let i=1;i<G;i++){ ctx.beginPath(); ctx.moveTo(i*cellSz,0); ctx.lineTo(i*cellSz,cvSz); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,i*cellSz); ctx.lineTo(cvSz,i*cellSz); ctx.stroke(); } /* Food */ if(food && state!=='idle') drawFood(c); /* Snake */ if(state!=='idle') drawSnake(c); /* Particles */ for(const p of particles){ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(p.x,p.y,Math.max(0.5,p.sz*p.life),0,Math.PI*2); ctx.fill(); } ctx.globalAlpha=1; /* Canvas HUD (score during play) */ if(state==='playing') drawHUD(c); /* Overlays */ if(state==='idle') drawIdle(c); if(state==='paused') drawPaused(c); } function drawFood(c){ const cx=food.x*cellSz+cellSz/2; const cy=food.y*cellSz+cellSz/2; const baseR=cellSz*0.3; const pulse=Math.sin(Date.now()*0.006)*cellSz*0.035; const r=Math.max(1,baseR+pulse); /* Glow */ ctx.save(); ctx.shadowColor=c.foodGlow; ctx.shadowBlur=16; ctx.fillStyle=c.food; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fill(); ctx.restore(); /* Highlight */ const g=ctx.createRadialGradient(cx-r*0.3,cy-r*0.35,0,cx,cy,r); g.addColorStop(0,c.foodHi); g.addColorStop(1,'rgba(255,255,255,0)'); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fill(); /* Stem */ ctx.strokeStyle=c.stem; ctx.lineWidth=Math.max(1,cellSz*0.06); ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(cx,cy-r+1); ctx.quadraticCurveTo(cx+cellSz*0.08,cy-r-cellSz*0.15,cx+cellSz*0.04,cy-r-cellSz*0.2); ctx.stroke(); /* Leaf */ ctx.fillStyle=c.stem; ctx.beginPath(); ctx.ellipse(cx+cellSz*0.1,cy-r-cellSz*0.1,cellSz*0.09,cellSz*0.04,Math.PI/4,0,Math.PI*2); ctx.fill(); } function drawSnake(c){ const len=snake.length; const pad=cellSz*0.07; /* Connections between adjacent segments */ for(let i=0;i<len-1;i++){ const cur=snake[i], nxt=snake[i+1]; const t=i/Math.max(1,len-1); ctx.fillStyle=rgb(lerpC(c.headR,c.tailR,t)); const p2=pad+cellSz*0.04; if(cur.x===nxt.x){ const minY=Math.min(cur.y,nxt.y); ctx.fillRect(cur.x*cellSz+p2, minY*cellSz+cellSz-pad, cellSz-p2*2, pad*2+cellSz*0.02); } else if(cur.y===nxt.y){ const minX=Math.min(cur.x,nxt.x); ctx.fillRect(minX*cellSz+cellSz-pad, cur.y*cellSz+p2, pad*2+cellSz*0.02, cellSz-p2*2); } } /* Segments (tail first so head is on top) */ for(let i=len-1;i>=0;i--){ const s=snake[i]; const t=i/Math.max(1,len-1); const col=lerpC(c.headR,c.tailR,t); const x=s.x*cellSz+pad, y=s.y*cellSz+pad; const sz=cellSz-pad*2; const rad=sz*0.28; /* Head glow */ if(i===0){ ctx.save(); ctx.shadowColor=`rgba(${c.headR.join(',')},0.4)`; ctx.shadowBlur=eatFlash>0?18:10; ctx.fillStyle=eatFlash>0?`rgba(255,220,120,${0.3+eatFlash*0.4})`:rgb(col); rrect(x,y,sz,sz,rad); ctx.fill(); ctx.restore(); if(eatFlash>0) eatFlash=Math.max(0,eatFlash-0.06); } else { ctx.fillStyle=rgb(col); rrect(x,y,sz,sz,rad); ctx.fill(); } /* Eyes on head */ if(i===0) drawEyes(s,c); } } function drawEyes(head,c){ const cx=head.x*cellSz+cellSz/2; const cy=head.y*cellSz+cellSz/2; const eR=cellSz*0.11, pR=cellSz*0.055; const off=cellSz*0.17, pOff=pR*0.5; let e1,e2,po; switch(dir){ case 'right': e1={x:cx+off,y:cy-off}; e2={x:cx+off,y:cy+off}; po={x:pOff,y:0}; break; case 'left': e1={x:cx-off,y:cy-off}; e2={x:cx-off,y:cy+off}; po={x:-pOff,y:0}; break; case 'up': e1={x:cx-off,y:cy-off}; e2={x:cx+off,y:cy-off}; po={x:0,y:-pOff}; break; case 'down': e1={x:cx-off,y:cy+off}; e2={x:cx+off,y:cy+off}; po={x:0,y:pOff}; break; } ctx.fillStyle=c.eyeW; ctx.beginPath(); ctx.arc(e1.x,e1.y,eR,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(e2.x,e2.y,eR,0,Math.PI*2); ctx.fill(); ctx.fillStyle=c.eyeP; ctx.beginPath(); ctx.arc(e1.x+po.x,e1.y+po.y,pR,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(e2.x+po.x,e2.y+po.y,pR,0,Math.PI*2); ctx.fill(); } function drawHUD(c){ ctx.fillStyle=c.hudBg; rrect(8,8,80,28,8); ctx.fill(); ctx.fillStyle=c.hudTxt; ctx.font='700 13px "JetBrains Mono",monospace'; ctx.textAlign='left'; ctx.textBaseline='middle'; ctx.fillText(''+score,18,22); } function drawIdle(c){ ctx.fillStyle=c.ovlBg; ctx.fillRect(0,0,cvSz,cvSz); ctx.fillStyle=c.ovlTxt; ctx.font='700 '+Math.round(cvSz*0.11)+'px "Space Grotesk",sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('SNAKE',cvSz/2,cvSz/2-cvSz*0.06); ctx.fillStyle=c.ovlSub; ctx.font='400 '+Math.round(cvSz*0.038)+'px "Space Grotesk",sans-serif'; ctx.fillText('Press Space or Tap to Start',cvSz/2,cvSz/2+cvSz*0.05); /* Draw a small decorative snake */ const sx=cvSz/2-cvSz*0.12, sy=cvSz/2+cvSz*0.14; ctx.strokeStyle=c.ovlSub; ctx.lineWidth=2.5; ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(sx,sy); ctx.quadraticCurveTo(sx+cvSz*0.06,sy-cvSz*0.04,sx+cvSz*0.12,sy); ctx.quadraticCurveTo(sx+cvSz*0.18,sy+cvSz*0.04,sx+cvSz*0.24,sy); ctx.stroke(); ctx.fillStyle=c.ovlSub; ctx.beginPath(); ctx.arc(sx+cvSz*0.24,sy,3,0,Math.PI*2); ctx.fill(); } function drawPaused(c){ ctx.fillStyle=c.ovlBg; ctx.fillRect(0,0,cvSz,cvSz); ctx.fillStyle=c.ovlTxt; ctx.font='700 '+Math.round(cvSz*0.09)+'px "Space Grotesk",sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('PAUSED',cvSz/2,cvSz/2-cvSz*0.03); ctx.fillStyle=c.ovlSub; ctx.font='400 '+Math.round(cvSz*0.038)+'px "Space Grotesk",sans-serif'; ctx.fillText('Press P or tap to resume',cvSz/2,cvSz/2+cvSz*0.06); } /* ── Game loop ── */ function loop(ts){ rafId=requestAnimationFrame(loop); if(state==='playing'){ if(ts-lastTick>=speed){ update(); lastTick=ts; } } updateParts(); draw(); } /* ── Start / Pause ── */ function startGame(){ if(state==='playing') return; if(state==='over'||state==='won'||state==='idle') init(); state='playing'; lastTick=performance.now(); ov.classList.remove('open'); setPauseIcon(false); } function togglePause(){ if(state==='playing'){ state='paused'; setPauseIcon(true); } else if(state==='paused'){ state='playing'; lastTick=performance.now(); setPauseIcon(false); } } function setPauseIcon(paused){ $('pauseIco').innerHTML=paused ? '<path d="M8 5v14l11-7z" fill="currentColor"/>' : '<rect x="6" y="4" width="4" height="16" rx="1" fill="currentColor"/><rect x="14" y="4" width="4" height="16" rx="1" fill="currentColor"/>'; } /* ── End modal ── */ function showEnd(won, isNew){ mBadge.className='m-badge '+(won?'won':'lose'); mBadge.innerHTML=won ?'<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--success)" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>' :'<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'; mTitle.textContent=won?'Incredible!':'Game Over'; mTitle.style.color=won?'var(--success)':'var(--danger)'; mScore.textContent=score; mBest.textContent=highScore; mBest.className='m-stat-v'+(isNew?' rec':''); mLen.textContent=snake.length; mLvl.textContent=level; ov.classList.add('open'); } /* ── Theme ── */ function setTheme(t){ document.documentElement.setAttribute('data-theme',t); localStorage.setItem('snk-theme',t); } function toggleTheme(){ setTheme(theme()==='dark'?'light':'dark'); } /* ── Input: Keyboard ── */ const KEYMAP={ ArrowUp:'up',ArrowDown:'down',ArrowLeft:'left',ArrowRight:'right', w:'up',s:'down',a:'left',d:'right', W:'up',S:'down',A:'left',D:'right', }; document.addEventListener('keydown',e=>{ if(e.key===' '||e.key==='Enter'){ e.preventDefault(); if(state==='idle') startGame(); else if(state==='over'||state==='won') startGame(); else if(ov.classList.contains('open')) startGame(); return; } if(e.key==='p'||e.key==='P'||e.key==='Escape'){ e.preventDefault(); if(state==='playing'||state==='paused') togglePause(); return; } const d=KEYMAP[e.key]; if(d){ e.preventDefault(); if(state==='idle') startGame(); if(state==='playing') qDir(d); } }); /* ── Input: D-pad buttons ── */ document.querySelectorAll('.dpad-btn').forEach(btn=>{ const d=btn.dataset.dir; const down=()=>{ btn.classList.add('pressed'); if(state==='idle') startGame(); if(state==='playing') qDir(d); }; const up=()=>btn.classList.remove('pressed'); btn.addEventListener('mousedown',down); btn.addEventListener('mouseup',up); btn.addEventListener('mouseleave',up); btn.addEventListener('touchstart',e=>{e.preventDefault();down();},{passive:false}); btn.addEventListener('touchend',e=>{e.preventDefault();up();},{passive:false}); }); /* ── Input: Canvas tap / swipe ── */ let tx0=0,ty0=0,tTime=0; cv.addEventListener('touchstart',e=>{ e.preventDefault(); const t=e.touches[0]; tx0=t.clientX; ty0=t.clientY; tTime=Date.now(); if(state==='idle') startGame(); else if(state==='paused') togglePause(); },{passive:false}); cv.addEventListener('touchend',e=>{ e.preventDefault(); if(state!=='playing') return; const t=e.changedTouches[0]; const dx=t.clientX-tx0, dy=t.clientY-ty0; const ax=Math.abs(dx), ay=Math.abs(dy); if(Math.max(ax,ay)<15) return; // tap, not swipe if(Date.now()-tTime>500) return; // too slow if(ax>ay) qDir(dx>0?'right':'left'); else qDir(dy>0?'down':'up'); },{passive:false}); /* Canvas click (desktop) */ cv.addEventListener('click',()=>{ if(state==='idle') startGame(); else if(state==='paused') togglePause(); }); /* ── Buttons ── */ $('btnPause').addEventListener('click',()=>{ if(state==='playing'||state==='paused') togglePause(); }); $('btnNew').addEventListener('click',()=>{ ov.classList.remove('open'); state='idle'; init(); }); $('btnAgain').addEventListener('click',()=>{ ov.classList.remove('open'); startGame(); }); ov.addEventListener('click',e=>{ if(e.target===ov) startGame(); }); $('themeTgl').addEventListener('click',toggleTheme); $('themeTgl').addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleTheme();}}); /* Prevent scroll on arrow keys */ window.addEventListener('keydown',e=>{ if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault(); },{passive:false}); /* ── Init ── */ const saved=localStorage.getItem('snk-theme'); if(saved) setTheme(saved); resize(); init(); rafId=requestAnimationFrame(loop); </script> </body> </html>
Cancel