Liveloop

An interactive timeline for social media. Every post is a tiny app you can play, save, and remix.

Product

  • Feed
  • Create
  • Claude Code plugin
  • Blog

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA

Project

  • Templates
© 2026 Liveloop. All rights reserved.
LiveloopVersion history

v1Current

@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>

v1Current

@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>
← Version history