1/* ============================================ 2 PEOPLE 3 - PI portrait: a point mesh sampled from the photo 4 (js/data/portrait-amira.js, made with tools/make-portrait-points.py). 5 Opening the bio disassembles the portrait: every point travels 6 onto the group logo, which keeps breathing like the auxetic 7 lattice it is. Closing the bio reassembles the face. 8 - Members: mesh circles with initials (js/data/people.js). 9 ============================================ */ 10 11(function () { 12 const AMG = (window.AMG = window.AMG || {}); 13 14 /* ---------- PI ---------- */ 15 function initPI() { 16 const pi = document.querySelector('[data-pi]'); 17 if (!pi) return; 18 const canvas = pi.querySelector('.pi-canvas'); 19 const data = window[canvas.getAttribute('data-points')]; 20 const toggles = pi.querySelectorAll('[data-pi-toggle]'); 21 const bio = pi.querySelector('.pi-bio'); 22 let open = false; 23 24 let field = null; 25 if (data && AMG.MeshField) { 26 pi.classList.add('has-mesh'); 27 field = new AMG.MeshField(canvas, { 28 points: data, 29 eventTarget: canvas.parentElement, 30 pointRadius: 0.68, 31 sizeByWeight: true, 32 pointAlpha: 0.82, 33 pointAlphaMin: 0.06, 34 lineAlpha: 0.1, 35 lineWidth: 0.4, 36 mouseRadius: 46, 37 mouseForce: 0.55, 38 returnSpeed: 0.06, 39 turbulence: 10, 40 morphStagger: 0.45, 41 morphWeight: 0.8, 42 ghostOptions: { strength: [0.25, 0.6], dur: [1.4, 3.2], rest: [1.5, 6] }, 43 altUpdate: logoTargets, 44 onBuild: f => assignLogoSlots(f), 45 }); 46 } 47 48 function set(v) { 49 open = v; 50 pi.classList.toggle('is-open', open); 51 toggles.forEach(t => t.setAttribute('aria-expanded', String(open))); 52 if (bio) bio.setAttribute('aria-hidden', String(!open)); 53 if (field) field.setMorph(open ? 1 : 0); 54 } 55 toggles.forEach(t => t.addEventListener('click', () => set(!open))); 56 pi.addEventListener('keydown', e => { if (e.key === 'Escape' && open) set(false); }); 57 set(false); 58 } 59 60 // Each portrait point gets a fixed slot on the logo (AMG.LogoPoints in 61 // js/mesh-field.js); the logo keeps breathing while the bio is open. 62 function assignLogoSlots(f) { 63 f.slot = AMG.LogoPoints.slots(f.N); 64 f.altX = new Float32Array(f.N); 65 f.altY = new Float32Array(f.N); 66 f.logoSeed = Math.random() * 50; 67 logoTargets(f, performance.now()); 68 } 69 function logoTargets(f, now) { 70 const theta = AMG.LogoPoints.theta(now * 0.001, f.logoSeed || 0); 71 AMG.LogoPoints.place(f.slot, f.N, f.w / 2, f.h / 2, Math.min(f.w, f.h) * 0.66, theta, f.altX, f.altY); 72 } 73 AMG.logoMorph = { assign: assignLogoSlots, update: logoTargets }; 74 75 /* ---------- members ---------- */ 76 function initialsOf(name) { 77 return name.split(/\s+/).filter(Boolean).map(w => w[0]).slice(0, 2).join('').toUpperCase(); 78 } 79 const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => 80 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); 81 82 function circleMask(ctx, w, h) { 83 ctx.beginPath(); 84 ctx.arc(w / 2, h / 2, Math.min(w, h) * 0.42, 0, Math.PI * 2); 85 ctx.fill(); 86 } 87 88 function initMembers() { 89 const root = document.querySelector('[data-members]'); 90 const P = window.AMG_PEOPLE; 91 if (!root || !P) return; 92 const items = (P.members || []).map(m => ({ 93 name: m.name, role: m.role || '', initials: m.initials || initialsOf(m.name), link: m.link, portrait: m.portrait, 94 })); 95 let html = items.map(m => { 96 const tag = m.link ? 'a' : 'div'; 97 const href = m.link ? ` href="${esc(m.link)}" target="_blank" rel="noopener"` : ''; 98 return `<${tag} class="member"${href}> 99 <canvas class="member-blob" data-initials="${esc(m.initials)}" data-seed="${esc(m.name)}"${m.portrait ? ` data-points="${esc(m.portrait)}"` : ''} aria-hidden="true"></canvas> 100 <span class="member-name">${esc(m.name)}</span> 101 <span class="member-role">${esc(m.role)}</span> 102 </${tag}>`; 103 }).join(''); 104 if (P.joinBlob) { 105 html += `<a class="member member--join" href="${esc(P.joinBlob.href)}"> 106 <canvas class="member-blob" data-initials="+" data-seed="join" aria-hidden="true"></canvas>
107 <span class="member-name">${esc(P.joinBlob.label)}</span> 108 <span class="member-role"> </span> 109 </a>`; 110 } 111 root.innerHTML = html; 112 113 const start = () => root.querySelectorAll('.member-blob').forEach(c => { 114 const pts = c.getAttribute('data-points'); 115 const isJoin = c.getAttribute('data-seed') === 'join'; 116 const common = { 117 step: 4, pointRadius: 0.7, pointAlpha: 0.9, pointAlphaMin: 0.08, lineAlpha: 0.3, lineAlphaGap: 0.035, 118 mouseRadius: 42, mouseForce: 0.6, returnSpeed: 0.07, turbulence: 12, 119 ghostOptions: { firstDelay: [1, 6] }, 120 }; 121 if (pts && window[pts]) { 122 new AMG.MeshField(c, Object.assign(common, { points: window[pts], sizeByWeight: true })); 123 } else { 124 new AMG.MeshField(c, Object.assign(common, { 125 mask: circleMask, 126 inner: AMG.textMask(c.getAttribute('data-initials'), { size: isJoin ? 120 : 66, weight: isJoin ? '400' : '600', fit: 0.62 }), 127 outerWeight: 0.22, 128 })); 129 } 130 }); 131 if (document.fonts) document.fonts.ready.then(start); else start(); 132 } 133 134 function init() { initPI(); initMembers(); } 135 const go = () => (document.fonts ? document.fonts.ready.then(init) : init()); 136 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go); 137 else go(); 138})();
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.