PageSourceSearch

https://aymuos15.github.io/js/niivue-viewer.js

js aymuos15.github.io collected 2026-10-03 08:41:05 UTC 5,894 bytes, 216 lines download raw bytes

1// ── NiiVue Neuro-Oncology Viewer ────────────────────────────────────────────
2(() => {
3  const viewer = document.getElementById("niivue-viewer");
4  const canvas = document.getElementById("nv-canvas");
5  const modSel = document.getElementById("nv-modality");
6  const viewSel = document.getElementById("nv-view");
7  if (!(viewer && canvas)) {
8    return;
9  }
10
11  const base = "assests/BraTS-MET-00001-000/";
12  const segUrl = `${base}BraTS-MET-00001-000-seg.nii.gz`;
13  const mods = {
14    t1c: `${base}BraTS-MET-00001-000-t1c.nii.gz`,
15    t1n: `${base}BraTS-MET-00001-000-t1n.nii.gz`,
16    t2f: `${base}BraTS-MET-00001-000-t2f.nii.gz`,
17    t2w: `${base}BraTS-MET-00001-000-t2w.nii.gz`,
18  };
19
20  let nv = null;
21  const blobUrls = {};
22  let segBlobUrl = null;
23  let prefetchPromise = null;
24  let initPromise = null;
25
26  async function waitForSize() {
27    for (let i = 0; i < 40; i += 1) {
28      if (canvas.clientWidth > 0 && canvas.clientHeight > 0) {
29        return;
30      }
31      // Polling frame by frame is the point here — the canvas has no size yet.
32      // biome-ignore lint/performance/noAwaitInLoops: sequential by design
33      await new Promise((r) => requestAnimationFrame(r));
34    }
35  }
36
37  function reportError(msg) {
38    let box = document.getElementById("nv-error");
39    if (!box) {
40      box = document.createElement("div");
41      box.id = "nv-error";
42      box.className = "niivue-error";
43      viewer.appendChild(box);
44    }
45    box.textContent = msg;
46    console.error("[niivue]", msg);
47  }
48
49  async function fetchBlob(url) {
50    const r = await fetch(url);
51    if (!r.ok) {
52      throw new Error(`HTTP ${r.status} for ${url}`);
53    }
54    return URL.createObjectURL(await r.blob());
55  }
56
57  function prefetchAll() {
58    if (prefetchPromise) {
59      return prefetchPromise;
60    }
61    const tasks = [
62      fetchBlob(segUrl).then((u) => {
63        segBlobUrl = u;
64      }),
65      ...Object.keys(mods).map((k) =>
66        fetchBlob(mods[k]).then((u) => {
67          blobUrls[k] = u;
68        })
69      ),
70    ];
71    prefetchPromise = Promise.all(tasks).catch((e) => {
72      reportError(
73        "Volume prefetch failed:\n" +
74          e.message +
75          (location.protocol === "file:"
76            ? "\nServe over http (e.g. `python -m http.server`)."
77            : "")
78      );
79      throw e;
80    });
81    return prefetchPromise;
82  }
83
84  function init() {
85    if (initPromise) {
86      return initPromise;
87    }
88    initPromise = (async () => {
89      if (typeof niivue === "undefined" || !niivue.Niivue) {
90        reportError("NiiVue library not loaded (check network / CDN).");
91        return;
92      }
93
94      // Off-screen sizing so attachToCanvas / texture uploads run before the user clicks.
95      const wasActive = viewer.classList.contains("active");
96      if (!wasActive) {
97        viewer.classList.add("preheating");
98      }
99      await waitForSize();
100
101      try {
102        await prefetchAll();
103        nv = new niivue.Niivue({
104          backColor: [0, 0, 0, 1],
105          crosshairColor: [1, 1, 1, 0.6],
106          isColorbar: false,
107          loadingText: "loading…",
108          show3Dcrosshair: true,
109        });
110        await nv.attachToCanvas(canvas);
111        await loadMod(modSel.value);
112        applyView(viewSel.value);
113        if (nv.resizeListener) {
114          nv.resizeListener();
115        }
116        if (nv.drawScene) {
117          nv.drawScene();
118        }
119        window.addEventListener("resize", () => {
120          if (nv?.resizeListener) {
121            nv.resizeListener();
122          }
123        });
124      } catch (e) {
125        if (e.message) {
126          reportError(`NiiVue init failed: ${e.message}`);
127        }
128      } finally {
129        if (!wasActive) {
130          viewer.classList.remove("preheating");
131        }
132      }
133    })();
134    return initPromise;
135  }
136
137  async function loadMod(mod) {
138    if (!nv) {
139      return;
140    }
141    const baseVol = {
142      colormap: "gray",
143      name: `${mod}.nii.gz`,
144      url: blobUrls[mod] || mods[mod],
145    };
146    const seg = {
147      colormap: "redyell",
148      name: "seg.nii.gz",
149      opacity: 0.6,
150      url: segBlobUrl || segUrl,
151    };
152    try {
153      await nv.loadVolumes([baseVol, seg]);
154    } catch (err) {
155      console.error("NiiVue load failed (with seg)", err);
156      try {
157        await nv.loadVolumes([baseVol]);
158      } catch (e2) {
159        reportError(`Volume load failed: ${e2.message ? e2.message : e2}`);
160      }
161    }
162  }
163
164  function applyView(v) {
165    if (!nv) {
166      return;
167    }
168    const map = {
169      axial: nv.sliceTypeAxial,
170      coronal: nv.sliceTypeCoronal,
171      multi: nv.sliceTypeMultiplanar,
172      sagittal: nv.sliceTypeSagittal,
173    };
174    nv.setSliceType(map[v]);
175  }
176
177  modSel.addEventListener("change", () => loadMod(modSel.value));
178  viewSel.addEventListener("change", () => applyView(viewSel.value));
179
180  // ── Public API: the research modal drives visibility ──
181  function activate() {
182    viewer.classList.remove("preheating");
183    viewer.classList.add("active");
184    init()
185      .then(() => {
186        if (nv?.resizeListener) {
187          nv.resizeListener();
188        }
189      })
190      .catch((e) => console.debug("[niivue] init rejected", e));
191  }
192
193  function deactivate() {
194    viewer.classList.remove("active");
195  }
196
197  const warm = () => {
198    init().catch(() => {
199      // Warm-up is best effort; a real failure surfaces on activate().
200    });
201  };
202
203  window.NiiViewer = { activate, deactivate, prefetch: warm };
204
205  // Warm the volumes as soon as the visitor heads for the research section.
206  for (const el of document.querySelectorAll(
207    'a[href="#research"], .research-link'
208  )) {
209    el.addEventListener("click", warm, { once: true });
210  }
211
212  const researchSection = document.getElementById("research");
213  if (researchSection?.classList.contains("active")) {
214    warm();
215  }
216})();

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.