PageSourceSearch

https://aymuos15.github.io/js/research.js

js aymuos15.github.io collected 2026-10-03 08:41:04 UTC 15,803 bytes, 509 lines download raw bytes

1// ── Research: fanned theme deck → modal ──────────────────────────────────────
2
3// Pastel VIBGYOR tints, echoing the instance-diagram blobs.
4const researchThemes = [
5  {
6    blurb: "Tumours at every scale.",
7    id: "segmentation",
8    interactive: "niivue",
9    intro:
10      "The core of my PhD is segmenting neuro-oncology images at every granularity. Drag, scroll and switch modality on a real BraTS multi-metastasis case below.",
11    orb3d: "head",
12    papers: [
13      {
14        title: "SegPatch",
15        url: "https://github.com/aymuos15/SegPatch",
16        venue: "ISBI 2024",
17      },
18    ],
19    tint: "#b8a0d8",
20    title: "Segmentation",
21  },
22  {
23    blurb: "When averaging hides the small lesions.",
24    id: "instance-metrics",
25    interactive: "instance",
26    intro:
27      "Overlap metrics favour big structures, so tiny lesions barely dent the score. I build instance-aware losses that weight every lesion equally. Toggle to see the effect.",
28    orb3d: "sphere",
29    papers: [
30      {
31        title: "Cluster Dice",
32        url: "https://github.com/aymuos15/Cluster-Dice",
33        venue: "SPIE Medical Imaging 2025",
34      },
35      {
36        title:
37          "Instance Awareness of Multi-class Semantic Segmentation Loss Functions",
38        url: "https://github.com/BrainImageAnalysis/instance-loss",
39        venue: "CVPR CV4Clinical Workshop 2026",
40      },
41    ],
42    tint: "#8b9bd4",
43    title: "Instance Metrics",
44  },
45];
46
47// ── Instance Imbalance Interactive Diagram (mounts into a given container) ────
48function buildInstanceDiagram(container) {
49  if (!container) {
50    return;
51  }
52
53  const blobs = [
54    { r: 34, x: 14, y: 40 },
55    { r: 24, x: 38, y: 28 },
56    { r: 18, x: 62, y: 58 },
57    { r: 13, x: 28, y: 72 },
58    { r: 9, x: 76, y: 32 },
59    { r: 6, x: 50, y: 16 },
60    { r: 5, x: 82, y: 68 },
61    { r: 4, x: 90, y: 18 },
62  ];
63
64  const totalArea = blobs.reduce((s, b) => s + b.r * b.r, 0);
65  const N = blobs.length;
66  for (const b of blobs) {
67    b.diceW = (b.r * b.r) / totalArea;
68    b.instW = 1 / N;
69  }
70
71  // Pastel VIBGYOR instance colors
72  const vibgyor = [
73    "#b8a0d8",
74    "#8b9bd4",
75    "#7cbcd4",
76    "#8cc8a0",
77    "#d4cc80",
78    "#d8a878",
79    "#d48888",
80    "#d4a0b8",
81  ];
82
83  let mode = "dice";
84
85  // ── Label type tabs ──
86  const labels = document.createElement("div");
87  labels.className = "diagram-labels";
88
89  const lblSem = document.createElement("button");
90  lblSem.className = "label-tab active";
91  lblSem.textContent = "Semantic Labels";
92  lblSem.addEventListener("click", () => setMode("dice"));
93
94  const lblInst = document.createElement("button");
95  lblInst.className = "label-tab";
96  lblInst.textContent = "Instance Labels";
97  lblInst.addEventListener("click", () => setMode("instance"));
98
99  labels.appendChild(lblSem);
100  labels.appendChild(lblInst);
101
102  // ── Scatter view ──
103  const scatter = document.createElement("div");
104  scatter.className = "diagram-scatter";
105  for (const [i, b] of blobs.entries()) {
106    const el = document.createElement("div");
107    el.className = "diagram-blob";
108    el.style.left = `${b.x}%`;
109    el.style.top = `${b.y}%`;
110    el.style.width = `${b.r * 2}px`;
111    el.style.height = `${b.r * 2}px`;
112
113    const ring = document.createElement("div");
114    ring.className = "blob-ring";
115    el.appendChild(ring);
116
117    b.el = el;
118    b.ring = ring;
119    scatter.appendChild(el);
120
121    el.addEventListener("mouseenter", () => hi(i));
122    el.addEventListener("mouseleave", lo);
123  }
124
125  // ── Controls row: toggle + scores ──
126  const controls = document.createElement("div");
127  controls.className = "diagram-controls";
128
129  const toggle = document.createElement("div");
130  toggle.className = "diagram-toggle";
131
132  const btnD = document.createElement("button");
133  btnD.className = "diagram-btn active";
134  btnD.textContent = "Dice Metric";
135  btnD.addEventListener("click", () => setMode("dice"));
136
137  const btnI = document.createElement("button");
138  btnI.className = "diagram-btn";
139  btnI.textContent = "Instance-Aware Dice Metric";
140  btnI.addEventListener("click", () => setMode("instance"));
141
142  toggle.appendChild(btnD);
143  toggle.appendChild(btnI);
144
145  // ── Score cards ──
146  const scores = document.createElement("div");
147  scores.className = "diagram-scores";
148
149  function makeScoreCard(label) {
150    const card = document.createElement("div");
151    card.className = "score-card";
152    const lbl = document.createElement("span");
153    lbl.className = "score-label";
154    lbl.textContent = label;
155    const val = document.createElement("span");
156    val.className = "score-value";
157    card.appendChild(lbl);
158    card.appendChild(val);
159    return { card, val };
160  }
161
162  const dscCard = makeScoreCard("DSC");
163  const pqCard = makeScoreCard("PQ");
164  scores.appendChild(dscCard.card);
165  scores.appendChild(pqCard.card);
166
167  controls.appendChild(toggle);
168  controls.appendChild(scores);
169
170  const scoreTargets = {
171    dice: { dsc: 0.95, pq: 0.38 },
172    instance: { dsc: 0.88, pq: 0.76 },
173  };
174  let currentDsc = 0,
175    currentPq = 0;
176
177  // ── Vertical contribution bars ──
178  const barsWrap = document.createElement("div");
179  barsWrap.className = "diagram-bars";
180  for (const [i, b] of blobs.entries()) {
181    const col = document.createElement("div");
182    col.className = "diagram-bar-col";
183
184    const pct = document.createElement("span");
185    pct.className = "bar-pct";
186
187    const track = document.createElement("div");
188    track.className = "bar-track-v";
189
190    const fill = document.createElement("div");
191    fill.className = "bar-fill-v";
192    track.appendChild(fill);
193
194    const dot = document.createElement("div");
195    dot.className = "bar-dot";
196    const dotSize = Math.max(4, Math.round(b.r * 0.45));
197    dot.style.width = `${dotSize}px`;
198    dot.style.height = `${dotSize}px`;
199
200    col.appendChild(pct);
201    col.appendChild(track);
202    col.appendChild(dot);
203
204    b.fill = fill;
205    b.pct = pct;
206    b.col = col;
207    barsWrap.appendChild(col);
208
209    col.addEventListener("mouseenter", () => hi(i));
210    col.addEventListener("mouseleave", lo);
211  }
212
213  // ── Caption ──
214  const caption = document.createElement("p");
215  caption.className = "diagram-caption";
216
217  const top = document.createElement("div");
218  top.className = "diagram-top";
219  top.appendChild(scatter);
220  top.appendChild(barsWrap);
221
222  container.appendChild(labels);
223  container.appendChild(top);
224  container.appendChild(controls);
225  container.appendChild(caption);
226
227  // ── Updates ──
228  function setMode(m) {
229    if (m === mode) {
230      return;
231    }
232    mode = m;
233    btnD.classList.toggle("active", mode === "dice");
234    btnI.classList.toggle("active", mode === "instance");
235    btnI.classList.toggle("rainbow", mode === "instance");
236    lblSem.classList.toggle("active", mode === "dice");
237    lblInst.classList.toggle("active", mode === "instance");
238    lblInst.classList.toggle("rainbow", mode === "instance");
239    update();
240  }
241
242  function animateScore(el, from, to, flagLow) {
243    const duration = 500;
244    const start = performance.now();
245    function tick(now) {
246      const p = Math.min(1, (now - start) / duration);
247      const ease = 1 - (1 - p) ** 3;
248      const v = from + (to - from) * ease;
249      el.textContent = v.toFixed(2);
250      if (p < 1) {
251        requestAnimationFrame(tick);
252      }
253    }
254    requestAnimationFrame(tick);
255    el.classList.toggle("low", flagLow && to < 0.5);
256    el.classList.toggle("high", flagLow && to >= 0.5);
257  }
258
259  function update() {
260    const maxW = Math.max(
261      ...blobs.map((b) => (mode === "dice" ? b.diceW : b.instW))
262    );
263    for (const [i, b] of blobs.entries()) {
264      const w = mode === "dice" ? b.diceW : b.instW;
265      const n = w / maxW;
266
267      b.el.style.opacity = 0.12 + 0.88 * n;
268      b.ring.style.transform = `scale(${1 + 0.5 * n})`;
269      b.ring.style.opacity = 0.08 + 0.72 * n;
270      b.fill.style.height = `${n * 100}%`;
271      b.pct.textContent = `${(w * 100).toFixed(1)}%`;
272
273      if (mode === "instance") {
274        b.el.style.setProperty("--blob-color", vibgyor[i]);
275        b.col.style.setProperty("--blob-color", vibgyor[i]);
276      } else {
277        b.el.style.removeProperty("--blob-color");
278        b.col.style.removeProperty("--blob-color");
279      }
280    }
281
282    const t = scoreTargets[mode];
283    animateScore(dscCard.val, currentDsc, t.dsc, false);
284    animateScore(pqCard.val, currentPq, t.pq, true);
285    currentDsc = t.dsc;
286    currentPq = t.pq;
287
288    caption.textContent =
289      mode === "dice"
290        ? "With the standard Dice Score, the largest instance owns 48.5% of the metric. The 3 smallest share just 3.2%. Missing them barely changes the score."
291        : "Instance-aware: every instance contributes 12.5% to the score, regardless of size. Small lesions are weighted equally.";
292  }
293
294  // ── Hover cross-highlighting ──
295  function hi(idx) {
296    container.classList.add("has-highlight");
297    for (const [i, b] of blobs.entries()) {
298      const on = i === idx;
299      b.el.classList.toggle("highlighted", on);
300      b.col.classList.toggle("highlighted", on);
301    }
302  }
303
304  function lo() {
305    container.classList.remove("has-highlight");
306    for (const b of blobs) {
307      b.el.classList.remove("highlighted");
308      b.col.classList.remove("highlighted");
309    }
310  }
311
312  update();
313}
314
315// ── Deck + modal ─────────────────────────────────────────────────────────────
316(() => {
317  const deck = document.getElementById("research-deck");
318  const modal = document.getElementById("research-modal");
319  const modalBody = document.getElementById("research-modal-body");
320  const interactivesHolder = document.getElementById("research-interactives");
321  if (!(deck && modal && modalBody)) {
322    return;
323  }
324
325  // ── Build the fanned deck ──
326  const N = researchThemes.length;
327  const center = (N - 1) / 2;
328  for (const [i, theme] of researchThemes.entries()) {
329    const offset = i - center;
330    const rot = offset * 5;
331    const ty = Math.abs(offset) * 11;
332    const z = 10 - Math.abs(offset);
333
334    const card = document.createElement("button");
335    card.className = "research-card";
336    card.type = "button";
337    card.dataset.id = theme.id;
338    card.style.setProperty("--tint", theme.tint);
339    card.style.setProperty("--rot", `${rot}deg`);
340    card.style.setProperty("--ty", `${ty}px`);
341    card.style.setProperty("--z", z);
342    card.setAttribute("aria-label", `${theme.title} — ${theme.blurb}`);
343
344    let orbHtml = '<span class="research-card-orb"></span>';
345    if (theme.orb3d === "head") {
346      orbHtml =
347        '<span class="research-card-orb research-card-orb--scan"><canvas class="research-card-orb-scan" width="96" height="96" aria-hidden="true"></canvas></span>';
348    } else if (theme.orb3d === "sphere") {
349      orbHtml =
350        '<span class="research-card-orb research-card-orb--scan"><canvas class="research-card-orb-sphere" width="112" height="112" aria-hidden="true"></canvas></span>';
351    }
352
353    card.innerHTML = `
354            ${orbHtml}
355            <span class="research-card-label">${theme.title}</span>
356            <span class="research-card-blurb">${theme.blurb}</span>`;
357
358    card.addEventListener("click", () => openModal(theme.id));
359    deck.appendChild(card);
360  }
361
362  // ── Modal ──
363  let lastFocused = null;
364  let openInteractive = null; // track a reparented live node to return on close
365
366  function buildModalBody(theme) {
367    modalBody.innerHTML = "";
368    modalBody.style.setProperty("--tint", theme.tint);
369
370    const title = document.createElement("h3");
371    title.className = "research-modal-title";
372    title.id = "research-modal-title";
373    title.textContent = theme.title;
374    modalBody.appendChild(title);
375
376    const intro = document.createElement("p");
377    intro.className = "research-modal-intro";
378    intro.textContent = theme.intro;
379    modalBody.appendChild(intro);
380
381    if (theme.interactive === "instance") {
382      const mount = document.createElement("div");
383      mount.className = "instance-diagram";
384      modalBody.appendChild(mount);
385      buildInstanceDiagram(mount);
386    } else if (theme.interactive === "niivue") {
387      const mount = document.createElement("div");
388      mount.className = "research-niivue-mount";
389      modalBody.appendChild(mount);
390      const viewer = document.getElementById("niivue-viewer");
391      if (viewer) {
392        mount.appendChild(viewer);
393        openInteractive = viewer;
394        if (window.NiiViewer) {
395          window.NiiViewer.activate();
396          // Nudge NiiVue to size itself to the now-visible modal.
397          requestAnimationFrame(() =>
398            window.dispatchEvent(new Event("resize"))
399          );
400          setTimeout(() => window.dispatchEvent(new Event("resize")), 350);
401        }
402      }
403    }
404
405    if (theme.papers?.length) {
406      const heading = document.createElement("h4");
407      heading.className = "research-modal-heading";
408      heading.textContent = theme.papers.length > 1 ? "Papers" : "Paper";
409      modalBody.appendChild(heading);
410
411      const list = document.createElement("ul");
412      list.className = "research-papers";
413      for (const p of theme.papers) {
414        const li = document.createElement("li");
415        li.className = "research-paper";
416        li.innerHTML = `
417                    <a class="research-paper-title" href="${p.url}" target="_blank" rel="noopener">${p.title}</a>
418                    <span class="research-paper-venue">${p.venue}</span>`;
419        list.appendChild(li);
420      }
421      modalBody.appendChild(list);
422    }
423
424    if (typeof colorizeLinks === "function") {
425      colorizeLinks();
426    }
427  }
428
429  function returnInteractive() {
430    if (openInteractive && interactivesHolder) {
431      if (window.NiiViewer) {
432        window.NiiViewer.deactivate();
433      }
434      interactivesHolder.appendChild(openInteractive);
435      openInteractive = null;
436    }
437  }
438
439  function openModal(id) {
440    const theme = researchThemes.find((t) => t.id === id);
441    if (!theme) {
442      return;
443    }
444
445    lastFocused = document.activeElement;
446    buildModalBody(theme);
447
448    modal.hidden = false;
449    requestAnimationFrame(() => modal.classList.add("open"));
450    document.body.classList.add("research-modal-open");
451
452    const closeBtn = modal.querySelector(".research-modal-close");
453    if (closeBtn) {
454      closeBtn.focus();
455    }
456  }
457
458  function closeModal() {
459    if (modal.hidden) {
460      return;
461    }
462    returnInteractive();
463    modal.classList.remove("open");
464    document.body.classList.remove("research-modal-open");
465
466    const finish = () => {
467      modal.hidden = true;
468      modalBody.innerHTML = "";
469      modal.removeEventListener("transitionend", finish);
470    };
471    modal.addEventListener("transitionend", finish);
472    // Fallback if transitionend doesn't fire (e.g. reduced motion).
473    setTimeout(finish, 400);
474
475    if (lastFocused?.focus) {
476      lastFocused.focus();
477    }
478  }
479  for (const el of modal.querySelectorAll("[data-close]")) {
480    el.addEventListener("click", closeModal);
481  }
482
483  document.addEventListener("keydown", (e) => {
484    if (modal.hidden) {
485      return;
486    }
487    if (e.key === "Escape") {
488      closeModal();
489      return;
490    }
491    if (e.key === "Tab") {
492      const focusable = modal.querySelectorAll(
493        'button, a[href], select, [tabindex]:not([tabindex="-1"])'
494      );
495      if (!focusable.length) {
496        return;
497      }
498      const [first] = focusable;
499      const last = focusable.at(-1);
500      if (e.shiftKey && document.activeElement === first) {
501        e.preventDefault();
502        last.focus();
503      } else if (!e.shiftKey && document.activeElement === last) {
504        e.preventDefault();
505        first.focus();
506      }
507    }
508  });
509})();

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.