1// Swaps static mascot images for live Agentic Mascots rigs. The static SVG 2// stays in place without JavaScript, with reduced motion, or if a rig fails. 3import { createMascot } from './engine/web.js'; 4import { cast, makeDefinition } from './characters.js'; 5 6const CROP = '92 92 376 376'; 7 8if (!matchMedia('(prefers-reduced-motion: reduce)').matches) { 9 const characters = new Map(cast.map((character) => [character.id, character])); 10 const hosts = new Set(); 11 const onscreen = new Set(); 12 const visible = 'IntersectionObserver' in window ? new IntersectionObserver((entries) => { 13 for (const entry of entries) { 14 const mascot = entry.target.mascot; 15 if (entry.isIntersecting) { onscreen.add(entry.target); mascot.resume(); } 16 else { 17 // A look target keeps the engine rendering even while paused, so clear it first. 18 onscreen.delete(entry.target); 19 mascot.clearLookTarget(); 20 mascot.pause(); 21 } 22 } 23 }) : null; 24 25 document.querySelectorAll('img[data-mascot]').forEach((img, index) => { 26 const character = characters.get(img.dataset.mascot); 27 if (!character) return; 28 const state = img.dataset.mascotState || 'idle'; 29 const host = document.createElement('span'); 30 host.className = `${img.className} mascot-live`.trim(); 31 host.style.cssText = img.style.cssText; 32 host.setAttribute('aria-hidden', 'true'); 33 try { 34 const mascot = createMascot(host, { definition: makeDefinition(character), animation: state, autoplay: false, maxFps: 30 }); 35 mascot.element.setAttribute('viewBox', CROP); 36 // The transparent stage rect and the disabled shadow paint past the crop and 37 // would catch the pointer for the neighbouring character; neither is visible. 38 mascot.element.querySelectorAll(':scope > rect, :scope > ellipse, :scope > defs').forEach((node) => node.remove()); 39 host.mascot = mascot; 40 hosts.add(host); 41 img.replaceWith(host); 42 // Stagger starts so neighbours don't move in unison. 43 setTimeout(() => { 44 mascot.play(state); 45 if (visible) visible.observe(host); else onscreen.add(host); 46 }, (index % 7) * 380); 47 if (img.dataset.mascotHover) { 48 host.addEventListener('pointerenter', () => mascot.play(img.dataset.mascotHover)); 49 host.addEventListener('pointerleave', () => mascot.play(state)); 50 } 51 } catch (error) { 52 host.remove(); 53 } 54 }); 55 56 // Onscreen characters look toward the mouse pointer. 57 let pointer = null; 58 let frame = 0; 59 const look = () => { 60 frame = 0; 61 for (const host of onscreen) { 62 const box = host.getBoundingClientRect(); 63 const reach = Math.max(260, box.width * 6); 64 host.mascot.setLookTarget({ 65 x: (pointer.x - (box.left + box.width / 2)) / reach, 66 y: (pointer.y - (box.top + box.height / 2)) / reach, 67 }); 68 } 69 }; 70 addEventListener('pointermove', (event) => { 71 if (event.pointerType !== 'mouse') return; 72 pointer = { x: event.clientX, y: event.clientY }; 73 if (!frame) frame = requestAnimationFrame(look); 74 }, { passive: true }); 75 document.documentElement.addEventListener('pointerleave', () => { 76 for (const host of hosts) host.mascot.clearLookTarget(); 77 }); 78}
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.