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.