@liveloop · 6/9/2026, 6:18:03 PM
Initial version — all lines are new.
+<style>+ html,body{height:100%;margin:0;overflow:hidden;background:#F4EEE3;}+ *{box-sizing:border-box;}+ #wrap{position:relative;height:100%;width:100%;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none;}+ canvas{display:block;width:100%;height:100%;position:absolute;inset:0;touch-action:none;}+ #head{position:absolute;top:0;left:0;right:0;padding:1.15rem 1.25rem .4rem;pointer-events:none;z-index:3;}+ #head h1{margin:0;font-family:"Instrument Serif",Georgia,"Times New Roman",serif;font-weight:400;font-size:2.35rem;line-height:1;color:#16110D;}+ #head p{margin:.25rem 0 0;font-size:.82rem;color:#8F857B;font-weight:500;}+ #count{position:absolute;top:4.1rem;left:1.25rem;z-index:3;pointer-events:none;font-size:.72rem;font-weight:700;color:#6B4EFE;background:#E8E2FF;border-radius:999px;padding:.32rem .7rem;letter-spacing:.02em;}+ #bar{position:absolute;left:0;right:0;bottom:0;z-index:3;display:flex;gap:.5rem;padding:.8rem .9rem calc(.8rem + env(safe-area-inset-bottom));background:linear-gradient(180deg,rgba(244,238,227,0),rgba(244,238,227,.92) 38%);}+ #inp{flex:1;min-width:0;border:1px solid #ECE7DF;background:#fff;border-radius:999px;padding:.7rem 1rem;font-size:.95rem;color:#16110D;outline:none;box-shadow:0 4px 16px rgba(60,32,217,.06);}+ #inp::placeholder{color:#B6ADA2;}+ #inp:focus{border-color:#C9BEFF;box-shadow:0 0 0 3px rgba(124,92,255,.16);}+ #send{flex:0 0 auto;border:0;border-radius:999px;width:3rem;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#6B4EFE,#FF5A1F);color:#fff;font-size:1.2rem;font-weight:800;cursor:pointer;box-shadow:0 6px 18px rgba(108,78,210,.32);}+ #send:active{transform:scale(.94);}+ #toast{position:absolute;left:50%;top:46%;transform:translate(-50%,-50%);z-index:4;max-width:78%;text-align:center;background:#fff;border:1px solid #ECE7DF;border-radius:18px;padding:.85rem 1.1rem;font-size:1.02rem;color:#16110D;box-shadow:0 12px 36px rgba(0,0,0,.12);opacity:0;pointer-events:none;transition:opacity .25s ease, transform .25s ease;font-family:"Instrument Serif",Georgia,serif;}+ #toast.show{opacity:1;transform:translate(-50%,-50%) scale(1);}+ #toast .lab{display:block;font-family:system-ui,sans-serif;font-size:.6rem;letter-spacing:.16em;text-transform:uppercase;color:#8F857B;margin-bottom:.3rem;font-weight:700;}+</style>+<div id="wrap">+ <canvas id="orbs"></canvas>+ <canvas id="cv"></canvas>+ <div id="head"><h1>Gratitude Jar</h1><p>Add one good thing. Watch the jar fill.</p></div>+ <div id="count">0</div>+ <div id="toast"><span class="lab">you wrote</span><span id="toasttxt"></span></div>+ <div id="bar">+ <input id="inp" type="text" maxlength="80" autocomplete="off" placeholder="What are you grateful for?" />+ <button id="send" aria-label="Add to jar">+</button>+ </div>+</div>+<script>+(function(){+ var LL=window.liveloop||null;+ // --- sound: short tap SFX gated on the feed-wide mute; declareMedia so the+ // platform draws the sound control (carries across loops); SDK auto-unlocks. ---+ var muted = LL ? LL.muted : true;+ if(LL && LL.onMute){ try{ LL.onMute(function(m){ muted=m; }); }catch(e){} }+ if(LL && LL.declareMedia){ try{ LL.declareMedia({sound:true}); }catch(e){} }+ var _ac=null;+ function actx(){ if(!_ac){ try{ _ac=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ _ac=null; } } return _ac; }+ function tone(freq,dur,type,vol,delay){+ if(muted) return; var c=actx(); if(!c) return;+ var t=c.currentTime+(delay||0), o=c.createOscillator(), g=c.createGain();+ o.type=type||'sine'; o.frequency.setValueAtTime(freq,t);+ g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(vol||0.16,t+0.014);+ g.gain.exponentialRampToValueAtTime(0.0001,t+dur);+ o.connect(g); g.connect(c.destination); o.start(t); o.stop(t+dur+0.03);+ }+ function plip(){ tone(392,0.13,'sine',0.14); tone(587.33,0.22,'sine',0.07,0.04); }+ var wrap=document.getElementById('wrap');+ var orbC=document.getElementById('orbs'), oc=orbC.getContext('2d');+ var cv=document.getElementById('cv'), ctx=cv.getContext('2d');+ var inp=document.getElementById('inp'), send=document.getElementById('send');+ var countEl=document.getElementById('count');+ var toast=document.getElementById('toast'), toastTxt=document.getElementById('toasttxt');+ var W=0,H=0,dpr=1,T=0,raf=0,paused=false;+ var notes=[], texts=[], COLORS=['#6B4EFE','#FF5A1F','#E14C8F','#3C9A6A','#F6A93B','#7C5CFF'];+ var jar={};++ function computeJar(){+ var jw=Math.min(W*0.6,260), jh=Math.min(H*0.46,360);+ jar={ x:(W-jw)/2, y:H*0.30, w:jw, h:jh, neck:jw*0.42, lip:14 };+ jar.floor=jar.y+jar.h-16;+ }+ function size(){+ var r=wrap.getBoundingClientRect();+ W=Math.max(170,r.width); H=Math.max(240,r.height);+ dpr=Math.min(window.devicePixelRatio||1,2.5);+ [orbC,cv].forEach(function(c){ c.width=Math.round(W*dpr); c.height=Math.round(H*dpr); });+ oc.setTransform(dpr,0,0,dpr,0,0); ctx.setTransform(dpr,0,0,dpr,0,0);+ computeJar(); drawOrbs();+ }+ function blob(x,y,r,col){+ var g=oc.createRadialGradient(x,y,0,x,y,r);+ g.addColorStop(0,col); g.addColorStop(1,'rgba(244,238,227,0)');+ oc.fillStyle=g; oc.beginPath(); oc.arc(x,y,r,0,Math.PI*2); oc.fill();+ }+ function drawOrbs(){+ oc.clearRect(0,0,W,H);+ var bg=oc.createLinearGradient(0,0,0,H);+ bg.addColorStop(0,'#F8F3EA'); bg.addColorStop(1,'#F2EADC');+ oc.fillStyle=bg; oc.fillRect(0,0,W,H);+ blob(W*0.85,H*0.12,W*0.5,'rgba(232,226,255,0.9)');+ blob(W*0.10,H*0.16,W*0.46,'rgba(255,227,210,0.85)');+ blob(W*0.9,H*0.9,W*0.5,'rgba(255,210,225,0.5)');+ }++ function rrect(c,x,y,w,h,r){+ c.beginPath();+ c.moveTo(x+r,y); c.arcTo(x+w,y,x+w,y+h,r); c.arcTo(x+w,y+h,x,y+h,r);+ c.arcTo(x,y+h,x,y,r); c.arcTo(x,y,x+w,y,r); c.closePath();+ }++ function drawJar(){+ var j=jar;+ // glass body+ ctx.save();+ rrect(ctx,j.x,j.y,j.w,j.h,26); ctx.clip();+ // inner soft fill+ var g=ctx.createLinearGradient(0,j.y,0,j.y+j.h);+ g.addColorStop(0,'rgba(255,255,255,0.42)'); g.addColorStop(1,'rgba(232,226,255,0.30)');+ ctx.fillStyle=g; ctx.fillRect(j.x,j.y,j.w,j.h);+ ctx.restore();+ // notes are drawn clipped to the jar too+ ctx.save();+ rrect(ctx,j.x,j.y,j.w,j.h,26); ctx.clip();+ for(var i=0;i<notes.length;i++) drawNote(notes[i]);+ ctx.restore();+ // glass outline + highlight+ rrect(ctx,j.x,j.y,j.w,j.h,26);+ ctx.strokeStyle='rgba(108,78,210,0.28)'; ctx.lineWidth=2.4; ctx.stroke();+ ctx.strokeStyle='rgba(255,255,255,0.7)'; ctx.lineWidth=4;+ ctx.beginPath(); ctx.moveTo(j.x+16,j.y+30); ctx.lineTo(j.x+16,j.y+j.h-40); ctx.stroke();+ // lid+ var lx=j.x+(j.w-j.neck)/2;+ rrect(ctx,lx,j.y-j.lip-6,j.neck,j.lip+10,7);+ var lg=ctx.createLinearGradient(0,j.y-j.lip,0,j.y);+ lg.addColorStop(0,'#FF7A3C'); lg.addColorStop(1,'#FF5A1F');+ ctx.fillStyle=lg; ctx.fill();+ ctx.fillStyle='rgba(255,255,255,0.25)'; rrect(ctx,lx,j.y-j.lip-6,j.neck,5,4); ctx.fill();+ }++ function drawNote(n){+ ctx.save();+ ctx.translate(n.x,n.y); ctx.rotate(n.rot);+ ctx.shadowColor='rgba(60,32,217,0.18)'; ctx.shadowBlur=8; ctx.shadowOffsetY=2;+ rrect(ctx,-n.w/2,-n.h/2,n.w,n.h,5);+ var g=ctx.createLinearGradient(-n.w/2,-n.h/2,n.w/2,n.h/2);+ g.addColorStop(0,n.col2); g.addColorStop(1,n.col);+ ctx.fillStyle=g; ctx.fill();+ ctx.shadowBlur=0; ctx.shadowOffsetY=0;+ // fold line+ ctx.strokeStyle='rgba(255,255,255,0.5)'; ctx.lineWidth=1.4;+ ctx.beginPath(); ctx.moveTo(-n.w/2+4,-n.h/6); ctx.lineTo(n.w/2-4,-n.h/6); ctx.stroke();+ ctx.beginPath(); ctx.moveTo(-n.w/2+4,n.h/6); ctx.lineTo(n.w/2-4,n.h/6); ctx.stroke();+ ctx.restore();+ }++ function settleY(){+ // stack height grows with note count, clamped to jar floor.+ var per=14; var stack=Math.min(jar.h-30, notes.length*per);+ return jar.floor - stack + (Math.random()*8-4);+ }++ function add(text, restore){+ if(notes.length>120) return;+ var hue=notes.length%COLORS.length;+ // Place instantly (no fall) when restoring saved notes OR when we're+ // paused/off-screen — otherwise a note added while the RAF is stopped+ // would freeze mid-air. A live, on-screen add still drops + bounces.+ var instant=restore||paused||document.hidden;+ var rest=settleY();+ var n={+ x:jar.x+jar.w*(0.22+Math.random()*0.56),+ y:instant?rest:jar.y-10,+ vy:instant?0:0.4, ty:rest,+ w:jar.w*0.30+Math.random()*10, h:14,+ rot:(Math.random()-0.5)*0.7, vr:(Math.random()-0.5)*0.06,+ col:COLORS[hue], col2:COLORS[(hue+3)%COLORS.length],+ idx:texts.length, settled:instant+ };+ notes.push(n);+ texts.push(text||'');+ countEl.textContent=notes.length;+ if(!restore){ flash(text); persist(); plip(); }+ }++ function flash(text){+ if(!text) return;+ toastTxt.textContent='“'+text+'”';+ toast.classList.add('show');+ clearTimeout(flash._t); flash._t=setTimeout(function(){ toast.classList.remove('show'); },1700);+ }+ function persist(){+ if(LL && LL.storage){ try{ LL.storage.set({texts:texts}); }catch(e){} }+ }++ function frame(now){+ raf=requestAnimationFrame(frame);+ if(paused) return;+ var dt=Math.min(2.2,(now-T)/16.67); T=now;+ ctx.clearRect(0,0,W,H);+ for(var i=0;i<notes.length;i++){+ var n=notes[i];+ if(!n.settled){+ n.vy+=0.5*dt; n.y+=n.vy*dt; n.rot+=n.vr*dt;+ if(n.y>=n.ty){ n.y=n.ty; n.vy*=-0.28; n.vr*=0.5; if(Math.abs(n.vy)<0.6){ n.settled=true; n.vy=0; } }+ }+ }+ drawJar();+ }++ function submit(){+ var v=inp.value.trim();+ if(!v) { inp.focus(); return; }+ add(v,false); inp.value='';+ if(LL && LL.confetti){ try{ LL.confetti(); }catch(e){} }+ }+ send.addEventListener('click',submit);+ inp.addEventListener('keydown',function(e){ if(e.key==='Enter') submit(); });++ // Tap a note in the jar to re-read what you wrote.+ cv.addEventListener('pointerdown',function(e){+ var r=cv.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top;+ for(var i=notes.length-1;i>=0;i--){+ var n=notes[i];+ if(Math.abs(mx-n.x)<n.w/2+6 && Math.abs(my-n.y)<14){ flash(texts[n.idx]); break; }+ }+ });++ function setPaused(p){+ paused=p;+ if(p){ // snap any in-flight notes so nothing is frozen mid-air off-screen+ for(var i=0;i<notes.length;i++){ var n=notes[i]; if(!n.settled){ n.y=n.ty; n.settled=true; n.vy=0; } }+ } else { T=performance.now(); }+ }+ if(LL && LL.onPause){ try{ LL.onPause(setPaused); }catch(e){} }+ document.addEventListener('visibilitychange',function(){ setPaused(document.hidden); });+ window.addEventListener('resize',size);++ size();+ // restore previous notes (own data) so the jar isn't empty on return+ (function(){+ function seedDemo(){ ['the morning light','a friend who texted back','coffee, still warm','this little app'].forEach(function(t){ add(t,true); }); }+ if(LL && LL.storage){+ try{ LL.storage.get().then(function(s){+ if(s && s.texts && s.texts.length){ s.texts.forEach(function(t){ add(t,true); }); }+ else seedDemo();+ }).catch(seedDemo); }catch(e){ seedDemo(); }+ } else seedDemo();+ })();+ T=performance.now();+ raf=requestAnimationFrame(frame);+})();+</script>