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

v1Current

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