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