holiday-card-wordpress_1.html
<!-- Holiday card: paste into a WordPress Custom HTML block. Set IMG_URL in the script to your Media Library image URL. -->
<style>
.sgb{--ar:1.25;position:relative;box-sizing:border-box;width:min(100%,calc((100vh - 48px) * var(--ar)),1000px);margin:24px auto}
@media (orientation:portrait){.sgb{--ar:.85}}
.sgb *{box-sizing:border-box}
.sgb-stage{position:relative;overflow:hidden;width:100%;aspect-ratio:var(--ar);border-radius:clamp(14px,2.6vw,28px);background:radial-gradient(circle at 30% 22%,#f1f2ee,#a9b6bb);touch-action:pan-y;cursor:crosshair;-webkit-user-select:none;user-select:none;font-family:Georgia,"Times New Roman",serif}
.sgb-stage svg{position:absolute;inset:0;width:100%;height:100%;max-width:none;display:block}
.sgb-bar{position:absolute;left:0;right:0;height:0;background:#000;pointer-events:none;top:0}
.sgb-bar.b{top:auto;bottom:0}
#sgb-hint{position:absolute;left:0;right:0;bottom:5%;text-align:center;color:#4b5558;font-size:clamp(12px,1.6vw,15px);letter-spacing:.02em;transition:opacity .5s;pointer-events:none}
#sgb-rec{position:absolute;top:4%;left:4%;display:flex;align-items:center;gap:8px;font:600 clamp(11px,1.5vw,14px)/1 ui-monospace,Menlo,Consolas,monospace;color:#1c2124;opacity:0;transition:opacity .3s,color .4s;pointer-events:none;z-index:3}
#sgb-rec i{width:.7em;height:.7em;border-radius:50%;background:#e5372f;animation:sgbblink 1s steps(2,jump-none) infinite}
@keyframes sgbblink{50%{opacity:.15}}
#sgb-title{position:absolute;left:0;right:0;bottom:7.5%;padding:12% 5% 3.5%;color:#fff;background:linear-gradient(transparent,rgba(0,0,0,.6));opacity:0;pointer-events:none;z-index:2}
#sgb-titleH{display:block;font-weight:400;font-size:clamp(22px,5vw,44px);line-height:1.1}
#sgb-credit{display:block;margin-top:6px;font-size:clamp(11px,1.5vw,14px);opacity:.8;letter-spacing:.03em}
#sgb-replay{position:absolute;top:4%;right:4%;z-index:4;opacity:0;pointer-events:none;transition:opacity .4s;background:rgba(0,0,0,.45);color:#fff;border:1px solid rgba(255,255,255,.35);border-radius:999px;padding:6px 14px;font:inherit;font-size:clamp(11px,1.5vw,13px);cursor:pointer}
.sgb.done #sgb-replay{opacity:1;pointer-events:auto}
@media (prefers-reduced-motion:reduce){#sgb-rec i{animation:none}}
</style>
<div class="sgb" id="sgb-root"><div class="sgb-stage" id="sgb-stage">
<svg id="sgb-s" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="sgb-body" cx="40%" cy="34%" r="85%" fx="40%" fy="34%">
<stop offset="0" stop-color="#3b3e43"/>
<stop offset=".5" stop-color="#0e0f11"/>
<stop offset="1" stop-color="#020203"/>
</radialGradient>
<radialGradient id="sgb-clear" cx="50%" cy="45%" r="75%">
<stop offset="0" stop-color="#fbf9f3"/>
<stop offset="1" stop-color="#e2ddd0"/>
</radialGradient>
<!-- Glass material: rough edge, frost grain lit by the cursor, beveled rim lit by the cursor -->
<filter id="sgb-glass" x="-25%" y="-25%" width="150%" height="150%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency=".8" numOctaves="3" seed="7" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="7" xChannelSelector="R" yChannelSelector="G" result="body"/>
<feOffset id="sgb-drift" in="noise" dx="0" dy="0" result="noiseD"/>
<feSpecularLighting id="sgb-frost" in="noiseD" surfaceScale="2.8" specularConstant="1" specularExponent="16" lighting-color="#dbe6ec" result="frostSpec">
<fePointLight id="sgb-l1" x="0" y="0" z="120"/>
</feSpecularLighting>
<feComposite in="frostSpec" in2="body" operator="in" result="frostIn"/>
<feGaussianBlur in="body" stdDeviation="7" result="soft"/>
<feSpecularLighting id="sgb-rim" in="soft" surfaceScale="10" specularConstant="1.3" specularExponent="26" lighting-color="#ffffff" result="rimSpec">
<fePointLight id="sgb-l2" x="0" y="0" z="80"/>
</feSpecularLighting>
<feComposite in="rimSpec" in2="body" operator="in" result="rimIn"/>
<feMerge><feMergeNode in="body"/><feMergeNode in="frostIn"/><feMergeNode in="rimIn"/></feMerge>
</filter>
<!-- Feathered, organic reveal edge (not a hard circle) -->
<filter id="sgb-soft" filterUnits="userSpaceOnUse" x="0" y="0" width="800" height="600">
<feTurbulence id="sgb-wob" type="fractalNoise" baseFrequency=".011" numOctaves="2" seed="4" result="w"/>
<feDisplacementMap id="sgb-wobMap" in="SourceGraphic" in2="w" scale="60" xChannelSelector="R" yChannelSelector="G" result="d"/>
<feGaussianBlur id="sgb-wobBlur" in="d" stdDeviation="24"/>
</filter>
<filter id="sgb-halo" filterUnits="userSpaceOnUse" x="0" y="0" width="800" height="600"><feGaussianBlur id="sgb-haloBlur" stdDeviation="12"/></filter>
<filter id="sgb-shadowBlur" x="-50%" y="-200%" width="200%" height="500%"><feGaussianBlur stdDeviation="14"/></filter>
<clipPath id="sgb-blobClip"><path id="sgb-clipPath"/></clipPath>
<mask id="sgb-revealMask" maskUnits="userSpaceOnUse" x="-200" y="-200" width="3000" height="3000">
<g filter="url(#sgb-soft)"><circle id="sgb-revealCircle" fill="#fff" r="0" cx="0" cy="0"/></g>
</mask>
</defs>
<ellipse id="sgb-shadow" fill="#000" opacity=".28" filter="url(#sgb-shadowBlur)"/>
<g filter="url(#sgb-glass)">
<path id="sgb-blob" fill="url(#sgb-body)"/>
<g clip-path="url(#sgb-blobClip)">
<g mask="url(#sgb-revealMask)">
<g id="sgb-imgG"><image id="sgb-revealImg" preserveAspectRatio="xMidYMid slice" x="0" y="0" width="800" height="600"/></g>
</g>
<g filter="url(#sgb-halo)"><circle id="sgb-haloRing" fill="none" stroke="#fff" r="0" opacity="0"/></g>
</g>
</g>
</svg>
<div class="sgb-bar" id="sgb-barT"></div><div class="sgb-bar b" id="sgb-barB"></div>
<div id="sgb-rec"><i></i><span id="sgb-tc">00:00:00:00</span></div>
<div id="sgb-title"><span id="sgb-titleH"></span><span id="sgb-credit"></span></div>
<button id="sgb-replay" type="button">Replay</button>
<div id="sgb-hint">Hold to pull focus</div>
</div></div>
<script>
(function(){
var IMG_URL="PASTE_YOUR_IMAGE_URL_HERE", TITLE="Happy Holidays", CREDIT="Cinematography by Scott Strellis";
var $=function(i){return document.getElementById(i)};
var root=$('sgb-root'),stage=$('sgb-stage'),blob=$('sgb-blob'),clip=$('sgb-clipPath'),rc=$('sgb-revealCircle'),img=$('sgb-revealImg'),imgG=$('sgb-imgG'),
hint=$('sgb-hint'),halo=$('sgb-haloRing'),shadow=$('sgb-shadow'),body=$('sgb-body'),l1=$('sgb-l1'),l2=$('sgb-l2'),frost=$('sgb-frost'),rim=$('sgb-rim'),
drift=$('sgb-drift'),wob=$('sgb-wob'),wobMap=$('sgb-wobMap'),wobBlur=$('sgb-wobBlur'),haloBlur=$('sgb-haloBlur'),soft=$('sgb-soft'),haloF=$('sgb-halo'),
barT=$('sgb-barT'),barB=$('sgb-barB'),rec=$('sgb-rec'),tc=$('sgb-tc'),title=$('sgb-title'),replay=$('sgb-replay');
$('sgb-titleH').textContent=TITLE;$('sgb-credit').textContent=CREDIT;
img.setAttribute('href',IMG_URL);
var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var W=0,H=0,N=72,HOLD=3000;
var m={x:0,y:0,sx:0,sy:0,active:false,pull:0},lt={x:0,y:0};
var pts=[];for(var i=0;i<N;i++)pts.push({ox:0,oy:0,vx:0,vy:0});
var holdMs=0,p=0,done=false,last=performance.now(),rx=0,ry=0,init=false,lastTc='';
function ss(a,b,x){x=Math.max(0,Math.min(1,(x-a)/(b-a)));return x*x*(3-2*x);}
function pad(n){return (n<10?'0':'')+n;}
function resize(){
W=stage.clientWidth;H=stage.clientHeight;
$('sgb-s').setAttribute('viewBox','0 0 '+W+' '+H);
[soft,haloF].forEach(function(f){f.setAttribute('width',W);f.setAttribute('height',H);});
img.setAttribute('x',-40);img.setAttribute('y',-40);img.setAttribute('width',W+80);img.setAttribute('height',H+80);
if(!init){m.sx=m.x=W/2;m.sy=m.y=H/2;lt.x=W*.35;lt.y=H*.2;rx=W/2;ry=H/2;init=true;}
}
window.addEventListener('resize',resize);
if(window.ResizeObserver)new ResizeObserver(resize).observe(stage);
resize();
function move(e){var b=stage.getBoundingClientRect();m.x=e.clientX-b.left;m.y=e.clientY-b.top;m.active=true;}
stage.addEventListener('pointermove',move);
stage.addEventListener('pointerdown',move);
stage.addEventListener('pointerleave',function(){m.active=false;});
stage.addEventListener('pointercancel',function(){m.active=false;});
window.addEventListener('pointerup',function(e){if(e.pointerType!=='mouse')m.active=false;});
replay.addEventListener('click',function(e){e.stopPropagation();done=false;holdMs=0;p=0;m.active=false;root.classList.remove('done');rx=W/2;ry=H/2;});
function frame(now){
var dt=Math.min(64,now-last);last=now;var t=now/1000;
var R=Math.min(W,H)*0.27;
// hold timer (rack focus): only counts while the cursor rests on the blob
var inside=false;
if(!done&&m.active){try{inside=blob.isPointInFill(new DOMPoint(m.x,m.y));}catch(e){}}
if(!done){
holdMs=Math.max(0,holdMs+(inside?dt:-dt*3.3));
if(holdMs>=HOLD){holdMs=HOLD;done=true;root.classList.add('done');}
}
p=Math.min(1,holdMs/HOLD);
var e=ss(0,1,p), g=ss(.72,1,p);
var cover=Math.hypot(W,H)*0.62;
m.pull+=((m.active&&!done?1:0)-m.pull)*0.08;
var tx=m.active?m.x:W/2,ty=m.active?m.y:H/2;
m.sx+=(tx-m.sx)*0.16;m.sy+=(ty-m.sy)*0.16;
var cx=W/2+(m.sx-W/2)*0.07*m.pull, cy=H/2+(m.sy-H/2)*0.07*m.pull;
var R2=R*(1+p*0.07*Math.sin(t*15));
var Reff=R2+(cover-R2)*g;
var sp=(reduce?0.25:1)*(1+p*2.4), amp=(1+p*1.5)*(1-0.5*g);
// spring-physics jelly body
var P=[];
for(var i=0;i<N;i++){
var a=i/N*Math.PI*2, s=pts[i];
var wv=0.085*Math.sin(3*a+t*0.8*sp)+0.06*Math.sin(5*a-t*1.1*sp+1.3)+0.04*Math.sin(2*a+t*0.6*sp+2)+0.02*Math.sin(9*a+t*1.7*sp)+p*0.014*Math.sin(17*a+t*28);
var r=Reff*(1+wv*amp), x=cx+Math.cos(a)*r, y=cy+Math.sin(a)*r;
var dx=m.sx-x, dy=m.sy-y, d=Math.hypot(dx,dy), f=d<R2?Math.pow(1-d/R2,2)*0.46*m.pull:0;
s.vx+=(dx*f-s.ox)*0.15; s.vy+=(dy*f-s.oy)*0.15; s.vx*=0.76; s.vy*=0.76;
s.ox+=s.vx; s.oy+=s.vy;
P.push([x+s.ox,y+s.oy]);
}
var path='M'+P[0][0].toFixed(1)+' '+P[0][1].toFixed(1);
for(var j=0;j<N;j++){
var p0=P[(j-1+N)%N],p1=P[j],p2=P[(j+1)%N],p3=P[(j+2)%N];
path+='C'+(p1[0]+(p2[0]-p0[0])/6).toFixed(1)+' '+(p1[1]+(p2[1]-p0[1])/6).toFixed(1)+' '+(p2[0]-(p3[0]-p1[0])/6).toFixed(1)+' '+(p2[1]-(p3[1]-p1[1])/6).toFixed(1)+' '+p2[0].toFixed(1)+' '+p2[1].toFixed(1);
}
path+='Z';
blob.setAttribute('d',path);clip.setAttribute('d',path);
// light follows the cursor; drifts on its own when idle
var idleX=cx+Math.cos(t*.45)*R*1.25, idleY=cy-R*.9+Math.sin(t*.45)*R*.35;
var lx=m.active?m.sx:idleX, ly=m.active?m.sy:idleY;
lt.x+=(lx-lt.x)*0.12;lt.y+=(ly-lt.y)*0.12;
l1.setAttribute('x',lt.x);l1.setAttribute('y',lt.y);l1.setAttribute('z',R*.5);
l2.setAttribute('x',lt.x);l2.setAttribute('y',lt.y);l2.setAttribute('z',R*.32);
frost.setAttribute('specularConstant',((1+p*0.9)*(1-g*.85)).toFixed(2));
frost.setAttribute('specularExponent',(16-p*7).toFixed(1));
rim.setAttribute('specularConstant',((1.3+p*1.1)*(1-g*.85)).toFixed(2));
drift.setAttribute('dx',((cx-W/2)+Math.sin(t*.6)*3).toFixed(1));
drift.setAttribute('dy',((cy-H/2)+Math.cos(t*.5)*3).toFixed(1));
var gx=50+(lt.x-cx)/R*20, gy=50+(lt.y-cy)/R*20;
body.setAttribute('cx',gx+'%');body.setAttribute('cy',gy+'%');body.setAttribute('fx',gx+'%');body.setAttribute('fy',gy+'%');
shadow.setAttribute('cx',cx);shadow.setAttribute('cy',cy+R*1.18);shadow.setAttribute('rx',R*.9);shadow.setAttribute('ry',R*.14);
shadow.setAttribute('opacity',(.28*(1-g)).toFixed(2));
// feathered, wobbling reveal bloom that starts where you hold
if(inside){rx+=(m.sx-rx)*0.2;ry+=(m.sy-ry)*0.2;}
if(p>=1){rx+=(cx-rx)*0.1;ry+=(cy-ry)*0.1;}
var rr=e*R*3.7+g*Math.hypot(W,H);
rc.setAttribute('cx',rx);rc.setAttribute('cy',ry);rc.setAttribute('r',rr.toFixed(1));
wobMap.setAttribute('scale',(R*0.42).toFixed(1));
wobBlur.setAttribute('stdDeviation',(R*0.12+e*R*0.06).toFixed(1));
wob.setAttribute('baseFrequency',(0.010+0.003*Math.sin(t*1.3)).toFixed(4));
halo.setAttribute('cx',rx);halo.setAttribute('cy',ry);halo.setAttribute('r',(e*R*3.7*0.82).toFixed(1));
halo.setAttribute('stroke-width',(R*0.12).toFixed(1));
halo.setAttribute('opacity',(Math.sin(Math.min(1,p*1.05)*Math.PI)*0.5*(1-g)).toFixed(2));
haloBlur.setAttribute('stdDeviation',(R*0.07).toFixed(1));
// photo pulls into focus and dollies in as you hold
var sc=1.12-0.12*e, bl=(1-e)*(1-e)*18;
imgG.setAttribute('transform','translate('+W/2+' '+H/2+') scale('+sc.toFixed(4)+') translate('+(-W/2)+' '+(-H/2)+')');
img.style.filter=bl>0.05?'blur('+bl.toFixed(1)+'px)':'none';
// camera-style overlays
barT.style.height=barB.style.height=(g*7.5).toFixed(2)+'%';
title.style.opacity=ss(.92,1,p).toFixed(2);
rec.style.opacity=holdMs>60?1:0;
rec.style.color=g>.5?'#fff':'#1c2124';
var ms=Math.min(holdMs,HOLD), tcs='00:00:'+pad(Math.floor(ms/1000))+':'+pad(Math.floor((ms%1000)/1000*24));
if(tcs!==lastTc){tc.textContent=tcs;lastTc=tcs;}
hint.style.opacity=(p>0.03||done)?0:1;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>