@niivue/niivue
Version:
minimal webgl2 nifti image viewer
152 lines (144 loc) • 4.82 kB
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</title>
<link rel="stylesheet" href="niivue.css" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
</head>
<header>
<label for="dragMode">Drag mode</label>
<select id="dragMode">
<option value="contrast">contrast</option>
<option value="measurement">measurement</option>
<option value="none">none</option>
<option value="pan" selected>pan/zoom</option>
</select>
<label for="thickSelect"> Thickness</label>
<select id="thickSelect">
<option>0.5</option>
<option>1.0</option>
<option>1.5</option>
<option selected>2.0</option>
<option>2.5</option>
<option>3.0</option>
</select>
</header>
<main id="canvas-container">
<div style="display: flex; width: 100%; height: 100%">
<canvas id="gl1"></canvas>
</div>
</main>
<footer>
<label id="statusBar"> </label>
</footer>
</html>
<script type="module" async>
import { Niivue, NVImage, NVMesh, NVMeshLoaders, SHOW_RENDER, DRAG_MODE, SLICE_TYPE } from './niivue/index.ts'
thickSelect.onchange = function() {
nv1.setMeshThicknessOn2D(parseFloat(this.options[this.selectedIndex].text))
}
dragMode.onchange = function () {
switch (this.value) {
case "none":
nv1.opts.dragMode = nv1.dragModes.none
break
case "contrast":
nv1.opts.dragMode = nv1.dragModes.contrast
break
case "measurement":
nv1.opts.dragMode = nv1.dragModes.measurement
break
case "pan":
nv1.opts.dragMode = nv1.dragModes.pan
break
}
}
var volumeList1 = [
{ url: "../demos/images/fs/brainmask.mgz" },
]
const onLocationChange = (data) => {
statusBar.innerHTML = data.string
}
let defaults = {
loglevel: 'debug',
show3Dcrosshair: true,
onLocationChange: onLocationChange,
}
var nv1 = new Niivue(defaults)
await nv1.attachToCanvas(gl1)
nv1.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS
nv1.opts.isOrientCube = true
nv1.opts.yoke3Dto2DZoom = true
nv1.opts.dragMode = nv1.dragModes.pan
nv1.opts.crosshairGap = 6
await nv1.loadVolumes(volumeList1)
await nv1.loadMeshes([
{ url: "../demos/images/fs/rh.white", rgba255: [255, 0, 255, 255] },
{ url: "../demos/images/fs/rh.pial", rgba255: [0, 255, 64, 255] },
])
nv1.setMeshShader(nv1.meshes[0].id, 'crosscut')
nv1.setMeshShader(nv1.meshes[1].id, 'crosscut')
nv1.setClipPlane([0.0, 180, 20])
// Chunk-based random volume data animation
function startChunkAnimation() {
const vol = nv1.volumes[0]
if (!vol || !vol.img) return
const nx = vol.hdr.dims[1]
const ny = vol.hdr.dims[2]
const nz = vol.hdr.dims[3]
const datatype = vol.hdr.datatypeCode
const CHUNK_SIZE = 8
// Pre-compute chunks (only include voxels with value > 0)
const chunks = []
for (let cz = 0; cz < nz; cz += CHUNK_SIZE) {
for (let cy = 0; cy < ny; cy += CHUNK_SIZE) {
for (let cx = 0; cx < nx; cx += CHUNK_SIZE) {
const indices = []
for (let z = cz; z < Math.min(cz + CHUNK_SIZE, nz); z++) {
for (let y = cy; y < Math.min(cy + CHUNK_SIZE, ny); y++) {
for (let x = cx; x < Math.min(cx + CHUNK_SIZE, nx); x++) {
const idx = x + y * nx + z * nx * ny
if (vol.img[idx] > 0) {
indices.push(idx)
}
}
}
}
// Only add chunk if it has non-zero voxels
if (indices.length > 0) {
chunks.push(indices)
}
}
}
}
// Get random value generator based on data type
function getRandomValue() {
switch (datatype) {
case 2: return Math.floor(Math.random() * 256) // UINT8
case 4: return Math.floor(Math.random() * 65536) - 32768 // INT16
case 512: return Math.floor(Math.random() * 65536) // UINT16
case 16: return Math.random() // FLOAT32
default: return Math.floor(Math.random() * 256)
}
}
// Animation loop - run as fast as possible
function animate() {
for (const chunkIndices of chunks) {
const value = getRandomValue()
for (const idx of chunkIndices) {
vol.img[idx] = value
}
}
nv1.updateGLVolume()
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
}
// Auto-execute on page load
//startChunkAnimation()
</script>