@liveloop · 5/18/2026, 12:43:47 PM
Initial version — all lines are new.
+<style>+ html,body{height:100%;margin:0;background:#070612;overflow:hidden;}+ *{box-sizing:border-box;}+ #wrap{position:relative;height:100%;width:100%;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}+ canvas{display:block;width:100%;height:100%;touch-action:none;}+ #hud{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;padding:.7rem .95rem;color:#e9d5ff;font-size:.86rem;font-weight:800;letter-spacing:.04em;pointer-events:none;text-shadow:0 0 10px rgba(0,0,0,.95);}+ #hud .lab{font-size:.56rem;opacity:.55;letter-spacing:.14em;margin-bottom:.05rem;}+ #hud .c{text-align:center;}+ #hud .r{text-align:right;}+ #time.low{color:#fb7185;}+ #ov{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;text-align:center;padding:1.6rem;background:rgba(7,6,18,.86);color:#e9d5ff;cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:none;user-select:none;-webkit-user-select:none;}+ #ov h1{margin:0;font-size:2.05rem;font-weight:900;letter-spacing:.02em;background:linear-gradient(90deg,#fb7185,#fbbf24,#a3e635,#22d3ee,#a855f7);-webkit-background-clip:text;background-clip:text;color:transparent;}+ #ov .sub{margin:.1rem 0 0;font-size:.9rem;color:#a5b4fc;max-width:26ch;}+ #ov .big{margin:.1rem 0 0;font-size:1.05rem;font-weight:800;color:#e9d5ff;}+ #ov .pulse{margin-top:.5rem;font-size:.84rem;color:#67e8f9;animation:gcpulse 1.3s ease-in-out infinite;}+ @keyframes gcpulse{0%,100%{opacity:.35}50%{opacity:1}}+</style>+<div id="wrap">+ <canvas id="cv"></canvas>+ <div id="hud">+ <div><div class="lab">SCORE</div><div id="score">0</div></div>+ <div class="c"><div class="lab">TIME</div><div id="time">60</div></div>+ <div class="r"><div class="lab">BEST</div><div id="best">0</div></div>+ </div>+ <div id="ov">+ <h1>GEM CASCADE</h1>+ <p class="sub" id="ovsub">Swipe a gem to swap it with a neighbour. Line up three of a colour.</p>+ <p class="big" id="ovbig"></p>+ <p class="pulse">Tap to play</p>+ </div>+</div>+<script>+(function(){+ var LL=window.liveloop||null;+ var cv=document.getElementById('cv'), ctx=cv.getContext('2d');+ var ov=document.getElementById('ov'), ovsub=document.getElementById('ovsub'),+ ovbig=document.getElementById('ovbig'), ovtitle=ov.querySelector('h1');+ var scoreEl=document.getElementById('score'), bestEl=document.getElementById('best'),+ timeEl=document.getElementById('time');+ var N=7;+ var GEMS=['#fb7185','#fbbf24','#a3e635','#22d3ee','#a855f7','#ec4899'];+ var W=0,H=0,dpr=1,cell=0,gridX=0,gridY=0;+ var state='start', score=0, best=0, timeLeft=60;+ var grid=[], parts=[], floaters=[];+ var phase='idle', timer=0, swapA=null, swapB=null, combo=0;+ var sel=null, downX=0, downY=0;+ var lastT=0, raf=0, running=false;++ function setCanvas(){+ var rc=cv.getBoundingClientRect();+ W=Math.max(200,rc.width); H=Math.max(280,rc.height);+ dpr=Math.min(window.devicePixelRatio||1,2.5);+ cv.width=Math.round(W*dpr); cv.height=Math.round(H*dpr);+ ctx.setTransform(dpr,0,0,dpr,0,0);+ cell=Math.floor(Math.min(W*0.93,H*0.56)/N);+ gridX=Math.round((W-cell*N)/2);+ gridY=Math.round(H*0.13);+ }+ function tx(c){ return gridX+c*cell+cell/2; }+ function ty(r){ return gridY+r*cell+cell/2; }+ function rnd(n){ return Math.floor(Math.random()*n); }+ function buildBoard(){+ grid=[];+ for(var r=0;r<N;r++){+ var row=[];+ for(var c=0;c<N;c++){+ var col;+ do{ col=rnd(GEMS.length); }+ while(+ (c>=2 && row[c-1].color===col && row[c-2].color===col) ||+ (r>=2 && grid[r-1][c].color===col && grid[r-2][c].color===col)+ );+ row.push({color:col,px:tx(c),py:ty(r)});+ }+ grid.push(row);+ }+ }+ function newGame(){+ score=0; timeLeft=60; combo=0; phase='idle'; sel=null;+ parts=[]; floaters=[];+ buildBoard();+ state='playing'; syncHud();+ }+ function syncHud(){+ scoreEl.textContent=score; bestEl.textContent=best;+ var s=Math.max(0,Math.ceil(timeLeft));+ timeEl.textContent=s;+ if(s<=10) timeEl.classList.add('low'); else timeEl.classList.remove('low');+ }+ function gameOver(){+ state='over';+ if(score>best){ best=score; saveBest(); }+ syncHud();+ ovtitle.textContent='TIME UP';+ ovsub.textContent='Sweet run.';+ ovbig.textContent='Score '+score+' · Best '+best;+ ov.style.display='flex';+ }+ function burst(x,y,color){+ for(var i=0;i<6;i++){+ var a=Math.random()*6.2832;+ parts.push({x:x,y:y,vx:Math.cos(a)*(1+Math.random()*3),+ vy:Math.sin(a)*(1+Math.random()*3),life:14+Math.random()*12,color:color});+ }+ }+ function findMatches(){+ var m={}, r,c,run,k;+ for(r=0;r<N;r++){+ c=0;+ while(c<N){+ run=1;+ while(c+run<N && grid[r][c+run] && grid[r][c] &&+ grid[r][c+run].color===grid[r][c].color) run++;+ if(run>=3) for(k=0;k<run;k++) m[r+'_'+(c+k)]=true;+ c+=run;+ }+ }+ for(c=0;c<N;c++){+ r=0;+ while(r<N){+ run=1;+ while(r+run<N && grid[r+run][c] && grid[r][c] &&+ grid[r+run][c].color===grid[r][c].color) run++;+ if(run>=3) for(k=0;k<run;k++) m[(r+k)+'_'+c]=true;+ r+=run;+ }+ }+ return m;+ }+ function applyGravity(){+ for(var c=0;c<N;c++){+ var colArr=[],r;+ for(r=0;r<N;r++) if(grid[r][c]) colArr.push(grid[r][c]);+ var holes=N-colArr.length;+ for(r=0;r<N;r++){+ if(r<holes){+ grid[r][c]={color:rnd(GEMS.length),px:tx(c),py:ty(r)-cell*(holes+1)};+ } else {+ grid[r][c]=colArr[r-holes];+ }+ }+ }+ }+ function resolveAll(){+ combo=0;+ while(combo<40){+ var m=findMatches();+ var keys=Object.keys(m);+ if(keys.length===0) break;+ combo++;+ var i,parts2=keys.length;+ for(i=0;i<keys.length;i++){+ var p=keys[i].split('_'), r=+p[0], c=+p[1];+ if(grid[r][c]){+ burst(grid[r][c].px,grid[r][c].py,GEMS[grid[r][c].color]);+ grid[r][c]=null;+ }+ }+ score+=parts2*10*combo;+ applyGravity();+ }+ if(combo>=2){+ floaters.push({x:W/2,y:gridY+cell*N/2,text:'Combo x'+combo,life:54,color:'#fde68a'});+ }+ syncHud();+ }+ function attemptSwap(r1,c1,r2,c2){+ var tmp=grid[r1][c1]; grid[r1][c1]=grid[r2][c2]; grid[r2][c2]=tmp;+ swapA={r:r1,c:c1}; swapB={r:r2,c:c2};+ phase='swap'; timer=10;+ }+ function step(dt){+ if(state==='playing'){+ timeLeft-=dt/60;+ if(timeLeft<=0){ timeLeft=0; gameOver(); }+ }+ if(timer>0){+ timer-=dt;+ if(timer<=0){+ if(phase==='swap'){+ var m=findMatches();+ if(Object.keys(m).length>0){+ resolveAll();+ phase='settle'; timer=20;+ } else {+ var a=swapA,b=swapB;+ var tmp=grid[a.r][a.c]; grid[a.r][a.c]=grid[b.r][b.c]; grid[b.r][b.c]=tmp;+ phase='unswap'; timer=10;+ }+ } else {+ phase='idle'; timer=0;+ }+ }+ }+ // ease gem render positions toward their cell targets+ var r,c,ease=Math.min(1,0.3*dt);+ for(r=0;r<N;r++) for(c=0;c<N;c++){+ var g=grid[r]&&grid[r][c];+ if(!g) continue;+ g.px+=(tx(c)-g.px)*ease;+ g.py+=(ty(r)-g.py)*ease;+ }+ var i;+ for(i=parts.length-1;i>=0;i--){+ var pt=parts[i];+ pt.x+=pt.vx*dt; pt.y+=pt.vy*dt; pt.life-=dt;+ if(pt.life<=0) parts.splice(i,1);+ }+ for(i=floaters.length-1;i>=0;i--){+ var fl=floaters[i];+ fl.y-=0.55*dt; fl.life-=dt;+ if(fl.life<=0) floaters.splice(i,1);+ }+ }+ function drawGem(g,rad){+ ctx.fillStyle=GEMS[g.color];+ ctx.beginPath(); ctx.arc(g.px,g.py,rad,0,6.2832); ctx.fill();+ ctx.fillStyle='rgba(255,255,255,0.4)';+ ctx.beginPath(); ctx.arc(g.px-rad*0.3,g.py-rad*0.32,rad*0.34,0,6.2832); ctx.fill();+ }+ function render(){+ var bg=ctx.createLinearGradient(0,0,0,H);+ bg.addColorStop(0,'#120d2e'); bg.addColorStop(1,'#070612');+ ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);+ // grid backing+ var r,c;+ for(r=0;r<N;r++) for(c=0;c<N;c++){+ ctx.fillStyle=(r+c)%2?'rgba(255,255,255,0.03)':'rgba(255,255,255,0.055)';+ ctx.fillRect(gridX+c*cell,gridY+r*cell,cell,cell);+ }+ // clip gems to the grid so falling gems stay hidden above it+ ctx.save();+ ctx.beginPath();+ ctx.rect(gridX,gridY,cell*N,cell*N);+ ctx.clip();+ var rad=cell*0.38;+ for(r=0;r<N;r++) for(c=0;c<N;c++){+ var g=grid[r]&&grid[r][c];+ if(g) drawGem(g,rad);+ }+ ctx.restore();+ // selection ring+ if(sel && grid[sel.r] && grid[sel.r][sel.c]){+ ctx.strokeStyle='#ffffff'; ctx.lineWidth=3;+ ctx.beginPath();+ ctx.arc(grid[sel.r][sel.c].px,grid[sel.r][sel.c].py,rad+5,0,6.2832);+ ctx.stroke();+ }+ var i;+ for(i=0;i<parts.length;i++){+ var pt=parts[i];+ ctx.globalAlpha=Math.max(0,Math.min(1,pt.life/16));+ ctx.fillStyle=pt.color;+ ctx.fillRect(pt.x-2,pt.y-2,4,4);+ }+ ctx.globalAlpha=1;+ for(i=0;i<floaters.length;i++){+ var fl=floaters[i];+ ctx.globalAlpha=Math.max(0,Math.min(1,fl.life/34));+ ctx.fillStyle=fl.color;+ ctx.font='900 '+Math.round(cell*0.62)+'px system-ui';+ ctx.textAlign='center'; ctx.textBaseline='middle';+ ctx.shadowColor='#f59e0b'; ctx.shadowBlur=16;+ ctx.fillText(fl.text,fl.x,fl.y);+ ctx.shadowBlur=0;+ }+ ctx.globalAlpha=1;+ }+ function frame(now){+ if(!running) return;+ var dt=(now-lastT)/16.667; lastT=now;+ if(dt<0.2)dt=0.2; if(dt>2.6)dt=2.6;+ step(dt); render();+ raf=requestAnimationFrame(frame);+ }+ function startLoop(){+ if(running) return;+ running=true; lastT=performance.now(); raf=requestAnimationFrame(frame);+ }+ function stopLoop(){ running=false; if(raf)cancelAnimationFrame(raf); raf=0; }+ function saveBest(){+ if(LL&&LL.storage&&LL.storage.set){ try{ LL.storage.set({best:best}); }catch(e){} }+ }+ function loadBest(){+ if(LL&&LL.storage&&LL.storage.get){+ LL.storage.get().then(function(s){+ if(s&&typeof s.best==='number'&&s.best>best){ best=s.best; syncHud(); }+ },function(){});+ }+ }+ function cellAt(x,y){+ var c=Math.floor((x-gridX)/cell), r=Math.floor((y-gridY)/cell);+ if(r<0||r>=N||c<0||c>=N) return null;+ return {r:r,c:c};+ }+ cv.addEventListener('pointerdown',function(e){+ if(state!=='playing'||phase!=='idle') return;+ var rc=cv.getBoundingClientRect();+ downX=e.clientX-rc.left; downY=e.clientY-rc.top;+ sel=cellAt(downX,downY);+ });+ cv.addEventListener('pointerup',function(e){+ if(!sel||phase!=='idle'){ sel=null; return; }+ var rc=cv.getBoundingClientRect();+ var dx=(e.clientX-rc.left)-downX, dy=(e.clientY-rc.top)-downY;+ var s=sel; sel=null;+ if(Math.abs(dx)<cell*0.28 && Math.abs(dy)<cell*0.28) return;+ var r2=s.r, c2=s.c;+ if(Math.abs(dx)>Math.abs(dy)) c2+=dx>0?1:-1; else r2+=dy>0?1:-1;+ if(r2<0||r2>=N||c2<0||c2>=N) return;+ attemptSwap(s.r,s.c,r2,c2);+ });+ cv.addEventListener('pointercancel',function(){ sel=null; });+ ov.addEventListener('pointerdown',function(e){+ e.preventDefault();+ ov.style.display='none';+ newGame();+ });+ function onResize(){ setCanvas(); }+ window.addEventListener('resize',onResize);+ if(LL && LL.onResize) LL.onResize(onResize);+ if(LL && LL.onVisibility) LL.onVisibility(function(v){ if(v) startLoop(); else stopLoop(); });+ document.addEventListener('visibilitychange',function(){+ if(document.hidden) stopLoop(); else startLoop();+ });++ setCanvas(); syncHud(); loadBest(); startLoop();+})();+</script>