PageSourceSearch

https://heyhaigh.ai/components/nav-icon-motion.js?v=25-about-toss

js heyhaigh.ai collected 2026-10-05 13:48:58 UTC 10,694 bytes, 124 lines download raw bytes

1const ns='http://www.w3.org/2000/svg';
2let serial=0;
3const reduced=matchMedia('(prefers-reduced-motion: reduce)');
4function bind(button,parts,frames,options={}){
5 let running=[];
6 const stop=()=>{for(const part of parts){const transform=getComputedStyle(part).transform,opacity=getComputedStyle(part).opacity;running.filter(a=>a.effect?.target===part).forEach(a=>a.cancel());if(!reduced.matches&&transform!=='none')part.animate([{transform,opacity},{transform:'none',opacity:1}],{duration:260,easing:'ease-out'});}running=[];};
7 const start=()=>{if(reduced.matches)return;stop();running=parts.map((p,i)=>p.animate(frames(i),{duration:600,easing:'ease-in-out',...options}));};
8 button.addEventListener('pointerenter',e=>{if(e.pointerType!=='touch')start()});button.addEventListener('pointerleave',stop);
9 button.addEventListener('focus',()=>{if(button.matches(':focus-visible'))start()});button.addEventListener('blur',stop);
10 reduced.addEventListener('change',stop);
11}
12export function setupNavIcons(host){
13 const links=host.querySelectorAll('.top-nav-link');
14 for(const link of links){
15 const svg=link.querySelector('svg');if(!svg||svg.dataset.motionReady)continue;svg.dataset.motionReady='true';svg.style.overflow='visible';
16 const path=svg.querySelector('path');const d=path.getAttribute('d');
17 if(link.id==='thoughtsNavLink'){
18 // Keep the finishing stroke out of the loop's reveal: their silhouettes overlap.
19 const id='scribble-reveal-'+(++serial);
20 const body='M140-725 Q290-920 310-735 Q305-650 210-520 Q115-320 240-325 Q280-315 400-465 Q650-800 690-470 Q710-180 555-170 Q425-175 490-285';
21 const tail='M490-285 Q540-420 840-415';
22 // Cut only the finishing stroke out of the body reveal. The smooth loop
23 // boundaries keep both notches hidden without clipping the rest of the artwork.
24 // Extend cutouts past the artwork edge so antialiasing cannot leave hairlines.
25 const loop='M0-960H960V0H0Z M741-474Q745-415 741-356H966V-474Z M420-246q0-80 63.5-144T641-466 Q645-410 640-350q-46 11-80 43.5T534-250 Q534-295 476-303 Q420-295 420-246Z';
26 svg.innerHTML=`<defs><clipPath id="${id}-loop" clipPathUnits="userSpaceOnUse"><path d="${loop}" clip-rule="evenodd"/></clipPath><mask id="${id}" maskUnits="userSpaceOnUse" x="0" y="-960" width="960" height="960"><g clip-path="url(#${id}-loop)"><path class="thoughts-trace" d="${body}" fill="none" stroke="white" stroke-width="185" stroke-linecap="round"/></g><path class="thoughts-trace" d="${tail}" fill="none" stroke="white" stroke-width="185" stroke-linecap="round"/></mask></defs><path d="${d}" mask="url(#${id})"/>`;
27 const traces=[...svg.querySelectorAll('.thoughts-trace')],bodyLength=traces[0].getTotalLength(),total=bodyLength+traces[1].getTotalLength();
28 // Both parts share the original distance-based timing and easing. The longer
29 // gap prevents a wrapped dash from exposing the tail before its turn.
30 bind(link,traces,i=>[{strokeDasharray:`${total} ${total*2}`,strokeDashoffset:String(total+(i?bodyLength:0))},{strokeDasharray:`${total} ${total*2}`,strokeDashoffset:String(i?bodyLength:0)}],{duration:950,easing:'cubic-bezier(0.22, 0.75, 0.3, 1)'});
31 }else if(link.getAttribute('href')==='/memory-graph'){
32 const split=d.indexOf('ZM240');svg.innerHTML=`<g class="memory-head"><path d="${d.slice(split+1)}"/><path class="memory-brain" d="${d.slice(0,split+1)}"/></g>`;
33 const head=svg.querySelector('g'),brain=svg.querySelector('.memory-brain');for(const p of [head,brain]){p.style.transformBox='fill-box';p.style.transformOrigin='center';}
34 head.style.transformOrigin='50% 90%';
35 bind(link,[head,brain],i=>i?[
36  {opacity:0,transform:'scale(.12)',offset:0},
37  {opacity:0,transform:'scale(.12)',offset:.18,easing:'cubic-bezier(.22,.75,.3,1)'},
38  {opacity:1,transform:'scale(1)',offset:.72},
39  {opacity:1,transform:'scale(1)',offset:1}
40 ]:[
41  {transform:'rotate(0deg)',offset:0,easing:'cubic-bezier(.55,0,1,.45)'},
42  {transform:'rotate(18deg)',offset:.18,easing:'cubic-bezier(.16,1,.3,1)'},
43  {transform:'rotate(0deg)',offset:1}
44 ],{duration:900,easing:'linear'});
45 }else{
46 // Knock out lower artwork inside each moving opening. The visible shapes
47 // retain their original open centers without borrowing a background color.
48 const id='about-toss-'+(++serial);
49 const figure="M360-80v-200q-61-5-121-14.5T120-320l20-80q84 23 168.5 31.5T480-360q87 0 171.5-8.5T820-400l20 80q-59 16-119 25.5T600-280v200H360Z M480-400q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Z";
50 const shapes=["M220-464 64-620l156-156 156 156-156 156Z M220-576l44-44-44-44-44 44 44 44Z", "M480-680a120 120 0 1 0 0-240 120 120 0 1 0 0 240Z M480-760a40 40 0 1 0 0-80 40 40 0 1 0 0 80Z", "M682-480l-68-120 68-120h136l68 120-68 120Z M
50728-560h44l22-40-22-40h-44l-22 40Z"];
51 const holes=shapes.map(d=>d.slice(d.indexOf('Z')+1));
52 // Extend each knockout inside the thick outline to avoid antialiasing seams.
53 const extent='<rect x="-400" y="-1500" width="1800" height="1900" fill="white"/>';
54 const mask=(layer)=>`<mask id="${id}-mask-${layer}" maskUnits="userSpaceOnUse" x="-400" y="-1500" width="1800" height="1900">${extent}${holes.map((_,i)=>i>layer?`<use class="juggle-occluder" data-shape="${i}" href="#${id}-hole-${i}"/>`:'').join('')}</mask>`;
55 svg.innerHTML=`<defs>${holes.map((d,i)=>`<path id="${id}-hole-${i}" fill="black" stroke="black" stroke-width="8" stroke-linejoin="round" d="${d}"/>`).join('')}${[-1,0,1].map(mask).join('')}</defs><path d="${figure}" mask="url(#${id}-mask--1)"/>${shapes.map((d,i)=>`<g${i<2?` mask="url(#${id}-mask-${i})"`:''}><g class="juggle" data-shape="${i}"><path fill-rule="evenodd" d="${d}"/></g></g>`).join('')}`;
56 const parts=[...svg.querySelectorAll('.juggle'),...svg.querySelectorAll('.juggle-occluder')];
57 parts.forEach(p=>{p.style.transformBox='fill-box';p.style.transformOrigin='center'});
58 const slots=[[220,-620],[480,-800],[750,-600]];
59 // Sample ballistic arcs: steady horizontal travel, slowing ascent and an
60 // accelerating fall. Each exchange catches briefly before the next throw.
61 bind(link,parts,index=>{
62  const i=Number(parts[index].dataset.shape),origin=slots[i],frames=[];
63  for(let exchange=0;exchange<3;exchange++){
64   const from=(i+exchange)%3,to=(from+1)%3;
65   const lift=from===2?300:95;
66   for(let step=0;step<=24;step++){
67    const t=step/24;
68    const x=slots[from][0]+(slots[to][0]-slots[from][0])*t;
69    const y=slots[from][1]+(slots[to][1]-slots[from][1])*t-4*lift*t*(1-t);
70    frames.push({offset:(exchange+t*.9)/3,transform:`translate(${x-origin[0]}px,${y-origin[1]}px) rotate(${(exchange+t)*120}deg)`});
71   }
72   const last=frames[frames.length-1];
73   frames.push({...last,offset:(exchange+1)/3});
74  }
75  return frames;
76 },{duration:1200,iterations:1,easing:'linear'});
77 }
78 }
79}
80export function setupCornerIcons(){
81 const log=document.getElementById('changelogButton'),svg=log?.querySelector('svg');
82 if(svg&&!svg.dataset.motionReady){
83  svg.dataset.motionReady='true';
84  const original=svg.querySelector('path').getAttribute('d');
85  const prefix='log-rows-'+(++serial);
86  const left='M160-840h360v240H160Z M240-760v80h200v-80Z';
87  const right='M440-840h360v240H440Z M520-760v80h200v-80Z';
88  const row2='M160-680h640v240H160Z M240-600v80h200v-80Z M520-600v80h200v-80Z';
89  const row3='M160-440h80v80h200v80H160Z';
90  const row4='M160-280h80v80h200v80H160Z';
91  const check='M616-120L504-232l56-56 56 56 142-142 56 56-198 198Z';
92  // Full boxes overlap structurally, but fixed reveal windows hide everything
93  // behind the preceding box/row. Only the emerging portion can be painted.
94  const windows=[[520,-840,280,240],[160,-600,640,160],[160,-440,280,160],[160,-280,280,160]];
95  const defs=windows.map(([x,y,w,h],i)=>`<clipPath id="${prefix}-${i}" clipPathUnits="userSpaceOnUse"><rect x="${x}" y="${y}" width="${w}" height="${h}"/></clipPath>`).join('');
96  const rows=[right,row2,row3,row4].map((d,i)=>`<g clip-path="url(#${prefix}-${i})"><g class="log-step" data-row="${i}"><path fill-rule="evenodd" d="${d}"/></g></g>`).join('');
97  svg.innerHTML=`<defs>${defs}</defs><g class="log-base"><path fill-rule="evenodd" d="${left}"/></g>${rows}<g class="log-step log-check"><path d="${check}"/></g><path class="log-rest" d="${original}"/>`;
98  // Keep the rectilinear rows crisp while their translations cross subpixels.
99  // The diagonal checkmark retains normal antialiasing during its scale pop.
100  svg.querySelectorAll('[data-row] path,.log-base path').forEach(path=>path.setAttribute('shape-rendering','crispEdges'));
101  const parts=[...svg.querySelectorAll('.log-step'),svg.querySelector('.log-base'),svg.querySelector('.log-rest')];
102  parts.forEach(p=>{if(!p.classList.contains('log-rest'))p.style.opacity='0';p.style.transformBox='fill-box';p.style.transformOrigin='center'});
103  bind(log,parts,i=>{
104   const part=parts[i];
105   if(part.classList.contains('log-rest'))return [{opacity:0}
105,{opacity:0}];
106   if(part.classList.contains('log-base'))return [{opacity:1},{opacity:1}];
107   const isCheck=part.classList.contains('log-check'),start=isCheck?.72:i*.17;
108   // Keep waiting rows fully hidden; a small overscan avoids antialiasing at the clip boundary.
109   const entry=isCheck?'scale(.35)':i===0?'translateX(-281px)':'translateY(-161px)';
110   return [
111    {offset:0,opacity:0,transform:entry},
112    {offset:start,opacity:0,transform:entry},
113    {offset:start+.001,opacity:isCheck?0:1,transform:entry,easing:'cubic-bezier(.22,.75,.3,1)'},
114    {offset:start+.17,opacity:1,transform:isCheck?'scale(1.18)':'translate(0px,0px)',easing:'ease-out'},
115    {offset:1,opacity:1,transform:isCheck?'scale(1)':'translate(0px,0px)'}
116   ];
117  },{duration:700,easing:'linear'});
118 }
119 const button=document.getElementById('themeToggle'),icon=button?.querySelector('.theme-toggle-icon');if(!icon||icon.dataset.motionReady)return;icon.dataset.motionReady='true';
120 // Four fast revolutions with a long deceleration back to the original orientation.
121 bind(button,[icon],()=>[{transform:'rotate(0deg)'},{transform:'rotate(1440deg)'}],{duration:1200,easing:'cubic-bezier(.12,.85,.18,1)'});
122 let previous=icon.textContent;
123 new MutationObserver(()=>{const next=icon.textContent;if(next===previous)return;const old=previous;previous=next;if(reduced.matches)return;const ghost=document.createElement('span');ghost.textContent=old;ghost.setAttribute('aria-hidden','true');ghost.style.cssText='position:absolute;pointer-events:none';button.append(ghost);ghost.animate([{opacity:1,transform:'rotate(0deg)'},{opacity:0,transform:'rotate(60deg)'}],{duration:260,fill:'forwards'}).finished.then(()=>ghost.remove());icon.animate([{opacity:0,transform:'rotate(-60deg)'},{opacity:1,transform:'rotate(0deg)'}],{duration:260});}).observe(icon,{childList:true,characterData:true,subtree:true});
124}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.