UNPKG

@niivue/niivue

Version:

minimal webgl2 nifti image viewer

786 lines (730 loc) 33 kB
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <title>NiiVue - Zarr Viewer</title> <link rel="stylesheet" href="niivue.css" /> <link rel="icon" href="data:;base64,iVBORw0KGgo=" /> <style> #controls { display: flex; gap: 16px; align-items: center; padding: 10px 12px; background: #111; flex-wrap: wrap; border-bottom: 1px solid #222; } #controls label { color: #999; font-size: 13px; } #controls input, #controls select { padding: 5px 8px; background: #222; color: #eee; border: 1px solid #444; border-radius: 3px; } #datasetSelect { max-width: 360px; } #datasetSelect option { padding: 2px 4px; } #datasetSelect optgroup { color: #aaa; font-style: normal; } #loadBtn { background: #fff; border: none; color: #000; padding: 8px 16px; cursor: pointer; border-radius: 4px; font-weight: 600; font-size: 13px; letter-spacing: 0.01em; } #loadBtn:hover { background: #ddd; } #loadBtn:disabled { background: #555; color: #888; cursor: not-allowed; } #navControls { display: none; gap: 8px; align-items: center; } #navControls button { background: #222; border: 1px solid #444; color: #eee; padding: 4px 12px; cursor: pointer; border-radius: 3px; } #navControls button:hover { background: #333; } #levelSelectorsContainer { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; } .level-selector { display: flex; gap: 4px; align-items: center; background: #1a1a1a; padding: 4px 8px; border-radius: 3px; border: 1px solid #333; } .level-selector label { color: #777; font-size: 11px; } .level-selector select { padding: 2px 4px; background: #222; color: #eee; border: 1px solid #444; border-radius: 3px; } #infoPanel { padding: 8px 12px; background: #0a0a0a; color: #999; font-family: monospace; font-size: 12px; display: none; border-bottom: 1px solid #222; } .hint { color: #666; font-size: 11px; font-style: italic; } #affineControls { display: none; padding: 8px 12px; background: #0d0d0d; border-top: 1px solid #222; } #affineControls h3 { color: #bbb; margin: 0 0 8px 0; font-size: 13px; display: flex; align-items: center; gap: 12px; } #affineControls h3 button { background: #333; border: 1px solid #555; color: #eee; padding: 2px 10px; cursor: pointer; border-radius: 3px; font-size: 11px; } #affineControls h3 button:hover { background: #444; } #affineSaveBtn, #affineLoadBtn { background: #fff !important; color: #000 !important; border-color: #fff !important; font-weight: 600; } #affineSaveBtn:hover, #affineLoadBtn:hover { background: #ddd !important; border-color: #ddd !important; } .affine-group { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 4px; } .affine-group label { color: #777; font-size: 11px; font-family: monospace; } .affine-slider { display: flex; align-items: center; gap: 6px; } .affine-slider input[type="range"] { width: 120px; } .affine-slider .val { color: #ccc; font-family: monospace; font-size: 11px; min-width: 50px; text-align: right; } </style> </head> <body> <header> <div id="controls"> <label for="datasetSelect"><a href="https://github.com/InsightSoftwareConsortium/OMEZarrOpenSciVisDatasets" target="_blank" rel="noopener" style="color:#999;text-decoration:underline">Dataset</a>:</label> <select id="datasetSelect"> <option value="">-- Custom URL --</option> <optgroup label="CT Scans"> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/aneurism.ome.zarr">Aneurism - head arteries, 256³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/backpack.ome.zarr">Backpack - CT of backpack, 512x512x373, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/beechnut.ome.zarr">Beechnut - microCT, 1024x1024x1546, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/bonsai.ome.zarr">Bonsai - CT of tree, 256³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/boston_teapot.ome.zarr">Boston Teapot - SIGGRAPH teapot, 256x256x178, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/bunny.ome.zarr">Bunny - Stanford Bunny CT, 512x512x361, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/carp.ome.zarr">Carp - fish CT, 256x256x512, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/chameleon.ome.zarr">Chameleon - CT, 1024x1024x1080, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/christmas_tree.ome.zarr">Christmas Tree - CT, 512x499x512, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/engine.ome.zarr">Engine - engine block CT, 256x256x128, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/foot.ome.zarr">Foot - human foot, 256³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/kingsnake.ome.zarr">Kingsnake - snake egg CT, 1024x1024x795, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/lobster.ome.zarr">Lobster - CT in resin, 301x324x56, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pancreas.ome.zarr">Pancreas - abdominal CT, 240x512x512, int16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pawpawsaurus.ome.zarr" selected>Pawpawsaurus - fossil CT, 958x646x1088, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pig_heart.ome.zarr">Pig Heart - microCT, 2048x2048x2612, int16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/present.ome.zarr">Present - industrial CT, 492x492x442, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/prone.ome.zarr">Prone - abdomen CT, 512x512x463, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/skull.ome.zarr">Skull - phantom skull, 256³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/spathorhynchus.ome.zarr">Spathorhynchus - fossil CT, 1024x1024x750, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/stag_beetle.ome.zarr">Stag Beetle - industrial CT, 832x832x494, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/statue_leg.ome.zarr">Statue Leg - bronze statue CT, 341x341x93, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/stent.ome.zarr">Stent - abdomen with stent, 512x512x174, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/synthetic_truss_with_five_defects.ome.zarr">Synthetic Truss - simulated CT, 1200³, float32</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/vertebra.ome.zarr">Vertebra - angiography, 512³, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/vis_male.ome.zarr">Visible Male - head scan, 128x256x256, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/woodbranch.ome.zarr">Woodbranch - microCT, 2048³, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/zeiss.ome.zarr">Zeiss - car part CT, 680³, uint8</option> </optgroup> <optgroup label="MRI Scans"> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/frog.ome.zarr">Frog - MRI, 256x256x44, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/mri_ventricles.ome.zarr">MRI Ventricles - head CSF, 256x256x124, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/mri_woman.ome.zarr">MRI Woman - head MRI, 256x256x109, uint16</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/mrt_angio.ome.zarr">MRT Angio - head angiography, 416x512x112, uint16</option> </optgroup> <optgroup label="Microscopy"> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/marmoset_neurons.ome.zarr">Marmoset Neurons - V1 cortex, 1024x1024x314, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/neocortical_layer_1_axons.ome.zarr">Neocortical Axons - barrel cortex, 1464x1033x76, uint8</option> </optgroup> <optgroup label="Simulations"> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/blunt_fin.ome.zarr">Blunt Fin - flow sim, 256x128x64, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/csafe_heptane.ome.zarr">CSAFE Heptane - combustion sim, 302³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/duct.ome.zarr">Duct - wall-bounded flow, 193x194x1000, float32</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/fuel.ome.zarr">Fuel - fuel injection sim, 64³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/hcci_oh.ome.zarr">HCCI OH - autoignition sim, 560³, float32</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/hydrogen_atom.ome.zarr">Hydrogen Atom - electron dist, 128³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/jicf_q.ome.zarr">JICF Q - jet crossflow, 1408x1080x1100, float32</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/magnetic_reconnection.ome.zarr">Magnetic Reconnection - sim, 512³, float32</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/marschner_lobb.ome.zarr">Marschner-Lobb - high freq test, 41³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/miranda.ome.zarr">Miranda - Rayleigh-Taylor sim, 1024³, float32</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/neghip.ome.zarr">Neghip - protein electron dist, 64³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/nucleon.ome.zarr">Nucleon - nuclear sim, 41³, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/richtmyer_meshkov.ome.zarr">Richtmyer-Meshkov - instability, 2048x2048x1920, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/shockwave.ome.zarr">Shockwave - planar shock sim, 64x64x512, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/silicium.ome.zarr">Silicium - grid sim, 98x34x34, uint8</option> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/tacc_turbulence.ome.zarr">TACC Turbulence - enstrophy, 256³, float32</option> </optgroup> <optgroup label="Other"> <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/tooth.ome.zarr">Tooth - 103x94x161, uint8</option> </optgroup> </select> <label for="storeUrl">URL:</label> <input type="text" id="storeUrl" value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pawpawsaurus.ome.zarr" style="width: 400px" /> <label for="maxVolSize">Max Volume Size:</label> <select id="maxVolSize"> <option value="128">128</option> <option value="256" selected>256</option> <option value="512">512</option> </select> <label for="zarrLevel">Level:</label> <select id="zarrLevel"> <option value="0" selected>0 (highest res)</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> <label for="colormapSelect">Colormap:</label> <select id="colormapSelect"></select> <label for="primaryDrag">Left Drag:</label> <select id="primaryDrag"> <option value="crosshair">crosshair</option> <option value="contrast">brightness/contrast</option> </select> <label for="secondaryDrag">Right Drag:</label> <select id="secondaryDrag"> <option value="contrast">contrast</option> <option value="pan" selected>pan</option> <option value="measurement">measurement</option> <option value="slicer3D">slicer3D</option> <option value="none">none</option> </select> <button id="loadBtn">Load Zarr</button> <button id="toggleAffineBtn" style="display:none;background:#222;border:1px solid #444;color:#eee;padding:8px 12px;cursor:pointer;border-radius:4px;font-size:13px;">Registration</button> <div id="navControls"> <span class="hint">Drag to pan</span> <div id="levelSelectorsContainer"></div> </div> </div> </header> <div id="infoPanel"></div> <div id="affineControls"> <h3> Registration Controls (Zarr Volume) <button id="affineResetBtn">Reset</button> <button id="affineSaveBtn">Save</button> <button id="affineLoadBtn">Load</button> </h3> <div class="affine-group"> <label>Translate (mm):</label> <div class="affine-slider"> <label>X</label> <input type="range" id="txSlider" min="-100" max="100" step="0.5" value="0" /> <span class="val" id="txVal">0.0</span> </div> <div class="affine-slider"> <label>Y</label> <input type="range" id="tySlider" min="-100" max="100" step="0.5" value="0" /> <span class="val" id="tyVal">0.0</span> </div> <div class="affine-slider"> <label>Z</label> <input type="range" id="tzSlider" min="-100" max="100" step="0.5" value="0" /> <span class="val" id="tzVal">0.0</span> </div> </div> <div class="affine-group"> <label>Rotate (deg):</label> <div class="affine-slider"> <label>X</label> <input type="range" id="rxSlider" min="-180" max="180" step="1" value="0" /> <span class="val" id="rxVal">0</span> </div> <div class="affine-slider"> <label>Y</label> <input type="range" id="rySlider" min="-180" max="180" step="1" value="0" /> <span class="val" id="ryVal">0</span> </div> <div class="affine-slider"> <label>Z</label> <input type="range" id="rzSlider" min="-180" max="180" step="1" value="0" /> <span class="val" id="rzVal">0</span> </div> </div> <div class="affine-group"> <label>Scale:</label> <div class="affine-slider"> <label>X</label> <input type="range" id="sxSlider" min="0.5" max="2.0" step="0.01" value="1.0" /> <span class="val" id="sxVal">1.00</span> </div> <div class="affine-slider"> <label>Y</label> <input type="range" id="sySlider" min="0.5" max="2.0" step="0.01" value="1.0" /> <span class="val" id="syVal">1.00</span> </div> <div class="affine-slider"> <label>Z</label> <input type="range" id="szSlider" min="0.5" max="2.0" step="0.01" value="1.0" /> <span class="val" id="szVal">1.00</span> </div> </div> </div> <main id="canvas-container"> <div style="display: flex; width: 100%; height: 100%"> <canvas id="gl1"></canvas> </div> </main> <footer> <label id="statusBar">Enter a zarr store URL and click Load</label> </footer> </body> </html> <script type="module" async> import { Niivue, DRAG_MODE, SLICE_TYPE, SHOW_RENDER, MULTIPLANAR_TYPE, copyAffine } from './niivue/index.ts' const datasetSelect = document.getElementById('datasetSelect') const storeUrlInput = document.getElementById('storeUrl') const maxVolSizeSelect = document.getElementById('maxVolSize') const zarrLevelSelect = document.getElementById('zarrLevel') const loadBtn = document.getElementById('loadBtn') const statusBar = document.getElementById('statusBar') const navControls = document.getElementById('navControls') const infoPanel = document.getElementById('infoPanel') const levelSelectorsContainer = document.getElementById('levelSelectorsContainer') const affineControls = document.getElementById('affineControls') const affineResetBtn = document.getElementById('affineResetBtn') const affineSaveBtn = document.getElementById('affineSaveBtn') const affineLoadBtn = document.getElementById('affineLoadBtn') const toggleAffineBtn = document.getElementById('toggleAffineBtn') toggleAffineBtn.addEventListener('click', () => { const visible = affineControls.style.display === 'block' affineControls.style.display = visible ? 'none' : 'block' toggleAffineBtn.textContent = visible ? 'Registration' : 'Hide Registration' nv.resizeListener() }) // Affine slider elements const sliders = { tx: document.getElementById('txSlider'), ty: document.getElementById('tySlider'), tz: document.getElementById('tzSlider'), rx: document.getElementById('rxSlider'), ry: document.getElementById('rySlider'), rz: document.getElementById('rzSlider'), sx: document.getElementById('sxSlider'), sy: document.getElementById('sySlider'), sz: document.getElementById('szSlider'), } const valDisplays = { tx: document.getElementById('txVal'), ty: document.getElementById('tyVal'), tz: document.getElementById('tzVal'), rx: document.getElementById('rxVal'), ry: document.getElementById('ryVal'), rz: document.getElementById('rzVal'), sx: document.getElementById('sxVal'), sy: document.getElementById('syVal'), sz: document.getElementById('szVal'), } // Sync dataset select with URL input datasetSelect.addEventListener('change', () => { if (datasetSelect.value) { storeUrlInput.value = datasetSelect.value } }) // When URL input is manually edited, switch dropdown to "Custom URL" storeUrlInput.addEventListener('input', () => { const match = [...datasetSelect.options].find(o => o.value === storeUrlInput.value.trim()) datasetSelect.value = match ? match.value : '' }) const ZARR_VOL_IDX = 1 function applyAffineFromSliders() { if (nv.volumes.length <= ZARR_VOL_IDX) return const transform = { translation: [parseFloat(sliders.tx.value), parseFloat(sliders.ty.value), parseFloat(sliders.tz.value)], rotation: [parseFloat(sliders.rx.value), parseFloat(sliders.ry.value), parseFloat(sliders.rz.value)], scale: [parseFloat(sliders.sx.value), parseFloat(sliders.sy.value), parseFloat(sliders.sz.value)], } // Reset to original before applying cumulative transform nv.resetVolumeAffine(ZARR_VOL_IDX) nv.applyVolumeTransform(ZARR_VOL_IDX, transform) } function resetAffineSliders() { sliders.tx.value = 0; sliders.ty.value = 0; sliders.tz.value = 0 sliders.rx.value = 0; sliders.ry.value = 0; sliders.rz.value = 0 sliders.sx.value = 1; sliders.sy.value = 1; sliders.sz.value = 1 for (const key of ['tx', 'ty', 'tz']) valDisplays[key].textContent = '0.0' for (const key of ['rx', 'ry', 'rz']) valDisplays[key].textContent = '0' for (const key of ['sx', 'sy', 'sz']) valDisplays[key].textContent = '1.00' } // Wire up slider input events for (const [key, slider] of Object.entries(sliders)) { slider.addEventListener('input', () => { const val = parseFloat(slider.value) if (key.startsWith('s')) { valDisplays[key].textContent = val.toFixed(2) } else if (key.startsWith('r')) { valDisplays[key].textContent = val.toFixed(0) } else { valDisplays[key].textContent = val.toFixed(1) } applyAffineFromSliders() }) } affineResetBtn.addEventListener('click', () => { if (nv.volumes.length <= ZARR_VOL_IDX) return nv.resetVolumeAffine(ZARR_VOL_IDX) resetAffineSliders() }) function getZarrUrl() { if (nv.volumes.length <= ZARR_VOL_IDX) return null return nv.volumes[ZARR_VOL_IDX].url || storeUrlInput.value.trim() } function getSliderValues() { return { tx: parseFloat(sliders.tx.value), ty: parseFloat(sliders.ty.value), tz: parseFloat(sliders.tz.value), rx: parseFloat(sliders.rx.value), ry: parseFloat(sliders.ry.value), rz: parseFloat(sliders.rz.value), sx: parseFloat(sliders.sx.value), sy: parseFloat(sliders.sy.value), sz: parseFloat(sliders.sz.value), } } function setSliderValues(vals) { for (const key of Object.keys(sliders)) { sliders[key].value = vals[key] const v = vals[key] if (key.startsWith('s')) valDisplays[key].textContent = v.toFixed(2) else if (key.startsWith('r')) valDisplays[key].textContent = v.toFixed(0) else valDisplays[key].textContent = v.toFixed(1) } } affineSaveBtn.addEventListener('click', () => { const url = getZarrUrl() if (!url) return const vals = getSliderValues() localStorage.setItem(url, JSON.stringify(vals)) statusBar.textContent = `Affine saved for ${url}` }) affineLoadBtn.addEventListener('click', () => { const url = getZarrUrl() if (!url) return const stored = localStorage.getItem(url) if (!stored) { statusBar.textContent = `No saved affine found for ${url}` return } const vals = JSON.parse(stored) setSliderValues(vals) applyAffineFromSliders() statusBar.textContent = `Affine loaded for ${url}` }) // Create NiiVue instance const nv = new Niivue({ show3Dcrosshair: true, logLevel: 'debug', dragMode: DRAG_MODE.pan, isColorbar: true, isOrientCube: true, backColor: [0.5, 0.5, 0.5, 1], multiplanarLayout: MULTIPLANAR_TYPE.GRID, // isSliceMM: true }) await nv.attachToCanvas(document.getElementById('gl1')) nv.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS window.nv = nv nv.onLocationChange = (data) => { let text = data.string const zarrVolumes = nv.getZarrVolumes() for (const vol of zarrVolumes) { const helper = vol.zarrHelper if (!helper) continue const coords = helper.mmToLevelCoords(data.mm[0], data.mm[1], data.mm[2]) const dims = coords.levelDims const inBounds = coords.width >= 0 && coords.width < dims.width && coords.height >= 0 && coords.height < dims.height && coords.depth >= 0 && coords.depth < dims.depth const coordStr = `L${coords.level}: ${coords.width}, ${coords.height}, ${coords.depth} / ${dims.width}x${dims.height}x${dims.depth}` text += ` [${coordStr}${inBounds ? '' : ' OOB'}]` } statusBar.textContent = text } // Populate colormap select const colormapSelect = document.getElementById('colormapSelect') const maps = nv.colormaps() for (const name of maps) { const opt = document.createElement('option') opt.value = name opt.textContent = name if (name === 'gray') opt.selected = true colormapSelect.appendChild(opt) } colormapSelect.addEventListener('change', () => { if (nv.volumes.length > 0) { nv.setColormap(nv.volumes[0].id, colormapSelect.value) } }) // Primary drag (left mouse button) document.getElementById('primaryDrag').addEventListener('change', (e) => { nv.opts.dragModePrimary = e.target.value === 'contrast' ? 1 : 8 }) // Secondary drag (right mouse button) document.getElementById('secondaryDrag').addEventListener('change', (e) => { if (nv.dragModes[e.target.value] !== undefined) { nv.opts.dragMode = nv.dragModes[e.target.value] } }) function updateInfoPanel() { const helper = nv.getZarrVolume()?.zarrHelper if (!helper) { infoPanel.style.display = 'none' return } const state = helper.getViewportState() const pyramidInfo = helper.getPyramidInfo() const volDims = helper.getVolumeDims() const levelDims = helper.getLevelDims() const level0Dims = pyramidInfo.levels[0].shape const currentLevel = pyramidInfo.levels[state.level] const wasHidden = infoPanel.style.display === 'none' || infoPanel.style.display === '' infoPanel.style.display = 'block' infoPanel.innerHTML = ` <strong>Dataset:</strong> ${pyramidInfo.name}<br> <strong>Full size:</strong> ${level0Dims.join(' x ')} (${pyramidInfo.is3D ? '3D' : '2D'})<br> <strong>Pyramid levels:</strong> ${pyramidInfo.levels.length}<br> <strong>Current level:</strong> ${state.level} (${currentLevel.shape.join(' x ')})<br> <strong>Center:</strong> X=${state.centerX.toFixed(1)}, Y=${state.centerY.toFixed(1)}, Z=${state.centerZ.toFixed(1)}<br> <strong>Virtual volume:</strong> ${volDims.width} x ${volDims.height} x ${volDims.depth} ` if (wasHidden) { nv.resizeListener() } } // Create level selectors for all zarr volumes function createLevelSelectors() { levelSelectorsContainer.innerHTML = '' const zarrVolumes = nv.getZarrVolumes() if (zarrVolumes.length === 0) return zarrVolumes.forEach((vol, volIdx) => { const helper = vol.zarrHelper if (!helper) return const pyramidInfo = helper.getPyramidInfo() const currentLevel = helper.getPyramidLevel() // Create container for this volume's level selector const selectorDiv = document.createElement('div') selectorDiv.className = 'level-selector' // Create label const label = document.createElement('label') const volName = pyramidInfo.name || `Zarr ${volIdx + 1}` label.textContent = `${volName}:` selectorDiv.appendChild(label) // Create select element const select = document.createElement('select') select.dataset.volumeIndex = volIdx pyramidInfo.levels.forEach((lvl, idx) => { const option = document.createElement('option') option.value = idx option.textContent = `${idx}: ${lvl.shape.join('x')}` if (idx === currentLevel) { option.selected = true } select.appendChild(option) }) // Add change handler for this select select.addEventListener('change', async () => { const targetVolIdx = parseInt(select.dataset.volumeIndex) const targetVol = nv.getZarrVolumes()[targetVolIdx] if (!targetVol?.zarrHelper) return const level = parseInt(select.value) await targetVol.zarrHelper.setPyramidLevel(level) nv.updateGLVolume() nv.drawScene() updateInfoPanel() }) selectorDiv.appendChild(select) levelSelectorsContainer.appendChild(selectorDiv) }) } // Load button handler loadBtn.addEventListener('click', async () => { const storeUrl = storeUrlInput.value.trim() if (!storeUrl) { alert('Please enter a zarr store URL') return } const maxVolSize = parseInt(maxVolSizeSelect.value) const level = parseInt(zarrLevelSelect.value) loadBtn.disabled = true loadBtn.textContent = 'Loading...' statusBar.textContent = 'Discovering zarr structure...' try { await nv.loadVolumes([ // mni nifti // { url: "../demos/images/mni152.nii.gz", opacity: 1,colormap: "gray" }, // mni zarr (LSM simulated) // { // // url: storeUrl, // url: "http://localhost:8080/K_HE_2_multiscale.zarr", // zarrLevel: level, // zarrMaxVolumeSize: maxVolSize, // opacity: 1.0, // channel:1, // colormap: "gray", // cal_min:150, // cal_max: 261 // }, // mni WSI image // { // // url: storeUrl, // url: "http://localhost:8080/mni152_axial.ome.zarr", // // url: "http://localhost:3000/K_HE_2_ome.zarr", // // url: "http://localhost:3000/E23191_25-1A.1.ome.zarr", // zarrLevel: level, // zarrMaxVolumeSize: maxVolSize, // channel: 0, // opacity: 1.0, // colormap: "red" // }, // { // url: "http://localhost:3000/export/nifti?zarr_path=K_HE_2_ome.zarr&scale=virtual_scale1&c=0&z=0&y=0&x=0&xdim=128&ydim=128&zdim=128", // name: "nifti.nii.gz" // } // LSM image { url: storeUrl, // url: "http://localhost:8080/mni152_axial.ome.zarr", // url: "http://localhost:3000/K_HE_2_ome.zarr", // url: "https://dandiarchive.s3.amazonaws.com/zarr/7723d02f-1f71-4553-a7b0-47bda1ae8b42", // url: "http://localhost:3000/E23191_25-1A.1.ome.zarr", // url: "http://localhost:3000/test-image.zarr", zarrLevel: level, zarrMaxVolumeSize: maxVolSize, opacity: 1.0, colormap: "gray", name: "demo.ome.zarr" }, ]) // await nv.addVolumeFromUrl({ url: "../demos/images/mni152.nii.gz", opacity: 0.4,colormap: "red" }) // nv.setColormap(nv.volumes[0].id, 'gray') // Show navigation controls and registration toggle navControls.style.display = 'flex' toggleAffineBtn.style.display = 'inline-block' affineControls.style.display = 'none' toggleAffineBtn.textContent = 'Registration' resetAffineSliders() // Create level selectors for all zarr volumes createLevelSelectors() const zarrVolumes = nv.getZarrVolumes() if (zarrVolumes.length > 0) { const statusParts = zarrVolumes.map((vol, idx) => { const helper = vol.zarrHelper if (!helper) return '' const pyramidInfo = helper.getPyramidInfo() const state = helper.getViewportState() const dims = pyramidInfo.levels[state.level].shape const name = pyramidInfo.name || `Zarr ${idx + 1}` return `${name}: L${state.level + 1}/${pyramidInfo.levels.length}` }).filter(s => s).join(' | ') statusBar.textContent = statusParts updateInfoPanel() } } catch (err) { console.error('Failed to load Zarr:', err) statusBar.textContent = `Error: ${err.message}` navControls.style.display = 'none' infoPanel.style.display = 'none' affineControls.style.display = 'none' toggleAffineBtn.style.display = 'none' } finally { loadBtn.disabled = false loadBtn.textContent = 'Load Zarr' } }) console.log(` Zarr Viewer Controls: - Left-click drag: Pan the view - Level dropdown: Switch pyramid level - Standard NiiVue controls for slice navigation Uses standard loadVolumes() API with zarrLevel option. `) </script>