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:28:28 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 .r{text-align:right;}
+ #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,#22d3ee,#a855f7,#ec4899);-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:bspulse 1.3s ease-in-out infinite;}
+ @keyframes bspulse{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="r"><div class="lab">BEST</div><div id="best">0</div></div>
+ </div>
+ <div id="ov">
+ <h1>BLOCK SNAP</h1>
+ <p class="sub" id="ovsub">Drag a block from the tray onto the grid. Fill a whole row OR column and it clears. It's over when no block fits — leave yourself room!</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');
+ var N=8;
+ var COLORS=['#22d3ee','#a855f7','#ec4899','#a3e635','#fbbf24','#38bdf8'];
+ var SHAPES=[
+ [[0,0]],
+ [[0,0],[0,1]],
+ [[0,0],[1,0]],
+ [[0,0],[0,1],[0,2]],
+ [[0,0],[1,0],[2,0]],
+ [[0,0],[0,1],[0,2],[0,3]],
+ [[0,0],[1,0],[2,0],[3,0]],
+ [[0,0],[0,1],[1,0],[1,1]],
+ [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]],
+ [[0,0],[1,0],[1,1]],
+ [[0,1],[1,0],[1,1]],
+ [[0,0],[0,1],[1,0]],
+ [[0,0],[0,1],[1,1]],
+ [[0,0],[1,0],[2,0],[2,1]],
+ [[0,1],[1,1],[2,0],[2,1]]
+ ];
+ var W=0,H=0,dpr=1,cell=0,gridX=0,gridY=0,trayTop=0,trayCell=0;
+ var state='start', score=0, best=0;
+ var board=[], tray=[null,null,null], held=null, parts=[], floaters=[];
+ var ptrX=0,ptrY=0;
+ var lastT=0,raf=0,running=false;
+
+ function emptyBoard(){
+ board=[];
+ for(var r=0;r<N;r++){ var row=[]; for(var c=0;c<N;c++) row.push(null); board.push(row); }
+ }
+ emptyBoard();
+
+ 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.92,H*0.56)/N);
+ gridX=Math.round((W-cell*N)/2);
+ gridY=Math.round(H*0.12);
+ trayTop=gridY+cell*N+Math.round((H-(gridY+cell*N))*0.16);
+ trayCell=cell*0.5;
+ }
+ function dims(shape){
+ var mr=0,mc=0,i;
+ for(i=0;i<shape.length;i++){
+ if(shape[i][0]>mr)mr=shape[i][0];
+ if(shape[i][1]>mc)mc=shape[i][1];
+ }
+ return {rows:mr+1,cols:mc+1};
+ }
+ function randomPiece(){
+ return {
+ shape:SHAPES[Math.floor(Math.random()*SHAPES.length)],
+ color:COLORS[Math.floor(Math.random()*COLORS.length)]
+ };
+ }
+ function refillTray(){ tray=[randomPiece(),randomPiece(),randomPiece()]; }
+ function canPlaceAt(shape,r0,c0){
+ for(var i=0;i<shape.length;i++){
+ var r=r0+shape[i][0], c=c0+shape[i][1];
+ if(r<0||r>=N||c<0||c>=N) return false;
+ if(board[r][c]) return false;
+ }
+ return true;
+ }
+ function canPlaceAnywhere(shape){
+ for(var r=0;r<N;r++) for(var c=0;c<N;c++){ if(canPlaceAt(shape,r,c)) return true; }
+ return false;
+ }
+ function anyMoveLeft(){
+ for(var s=0;s<3;s++){
+ var p=tray[s]; if(!p) continue;
+ if(canPlaceAnywhere(p.shape)) return true;
+ }
+ return false;
+ }
+ // Which rows/cols would become full if 'shape' were placed at r0,c0 —
+ // used to highlight the lines that are about to clear, teaching the rule.
+ function wouldClear(shape,r0,c0){
+ var occ={},i;
+ for(i=0;i<shape.length;i++){ occ[(r0+shape[i][0])+'_'+(c0+shape[i][1])]=1; }
+ function filled(r,c){ return !!board[r][c] || !!occ[r+'_'+c]; }
+ var rows=[],cols=[],r,c,full;
+ for(r=0;r<N;r++){ full=true; for(c=0;c<N;c++) if(!filled(r,c)) full=false; if(full) rows.push(r); }
+ for(c=0;c<N;c++){ full=true; for(r=0;r<N;r++) if(!filled(r,c)) full=false; if(full) cols.push(c); }
+ return {rows:rows,cols:cols};
+ }
+ // Sound — synthesized; the platform unlocks audio on the first tap.
+ var AC=window.AudioContext||window.webkitAudioContext, actx=null;
+ function ensureCtx(){ try{ if(!actx&&AC) actx=new AC(); if(actx&&actx.state==='suspended') actx.resume(); }catch(e){ actx=null; } }
+ function tone(freq,dur,type,vol,delay){
+ if(!actx) return;
+ var t=actx.currentTime+(delay||0), o=actx.createOscillator(), g=actx.createGain();
+ o.type=type||'triangle'; o.frequency.setValueAtTime(freq,t);
+ g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(vol||0.18,t+0.012);
+ g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
+ o.connect(g).connect(actx.destination); o.start(t); o.stop(t+dur+0.02);
+ }
+ function placeSfx(){ ensureCtx(); tone(300,0.07,'square',0.1); }
+ function clearSfx(lines){ ensureCtx(); var sc=[523,659,784,1047],i; for(i=0;i<Math.min(4,lines+1);i++) tone(sc[i],0.18,'triangle',0.2,i*0.06); }
+ function overSfx(){ ensureCtx(); tone(330,0.22,'sawtooth',0.16,0); tone(196,0.36,'sawtooth',0.16,0.12); }
+ function newGame(){
+ emptyBoard(); refillTray(); held=null; parts=[]; floaters=[];
+ score=0; state='playing'; syncHud();
+ }
+ function syncHud(){ scoreEl.textContent=score; bestEl.textContent=best; }
+ function gameOver(){
+ state='over';
+ if(score>best){ best=score; saveBest(); }
+ syncHud();
+ overSfx();
+ ovtitle.textContent='GAME OVER';
+ ovsub.textContent='No room left — none of the three blocks fit anywhere.';
+ ovbig.textContent='Score '+score+' · Best '+best;
+ ov.style.display='flex';
+ }
+ function cellCenter(r,c){ return {x:gridX+c*cell+cell/2,y:gridY+r*cell+cell/2}; }
+ function burst(r,c,color){
+ var p=cellCenter(r,c),i;
+ for(i=0;i<5;i++){
+ var a=Math.random()*6.2832;
+ parts.push({x:p.x,y:p.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 clearLines(){
+ var fullR=[],fullC=[],r,c,full;
+ for(r=0;r<N;r++){ full=true; for(c=0;c<N;c++) if(!board[r][c]) full=false; if(full) fullR.push(r); }
+ for(c=0;c<N;c++){ full=true; for(r=0;r<N;r++) if(!board[r][c]) full=false; if(full) fullC.push(c); }
+ var lines=fullR.length+fullC.length;
+ if(lines===0) return;
+ clearSfx(lines);
+ var i;
+ for(i=0;i<fullR.length;i++){
+ for(c=0;c<N;c++){ if(board[fullR[i]][c]){ burst(fullR[i],c,board[fullR[i]][c]); board[fullR[i]][c]=null; } }
+ }
+ for(i=0;i<fullC.length;i++){
+ for(r=0;r<N;r++){ if(board[r][fullC[i]]){ burst(r,fullC[i],board[r][fullC[i]]); board[r][fullC[i]]=null; } }
+ }
+ var bonus=lines*8*lines;
+ score+=bonus;
+ floaters.push({x:gridX+cell*N/2,y:gridY+cell*N/2,text:'+'+bonus,life:48,color:'#e9d5ff'});
+ }
+ function place(shape,color,r0,c0){
+ var i;
+ for(i=0;i<shape.length;i++) board[r0+shape[i][0]][c0+shape[i][1]]=color;
+ placeSfx();
+ score+=shape.length;
+ clearLines();
+ syncHud();
+ if(!tray[0]&&!tray[1]&&!tray[2]) refillTray();
+ if(!anyMoveLeft()) gameOver();
+ }
+ function snapTarget(){
+ if(!held) return null;
+ var d=dims(held.shape);
+ var cx=ptrX, cy=ptrY-cell*1.7;
+ var tlx=cx-d.cols*cell/2, tly=cy-d.rows*cell/2;
+ var c0=Math.round((tlx-gridX)/cell), r0=Math.round((tly-gridY)/cell);
+ return {r:r0,c:c0,valid:canPlaceAt(held.shape,r0,c0)};
+ }
+ function step(dt){
+ var i;
+ for(i=parts.length-1;i>=0;i--){
+ var p=parts[i];
+ p.x+=p.vx*dt; p.y+=p.vy*dt; p.life-=dt;
+ if(p.life<=0) parts.splice(i,1);
+ }
+ for(i=floaters.length-1;i>=0;i--){
+ var f=floaters[i];
+ f.y-=0.7*dt; f.life-=dt;
+ if(f.life<=0) floaters.splice(i,1);
+ }
+ }
+ function rr(x,y,w,h,rad){
+ var r=Math.min(rad,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();
+ }
+ function drawBlock(x,y,sz,color,alpha){
+ ctx.globalAlpha=alpha;
+ ctx.fillStyle=color;
+ rr(x+sz*0.06,y+sz*0.06,sz*0.88,sz*0.88,sz*0.22); ctx.fill();
+ ctx.fillStyle='rgba(255,255,255,0.26)';
+ rr(x+sz*0.2,y+sz*0.17,sz*0.6,sz*0.18,sz*0.09); ctx.fill();
+ ctx.globalAlpha=1;
+ }
+ function drawPiece(shape,color,ox,oy,sz,alpha){
+ for(var i=0;i<shape.length;i++){
+ drawBlock(ox+shape[i][1]*sz,oy+shape[i][0]*sz,sz,color,alpha);
+ }
+ }
+ 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
+ var r,c;
+ for(r=0;r<N;r++) for(c=0;c<N;c++){
+ var x=gridX+c*cell, y=gridY+r*cell;
+ if(board[r][c]){
+ drawBlock(x,y,cell,board[r][c],1);
+ } else {
+ ctx.fillStyle='rgba(255,255,255,0.045)';
+ rr(x+cell*0.06,y+cell*0.06,cell*0.88,cell*0.88,cell*0.22); ctx.fill();
+ }
+ }
+ // ghost preview — green footprint + clear-line highlight when valid,
+ // red footprint when the piece won't fit there (clear "why not").
+ if(held){
+ var t=snapTarget();
+ if(t&&t.valid){
+ var wc=wouldClear(held.shape,t.r,t.c),k;
+ ctx.fillStyle='rgba(103,232,249,0.18)';
+ for(k=0;k<wc.rows.length;k++) ctx.fillRect(gridX,gridY+wc.rows[k]*cell,cell*N,cell);
+ for(k=0;k<wc.cols.length;k++) ctx.fillRect(gridX+wc.cols[k]*cell,gridY,cell,cell*N);
+ for(var i=0;i<held.shape.length;i++){
+ var gx=gridX+(t.c+held.shape[i][1])*cell, gy=gridY+(t.r+held.shape[i][0])*cell;
+ drawBlock(gx,gy,cell,held.color,0.4);
+ }
+ } else if(t){
+ for(var j=0;j<held.shape.length;j++){
+ var rr2=t.r+held.shape[j][0], cc2=t.c+held.shape[j][1];
+ if(rr2<0||rr2>=N||cc2<0||cc2>=N) continue;
+ ctx.fillStyle='rgba(244,63,94,0.32)';
+ rr(gridX+cc2*cell+cell*0.06,gridY+rr2*cell+cell*0.06,cell*0.88,cell*0.88,cell*0.22); ctx.fill();
+ }
+ }
+ }
+ // tray — pieces that can't be placed anywhere are dimmed, so the
+ // player can see the squeeze coming instead of a sudden game over.
+ for(var s=0;s<3;s++){
+ if(!tray[s] || (held&&held.slot===s)) continue;
+ var d=dims(tray[s].shape);
+ var slotcx=W/6+s*(W/3);
+ var ox=slotcx-d.cols*trayCell/2, oy=trayTop-d.rows*trayCell/2;
+ drawPiece(tray[s].shape,tray[s].color,ox,oy,trayCell,canPlaceAnywhere(tray[s].shape)?0.96:0.3);
+ }
+ // held piece
+ if(held){
+ var dd=dims(held.shape);
+ var hx=ptrX-dd.cols*cell/2, hy=ptrY-cell*1.7-dd.rows*cell/2;
+ ctx.shadowColor=held.color; ctx.shadowBlur=14;
+ drawPiece(held.shape,held.color,hx,hy,cell,1);
+ ctx.shadowBlur=0;
+ }
+ // particles
+ for(i=0;i<parts.length;i++){
+ var pp=parts[i];
+ ctx.globalAlpha=Math.max(0,Math.min(1,pp.life/16));
+ ctx.fillStyle=pp.color;
+ ctx.fillRect(pp.x-2,pp.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/32));
+ ctx.fillStyle=fl.color;
+ ctx.font='900 '+Math.round(cell*0.7)+'px system-ui';
+ ctx.textAlign='center'; ctx.textBaseline='middle';
+ ctx.shadowColor='#a855f7'; 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 ptr(e){
+ var rc=cv.getBoundingClientRect();
+ ptrX=e.clientX-rc.left; ptrY=e.clientY-rc.top;
+ }
+ cv.addEventListener('pointerdown',function(e){
+ if(state!=='playing') return;
+ ptr(e);
+ if(ptrY<gridY+cell*N*0.99) return;
+ var s=Math.floor(ptrX/(W/3));
+ if(s<0)s=0; if(s>2)s=2;
+ if(tray[s]){ held={shape:tray[s].shape,color:tray[s].color,slot:s}; }
+ });
+ cv.addEventListener('pointermove',function(e){ if(held) ptr(e); });
+ function drop(){
+ if(!held) return;
+ var t=snapTarget();
+ if(t&&t.valid){
+ tray[held.slot]=null;
+ place(held.shape,held.color,t.r,t.c);
+ }
+ held=null;
+ }
+ cv.addEventListener('pointerup',function(){ drop(); });
+ cv.addEventListener('pointercancel',function(){ held=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:28:28 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 .r{text-align:right;}
+ #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,#22d3ee,#a855f7,#ec4899);-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:bspulse 1.3s ease-in-out infinite;}
+ @keyframes bspulse{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="r"><div class="lab">BEST</div><div id="best">0</div></div>
+ </div>
+ <div id="ov">
+ <h1>BLOCK SNAP</h1>
+ <p class="sub" id="ovsub">Drag a block from the tray onto the grid. Fill a whole row OR column and it clears. It's over when no block fits — leave yourself room!</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');
+ var N=8;
+ var COLORS=['#22d3ee','#a855f7','#ec4899','#a3e635','#fbbf24','#38bdf8'];
+ var SHAPES=[
+ [[0,0]],
+ [[0,0],[0,1]],
+ [[0,0],[1,0]],
+ [[0,0],[0,1],[0,2]],
+ [[0,0],[1,0],[2,0]],
+ [[0,0],[0,1],[0,2],[0,3]],
+ [[0,0],[1,0],[2,0],[3,0]],
+ [[0,0],[0,1],[1,0],[1,1]],
+ [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]],
+ [[0,0],[1,0],[1,1]],
+ [[0,1],[1,0],[1,1]],
+ [[0,0],[0,1],[1,0]],
+ [[0,0],[0,1],[1,1]],
+ [[0,0],[1,0],[2,0],[2,1]],
+ [[0,1],[1,1],[2,0],[2,1]]
+ ];
+ var W=0,H=0,dpr=1,cell=0,gridX=0,gridY=0,trayTop=0,trayCell=0;
+ var state='start', score=0, best=0;
+ var board=[], tray=[null,null,null], held=null, parts=[], floaters=[];
+ var ptrX=0,ptrY=0;
+ var lastT=0,raf=0,running=false;
+
+ function emptyBoard(){
+ board=[];
+ for(var r=0;r<N;r++){ var row=[]; for(var c=0;c<N;c++) row.push(null); board.push(row); }
+ }
+ emptyBoard();
+
+ 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.92,H*0.56)/N);
+ gridX=Math.round((W-cell*N)/2);
+ gridY=Math.round(H*0.12);
+ trayTop=gridY+cell*N+Math.round((H-(gridY+cell*N))*0.16);
+ trayCell=cell*0.5;
+ }
+ function dims(shape){
+ var mr=0,mc=0,i;
+ for(i=0;i<shape.length;i++){
+ if(shape[i][0]>mr)mr=shape[i][0];
+ if(shape[i][1]>mc)mc=shape[i][1];
+ }
+ return {rows:mr+1,cols:mc+1};
+ }
+ function randomPiece(){
+ return {
+ shape:SHAPES[Math.floor(Math.random()*SHAPES.length)],
+ color:COLORS[Math.floor(Math.random()*COLORS.length)]
+ };
+ }
+ function refillTray(){ tray=[randomPiece(),randomPiece(),randomPiece()]; }
+ function canPlaceAt(shape,r0,c0){
+ for(var i=0;i<shape.length;i++){
+ var r=r0+shape[i][0], c=c0+shape[i][1];
+ if(r<0||r>=N||c<0||c>=N) return false;
+ if(board[r][c]) return false;
+ }
+ return true;
+ }
+ function canPlaceAnywhere(shape){
+ for(var r=0;r<N;r++) for(var c=0;c<N;c++){ if(canPlaceAt(shape,r,c)) return true; }
+ return false;
+ }
+ function anyMoveLeft(){
+ for(var s=0;s<3;s++){
+ var p=tray[s]; if(!p) continue;
+ if(canPlaceAnywhere(p.shape)) return true;
+ }
+ return false;
+ }
+ // Which rows/cols would become full if 'shape' were placed at r0,c0 —
+ // used to highlight the lines that are about to clear, teaching the rule.
+ function wouldClear(shape,r0,c0){
+ var occ={},i;
+ for(i=0;i<shape.length;i++){ occ[(r0+shape[i][0])+'_'+(c0+shape[i][1])]=1; }
+ function filled(r,c){ return !!board[r][c] || !!occ[r+'_'+c]; }
+ var rows=[],cols=[],r,c,full;
+ for(r=0;r<N;r++){ full=true; for(c=0;c<N;c++) if(!filled(r,c)) full=false; if(full) rows.push(r); }
+ for(c=0;c<N;c++){ full=true; for(r=0;r<N;r++) if(!filled(r,c)) full=false; if(full) cols.push(c); }
+ return {rows:rows,cols:cols};
+ }
+ // Sound — synthesized; the platform unlocks audio on the first tap.
+ var AC=window.AudioContext||window.webkitAudioContext, actx=null;
+ function ensureCtx(){ try{ if(!actx&&AC) actx=new AC(); if(actx&&actx.state==='suspended') actx.resume(); }catch(e){ actx=null; } }
+ function tone(freq,dur,type,vol,delay){
+ if(!actx) return;
+ var t=actx.currentTime+(delay||0), o=actx.createOscillator(), g=actx.createGain();
+ o.type=type||'triangle'; o.frequency.setValueAtTime(freq,t);
+ g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(vol||0.18,t+0.012);
+ g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
+ o.connect(g).connect(actx.destination); o.start(t); o.stop(t+dur+0.02);
+ }
+ function placeSfx(){ ensureCtx(); tone(300,0.07,'square',0.1); }
+ function clearSfx(lines){ ensureCtx(); var sc=[523,659,784,1047],i; for(i=0;i<Math.min(4,lines+1);i++) tone(sc[i],0.18,'triangle',0.2,i*0.06); }
+ function overSfx(){ ensureCtx(); tone(330,0.22,'sawtooth',0.16,0); tone(196,0.36,'sawtooth',0.16,0.12); }
+ function newGame(){
+ emptyBoard(); refillTray(); held=null; parts=[]; floaters=[];
+ score=0; state='playing'; syncHud();
+ }
+ function syncHud(){ scoreEl.textContent=score; bestEl.textContent=best; }
+ function gameOver(){
+ state='over';
+ if(score>best){ best=score; saveBest(); }
+ syncHud();
+ overSfx();
+ ovtitle.textContent='GAME OVER';
+ ovsub.textContent='No room left — none of the three blocks fit anywhere.';
+ ovbig.textContent='Score '+score+' · Best '+best;
+ ov.style.display='flex';
+ }
+ function cellCenter(r,c){ return {x:gridX+c*cell+cell/2,y:gridY+r*cell+cell/2}; }
+ function burst(r,c,color){
+ var p=cellCenter(r,c),i;
+ for(i=0;i<5;i++){
+ var a=Math.random()*6.2832;
+ parts.push({x:p.x,y:p.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 clearLines(){
+ var fullR=[],fullC=[],r,c,full;
+ for(r=0;r<N;r++){ full=true; for(c=0;c<N;c++) if(!board[r][c]) full=false; if(full) fullR.push(r); }
+ for(c=0;c<N;c++){ full=true; for(r=0;r<N;r++) if(!board[r][c]) full=false; if(full) fullC.push(c); }
+ var lines=fullR.length+fullC.length;
+ if(lines===0) return;
+ clearSfx(lines);
+ var i;
+ for(i=0;i<fullR.length;i++){
+ for(c=0;c<N;c++){ if(board[fullR[i]][c]){ burst(fullR[i],c,board[fullR[i]][c]); board[fullR[i]][c]=null; } }
+ }
+ for(i=0;i<fullC.length;i++){
+ for(r=0;r<N;r++){ if(board[r][fullC[i]]){ burst(r,fullC[i],board[r][fullC[i]]); board[r][fullC[i]]=null; } }
+ }
+ var bonus=lines*8*lines;
+ score+=bonus;
+ floaters.push({x:gridX+cell*N/2,y:gridY+cell*N/2,text:'+'+bonus,life:48,color:'#e9d5ff'});
+ }
+ function place(shape,color,r0,c0){
+ var i;
+ for(i=0;i<shape.length;i++) board[r0+shape[i][0]][c0+shape[i][1]]=color;
+ placeSfx();
+ score+=shape.length;
+ clearLines();
+ syncHud();
+ if(!tray[0]&&!tray[1]&&!tray[2]) refillTray();
+ if(!anyMoveLeft()) gameOver();
+ }
+ function snapTarget(){
+ if(!held) return null;
+ var d=dims(held.shape);
+ var cx=ptrX, cy=ptrY-cell*1.7;
+ var tlx=cx-d.cols*cell/2, tly=cy-d.rows*cell/2;
+ var c0=Math.round((tlx-gridX)/cell), r0=Math.round((tly-gridY)/cell);
+ return {r:r0,c:c0,valid:canPlaceAt(held.shape,r0,c0)};
+ }
+ function step(dt){
+ var i;
+ for(i=parts.length-1;i>=0;i--){
+ var p=parts[i];
+ p.x+=p.vx*dt; p.y+=p.vy*dt; p.life-=dt;
+ if(p.life<=0) parts.splice(i,1);
+ }
+ for(i=floaters.length-1;i>=0;i--){
+ var f=floaters[i];
+ f.y-=0.7*dt; f.life-=dt;
+ if(f.life<=0) floaters.splice(i,1);
+ }
+ }
+ function rr(x,y,w,h,rad){
+ var r=Math.min(rad,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();
+ }
+ function drawBlock(x,y,sz,color,alpha){
+ ctx.globalAlpha=alpha;
+ ctx.fillStyle=color;
+ rr(x+sz*0.06,y+sz*0.06,sz*0.88,sz*0.88,sz*0.22); ctx.fill();
+ ctx.fillStyle='rgba(255,255,255,0.26)';
+ rr(x+sz*0.2,y+sz*0.17,sz*0.6,sz*0.18,sz*0.09); ctx.fill();
+ ctx.globalAlpha=1;
+ }
+ function drawPiece(shape,color,ox,oy,sz,alpha){
+ for(var i=0;i<shape.length;i++){
+ drawBlock(ox+shape[i][1]*sz,oy+shape[i][0]*sz,sz,color,alpha);
+ }
+ }
+ 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
+ var r,c;
+ for(r=0;r<N;r++) for(c=0;c<N;c++){
+ var x=gridX+c*cell, y=gridY+r*cell;
+ if(board[r][c]){
+ drawBlock(x,y,cell,board[r][c],1);
+ } else {
+ ctx.fillStyle='rgba(255,255,255,0.045)';
+ rr(x+cell*0.06,y+cell*0.06,cell*0.88,cell*0.88,cell*0.22); ctx.fill();
+ }
+ }
+ // ghost preview — green footprint + clear-line highlight when valid,
+ // red footprint when the piece won't fit there (clear "why not").
+ if(held){
+ var t=snapTarget();
+ if(t&&t.valid){
+ var wc=wouldClear(held.shape,t.r,t.c),k;
+ ctx.fillStyle='rgba(103,232,249,0.18)';
+ for(k=0;k<wc.rows.length;k++) ctx.fillRect(gridX,gridY+wc.rows[k]*cell,cell*N,cell);
+ for(k=0;k<wc.cols.length;k++) ctx.fillRect(gridX+wc.cols[k]*cell,gridY,cell,cell*N);
+ for(var i=0;i<held.shape.length;i++){
+ var gx=gridX+(t.c+held.shape[i][1])*cell, gy=gridY+(t.r+held.shape[i][0])*cell;
+ drawBlock(gx,gy,cell,held.color,0.4);
+ }
+ } else if(t){
+ for(var j=0;j<held.shape.length;j++){
+ var rr2=t.r+held.shape[j][0], cc2=t.c+held.shape[j][1];
+ if(rr2<0||rr2>=N||cc2<0||cc2>=N) continue;
+ ctx.fillStyle='rgba(244,63,94,0.32)';
+ rr(gridX+cc2*cell+cell*0.06,gridY+rr2*cell+cell*0.06,cell*0.88,cell*0.88,cell*0.22); ctx.fill();
+ }
+ }
+ }
+ // tray — pieces that can't be placed anywhere are dimmed, so the
+ // player can see the squeeze coming instead of a sudden game over.
+ for(var s=0;s<3;s++){
+ if(!tray[s] || (held&&held.slot===s)) continue;
+ var d=dims(tray[s].shape);
+ var slotcx=W/6+s*(W/3);
+ var ox=slotcx-d.cols*trayCell/2, oy=trayTop-d.rows*trayCell/2;
+ drawPiece(tray[s].shape,tray[s].color,ox,oy,trayCell,canPlaceAnywhere(tray[s].shape)?0.96:0.3);
+ }
+ // held piece
+ if(held){
+ var dd=dims(held.shape);
+ var hx=ptrX-dd.cols*cell/2, hy=ptrY-cell*1.7-dd.rows*cell/2;
+ ctx.shadowColor=held.color; ctx.shadowBlur=14;
+ drawPiece(held.shape,held.color,hx,hy,cell,1);
+ ctx.shadowBlur=0;
+ }
+ // particles
+ for(i=0;i<parts.length;i++){
+ var pp=parts[i];
+ ctx.globalAlpha=Math.max(0,Math.min(1,pp.life/16));
+ ctx.fillStyle=pp.color;
+ ctx.fillRect(pp.x-2,pp.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/32));
+ ctx.fillStyle=fl.color;
+ ctx.font='900 '+Math.round(cell*0.7)+'px system-ui';
+ ctx.textAlign='center'; ctx.textBaseline='middle';
+ ctx.shadowColor='#a855f7'; 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 ptr(e){
+ var rc=cv.getBoundingClientRect();
+ ptrX=e.clientX-rc.left; ptrY=e.clientY-rc.top;
+ }
+ cv.addEventListener('pointerdown',function(e){
+ if(state!=='playing') return;
+ ptr(e);
+ if(ptrY<gridY+cell*N*0.99) return;
+ var s=Math.floor(ptrX/(W/3));
+ if(s<0)s=0; if(s>2)s=2;
+ if(tray[s]){ held={shape:tray[s].shape,color:tray[s].color,slot:s}; }
+ });
+ cv.addEventListener('pointermove',function(e){ if(held) ptr(e); });
+ function drop(){
+ if(!held) return;
+ var t=snapTarget();
+ if(t&&t.valid){
+ tray[held.slot]=null;
+ place(held.shape,held.color,t.r,t.c);
+ }
+ held=null;
+ }
+ cv.addEventListener('pointerup',function(){ drop(); });
+ cv.addEventListener('pointercancel',function(){ held=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