Skip to content

Instantly share code, notes, and snippets.

@masonwang025
Created March 31, 2026 14:48
Show Gist options
  • Select an option

  • Save masonwang025/49edffdff399175af2262e921eaae50b to your computer and use it in GitHub Desktop.

Select an option

Save masonwang025/49edffdff399175af2262e921eaae50b to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cozy Window Shade</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#f5f4f0;--fg:#1a1a18;--muted:#6b665e}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--fg)}
canvas#c{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:100;mix-blend-mode:multiply}
body{overflow:hidden;height:100vh}
.page{max-width:660px;padding:48px 24px 48px 64px;position:relative;z-index:50}
h1{font-size:1.1rem;font-weight:500;letter-spacing:-0.01em;margin-bottom:4px}
.sub{font-size:0.82rem;font-weight:300;line-height:1.6;color:var(--muted);margin-bottom:20px}
.prose p{font-size:0.88rem;line-height:1.75;margin-bottom:1.2em;font-weight:300;color:var(--fg)}
.toggle{position:fixed;top:20px;right:20px;z-index:200;background:none;border:none;cursor:pointer;font-size:18px;color:var(--muted);transition:color 0.2s}
.toggle:hover{color:var(--fg)}
#btn{color:var(--muted);cursor:pointer}
#btn:hover{color:var(--fg)}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="page">
<h1>Lorem Ipsum.</h1>
<p class="sub">Dolor sit amet, consectetur adipiscing elit.</p>
<div class="prose">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>
</div>
<svg id="btn" onclick="tog()" style="cursor:pointer;margin-top:16px;display:block;transition:transform 1.8s cubic-bezier(0.4,0,0.2,1)" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"/>
<line class="ray" x1="12" y1="2" x2="12" y2="4.5" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="12" y1="19.5" x2="12" y2="22" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="4.93" y1="4.93" x2="6.7" y2="6.7" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="17.3" y1="17.3" x2="19.07" y2="19.07" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="2" y1="12" x2="4.5" y2="12" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="19.5" y1="12" x2="22" y2="12" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="4.93" y1="19.07" x2="6.7" y2="17.3" style="transition:opacity 1.8s ease"/>
<line class="ray" x1="17.3" y1="6.7" x2="19.07" y2="4.93" style="transition:opacity 1.8s ease"/>
</svg>
</div>
<script>
const C=document.getElementById('c'),X=C.getContext('2d');
let on=true;
function fit(){C.width=innerWidth;C.height=innerHeight}
addEventListener('resize',fit);fit();
function lerp(a,b,t){return a+(b-a)*t}
function lc(a,b,t){return[Math.round(lerp(a[0],b[0],t)),Math.round(lerp(a[1],b[1],t)),Math.round(lerp(a[2],b[2],t))]}
function getAnimTime(now){
const period=120000;
const phase=(now%(period*2))/period;
const tri=phase<1?phase:2-phase;
return tri*0.1;
}
function getAnimOpen(now){
return 0.5+Math.sin(now*0.00006)*0.04+Math.sin(now*0.00015)*0.02;
}
const offscreenCache={};
function getOff(key,w,h){
const c=offscreenCache[key];
if(c&&c.width===w&&c.height===h)return c;
const nc=document.createElement('canvas');nc.width=w;nc.height=h;
offscreenCache[key]=nc;return nc;
}
let fadeVal=0,fadeTarget=1,lastTime=0;
function draw(now){
const dt=lastTime?(now-lastTime)/1000:0.016;lastTime=now;
const speed=dt/1.8;
if(fadeVal<fadeTarget)fadeVal=Math.min(fadeVal+speed,1);
else if(fadeVal>fadeTarget)fadeVal=Math.max(fadeVal-speed,0);
const fadeEase=fadeVal*fadeVal*(3-2*fadeVal);
if(fadeVal===0&&fadeTarget===0){X.clearRect(0,0,C.width,C.height);requestAnimationFrame(draw);return}
const w=C.width,h=C.height;
const t=getAnimTime(now),open=getAnimOpen(now),n=t/0.35;
const shadowTarget=lc([228,214,196],[234,224,210],n);
const st=lc([255,255,255],shadowTarget,fadeEase);
const wlTarget=lc([255,215,155],[255,230,190],n);
const wl=lc([255,255,255],wlTarget,fadeEase);
const skewX=lerp(0.34,0.26,n),skewY=lerp(0.13,0.09,n);
const stretch=lerp(1.9,1.6,n);
const warmAlpha=lerp(0.28,0.17,n)*fadeEase;
const baseSoft=lerp(12,7,n);
X.clearRect(0,0,w,h);
X.fillStyle=`rgb(${st[0]},${st[1]},${st[2]})`;
X.fillRect(0,0,w,h);
const projW=Math.min(w*0.58,420)*stretch;
const projH=Math.min(h*0.72,500)*stretch*0.78;
const bx=Math.sin(now*0.00009)*5+Math.sin(now*0.00025)*2.5;
const by=Math.cos(now*0.00011)*3.5+Math.cos(now*0.00022)*1.8;
const px=lerp(w*0.01,w*0.06,n)+bx;
const py=lerp(h*0.01,h*0.03,n)+by;
const frameT=lerp(10,7,n);
const numSlats=18,innerH=projH-frameT*2;
const spacing=innerH/numSlats;
const slatThick=spacing*lerp(0.88,0.12,open);
const gapH=spacing-slatThick;
if(gapH<0.3){requestAnimationFrame(draw);return}
X.save();X.translate(px,py);X.transform(1,skewY,skewX,1,0,0);
const offW=Math.ceil(projW+80),offH=Math.ceil(projH+80);
const OC=getOff('off',offW,offH),OX=OC.getContext('2d');
OX.clearRect(0,0,offW,offH);
for(let i=0;i<numSlats;i++){
const baseY=frameT+i*spacing+slatThick;
const wb=Math.sin(now*0.00008+i*0.53)*1.1+Math.sin(now*0.00019+i*0.79)*0.6;
const sy=baseY+wb;
const vertPos=i/numSlats,slatSoft=baseSoft*(0.55+vertPos*1.0);
const distFromCenter=Math.abs(i-numSlats/2)/(numSlats/2);
const slatAlpha=1.0-distFromCenter*0.1;
const padY=slatSoft*1.2;
const g=OX.createLinearGradient(0,sy-padY,0,sy+gapH+padY);
g.addColorStop(0,'rgba(255,255,255,0)');
g.addColorStop(padY/(gapH+padY*2),`rgba(255,255,255,${slatAlpha})`);
g.addColorStop(1-padY/(gapH+padY*2),`rgba(255,255,255,${slatAlpha})`);
g.addColorStop(1,'rgba(255,255,255,0)');
OX.fillStyle=g;OX.fillRect(frameT,sy-padY,projW-frameT*2,gapH+padY*2);
}
OX.globalCompositeOperation='destination-in';
const hV=OX.createLinearGradient(frameT,0,projW-frameT,0);
hV.addColorStop(0,'rgba(255,255,255,0.1)');hV.addColorStop(0.06,'rgba(255,255,255,0.55)');
hV.addColorStop(0.15,'rgba(255,255,255,1)');hV.addColorStop(0.5,'rgba(255,255,255,1)');
hV.addColorStop(0.72,'rgba(255,255,255,0.8)');hV.addColorStop(0.85,'rgba(255,255,255,0.35)');
hV.addColorStop(0.94,'rgba(255,255,255,0.12)');hV.addColorStop(1,'rgba(255,255,255,0.02)');
OX.fillStyle=hV;OX.fillRect(0,0,offW,offH);
const vV=OX.createLinearGradient(0,frameT,0,projH-frameT);
vV.addColorStop(0,'rgba(255,255,255,0.08)');vV.addColorStop(0.05,'rgba(255,255,255,0.6)');
vV.addColorStop(0.12,'rgba(255,255,255,1)');vV.addColorStop(0.75,'rgba(255,255,255,0.85)');
vV.addColorStop(0.88,'rgba(255,255,255,0.35)');vV.addColorStop(0.95,'rgba(255,255,255,0.1)');
vV.addColorStop(1,'rgba(255,255,255,0.02)');
OX.fillStyle=vV;OX.fillRect(0,0,offW,offH);
OX.globalCompositeOperation='source-over';
OX.globalCompositeOperation='destination-out';
const mullW=frameT*0.5,mullSoft=baseSoft*0.9;
const mx=projW*0.47;
const mg=OX.createLinearGradient(mx-mullW-mullSoft,0,mx+mullW+mullSoft,0);
mg.addColorStop(0,'rgba(255,255,255,0)');mg.addColorStop(0.15,'rgba(255,255,255,1)');
mg.addColorStop(0.85,'rgba(255,255,255,1)');mg.addColorStop(1,'rgba(255,255,255,0)');
OX.fillStyle=mg;OX.fillRect(mx-mullW-mullSoft,0,(mullW+mullSoft)*2,projH);
const my=projH*0.4;
const hg=OX.createLinearGradient(0,my-mullW-mullSoft,0,my+mullW+mullSoft);
hg.addColorStop(0,'rgba(255,255,255,0)');hg.addColorStop(0.15,'rgba(255,255,255,1)');
hg.addColorStop(0.85,'rgba(255,255,255,1)');hg.addColorStop(1,'rgba(255,255,255,0)');
OX.fillStyle=hg;OX.fillRect(0,my-mullW-mullSoft,projW,(mullW+mullSoft)*2);
const cordX=projW*0.73+Math.sin(now*0.00025)*2.5,cordW=1.5,cordSoft=baseSoft*0.4;
const cG=OX.createLinearGradient(cordX-cordW-cordSoft,0,cordX+cordW+cordSoft,0);
cG.addColorStop(0,'rgba(255,255,255,0)');cG.addColorStop(0.25,'rgba(255,255,255,0.6)');
cG.addColorStop(0.75,'rgba(255,255,255,0.6)');cG.addColorStop(1,'rgba(255,255,255,0)');
OX.fillStyle=cG;OX.fillRect(cordX-cordW-cordSoft,frameT,(cordW+cordSoft)*2,projH-frameT*2);
OX.globalCompositeOperation='source-over';
X.globalCompositeOperation='destination-out';X.drawImage(OC,0,0);
X.globalCompositeOperation='source-over';
const WC=getOff('warm',offW,offH),WX=WC.getContext('2d');WX.clearRect(0,0,offW,offH);
const wG=WX.createRadialGradient(projW*0.4,projH*0.45,0,projW*0.4,projH*0.45,projW*0.8);
wG.addColorStop(0,`rgba(${wl[0]},${wl[1]},${wl[2]},${warmAlpha*0.9})`);
wG.addColorStop(0.5,`rgba(${wl[0]},${wl[1]},${wl[2]},${warmAlpha*0.6})`);
wG.addColorStop(1,`rgba(${wl[0]},${wl[1]},${wl[2]},${warmAlpha*0.15})`);
WX.fillStyle=wG;WX.fillRect(0,0,offW,offH);
WX.globalCompositeOperation='destination-in';WX.drawImage(OC,0,0);
WX.globalCompositeOperation='source-over';X.drawImage(WC,0,0);
const GC=getOff('glow',offW,offH),GX=GC.getContext('2d');GX.clearRect(0,0,offW,offH);
const glowX=projW*0.38,glowY=projH*0.42;
const gl=GX.createRadialGradient(glowX,glowY,0,glowX,glowY,projW*0.7);
gl.addColorStop(0,`rgba(255,220,160,${warmAlpha*0.35})`);
gl.addColorStop(0.5,`rgba(255,228,180,${warmAlpha*0.15})`);
gl.addColorStop(1,'rgba(255,235,200,0)');
GX.fillStyle=gl;GX.fillRect(0,0,offW,offH);
GX.globalCompositeOperation='destination-in';GX.drawImage(OC,0,0);
GX.globalCompositeOperation='source-over';X.drawImage(GC,0,0);
X.restore();
requestAnimationFrame(draw);
}
let rotDeg=0;
function tog(){
on=!on;fadeTarget=on?1:0;
const btn=document.getElementById('btn');
rotDeg+=360;
btn.style.transform=`rotate(${rotDeg}deg)`;
// Rays: full length when on, short/dim when off
const rays=btn.querySelectorAll('.ray');
rays.forEach(r=>{
r.style.opacity=on?'1':'0.35';
});
}
requestAnimationFrame(draw);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment