PageSourceSearch

https://architectedmatter.ai/js/people.js

js architectedmatter.ai collected 2026-10-05 16:16:39 UTC 5,595 bytes, 138 lines download raw bytes

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    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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">&nbsp;</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.