Skip to main content

Dev (Kitchen Sink)

Dev

examples/dev.htmlOpen full pageSource

Code

dev.js
import { ubuntu } from '../src/assets/fonts'
import { cortex, shiny } from '../src/assets/matcaps'
import NiiVue from '@niivue/niivue'
import { SHOW_RENDER } from '../src/NVConstants.ts'

let isAnimating = false
let azimuth = 0
const elevation = 15
let lastTime = performance.now()
let frameCount = 0

saveBtn.onclick = () => {
nv1.saveDocument('myScene.nvd')
}
saveMeshBtn.onclick = () => {
nv1.saveMesh(0, 'myMesh.iwm.cbor')
}

saveVolBtn.onclick = () => {
nv1.saveVolume({ filename: 'myImage.iwi.cbor' })
}

clipSelect.onchange = function () {
const index = parseInt(this.value, 10) // selected option value as integer
let planes = [[2.0, 180, 20]]
switch (index) {
case 1:
planes = [[0.1, 180, 20]]
break
case 2:
planes = [
[0.1, 180, 20],
[0.1, 0, -20],
]
break
case 3:
planes = [
[0.0, 90, 0], //right center
[0.0, 0, -20], //posterior oblique
[0.1, 0, -90], //inferior
]
break
case 4:
planes = [
[0.3, 270, 0], //left
[0.3, 90, 0], //right
[0.0, 180, 0], //anterior
[0.1, 0, 0], //posterior
]
break
case 5:
planes = [
[0.4, 270, 0], //left
[0.4, 90, 0], //right
[0.4, 180, 0], //anterior
[0.2, 0, 0], //posterior
[0.1, 0, -90], //inferior
]
break
case 6:
planes = [
[0.4, 270, 0], //left
[-0.1, 90, 0], //right
[0.4, 180, 0], //anterior
[0.2, 0, 0], //posterior
[0.1, 0, -90], //inferior
[0.3, 0, 90], //superior
]
break
}
nv1.setClipPlanes(planes)
}
function animate(currentTime) {
if (!isAnimating) return
frameCount++
const elapsed = currentTime - lastTime
if (elapsed >= 1000) {
const fps = Math.round((frameCount * 1000) / elapsed)
fpsCounter.textContent = `FPS: ${fps}`
frameCount = 0
lastTime = currentTime
}
azimuth = (azimuth + 1) % 360
nv1.azimuth = azimuth
nv1.elevation = elevation
requestAnimationFrame(animate)
}

animateCheck.onclick = () => {
const wasAnimating = isAnimating
isAnimating = animateCheck.checked
if (isAnimating && !wasAnimating) {
lastTime = performance.now()
frameCount = 0
requestAnimationFrame(animate)
} else if (!isAnimating) {
fpsCounter.textContent = 'FPS: --'
}
}

xraySlide.oninput = function () {
nv1.meshXRay = this.value * 0.01
}

crosshairCheck.onclick = function () {
nv1.is3DCrosshairVisible = this.checked
nv1.crosshairWidth = this.checked ? 2 : 0
}

radioCheck.onclick = function () {
nv1.isRadiological = this.checked
}

checkCutaway.onclick = function () {
nv1.isClipPlaneCutaway = this.checked
}

colorbarCheck.onclick = function () {
nv1.isColorbarVisible = this.checked
}

orientCheck.onclick = function () {
nv1.isOrientCubeVisible = this.checked
nv1.isOrientationTextVisible = this.checked
}

const parseNames = (input) => {
if (!input || input === 'none') return []
return input
.split('+')
.map((n) => n.trim())
.filter(Boolean)
}

// Surface meshes (kind 'mesh') are swapped by meshSelect; the connectome and tract persist
const meshIndices = (kind) =>
nv1.meshes.flatMap((m, i) => (m.kind === kind ? [i] : []))

meshSelect.onchange = async () => {
for (const i of meshIndices('mesh').reverse()) await nv1.removeMesh(i)
for (const n of parseNames(meshSelect.value)) {
await nv1.addMesh({
url: `/meshes/${n}.mz3`,
opacity: Number(meshOpacitySlider.value) * 0.01,
color: [1, 0.8, 1, 0.9],
shaderType: shaderSelect.value,
})
}
}

// 0 unloads; otherwise scales nodes and edges, 3 matching the defaults (nodeScale 3, edgeScale 1)
connectomeSlider.oninput = async function () {
const scale = Number(this.value)
const connectomeOptions = { nodeScale: scale, edgeScale: scale / 3 }
const [i] = meshIndices('connectome')
if (scale === 0) {
if (i !== undefined) await nv1.removeMesh(i)
} else if (i === undefined) {
await nv1.addMesh({ url: '/meshes/connectome.jcon', connectomeOptions })
} else {
await nv1.setConnectomeOptions(i, connectomeOptions)
}
}

tractCheck.onclick = function () {
for (const i of meshIndices('tract'))
nv1.setMesh(i, { visible: this.checked })
}

volumeSelect.onchange = async () => {
nv1.removeAllVolumes()
if (volumeSelect.value === 'none') {
return
}
const names = parseNames(volumeSelect.value)
for (let i = 0; i < names.length; i++) {
const name = names[i]
const path = `/volumes/${name}.nii.gz`
const clr = colormapSelect.value
const opts = { colormap: clr }
if (i === 1) {
opts.colormap = 'warm'
opts.calMin = 3
opts.calMax = 6
}

await nv1.addVolume({ url: path, ...opts })
}
}

colormapSelect.onchange = async () => {
nv1.setVolume(0, { colormap: colormapSelect.value })
}

matcapSelect.onchange = async () => {
await nv1.loadMatcap(matcapSelect.value)
}

gradSlider.oninput = () => {
nv1.volumeIllumination = Number(gradSlider.value) / 100
matcapSelect.disabled = Number(gradSlider.value) < 1
}

colorBtn.addEventListener('input', (event) => {
const input = event.target
const hex = input.value
const r = parseInt(hex.slice(1, 3), 16) / 255
const g = parseInt(hex.slice(3, 5), 16) / 255
const b = parseInt(hex.slice(5, 7), 16) / 255
nv1.backgroundColor = [r, g, b, 1.0]
})

dprSelect.onchange = async () => {
let v = dprSelect.value.trim()
if (v.toLowerCase() === 'auto') v = '-1'
nv1.devicePixelRatio = parseFloat(v)
}

shaderSelect.onchange = () => {
for (const i of meshIndices('mesh'))
nv1.setMesh(i, { shaderType: shaderSelect.value })
}

meshOpacitySlider.oninput = () => {
for (const i of meshIndices('mesh'))
nv1.setMesh(i, { opacity: Number(meshOpacitySlider.value) * 0.01 })
}

sliceType.onchange = () => {
nv1.sliceType = parseInt(sliceType.value, 10)
}

colorSelect.onchange = function () {
const index = this.selectedIndex
const clr = nv1.clipPlaneColor
switch (index) {
case 0:
clr[3] = 0.0
break
case 1:
clr[3] = 0.4
break
case 2:
clr[3] = -0.2
break
}
nv1.clipPlaneColor = clr
}

webgpuCheck.onchange = async function () {
await nv1.reinitializeView({ backend: this.checked ? 'webgpu' : 'webgl2' })
}

antiAliasCheck.onchange = async () => {
await nv1.reinitializeView({ isAntiAlias: antiAliasCheck.checked })
}

const api = webgpuCheck.checked ? 'webgpu' : 'webgl2'
const nv1 = new NiiVue({
backend: api,
backgroundColor: [1, 1, 1, 1],
font: ubuntu,
matcaps: { Cortex: cortex, Shiny: shiny },
showRender: SHOW_RENDER.ALWAYS,
logLevel: 'debug',
})
//const nv1 = new NiiVue({ backend: 'webgl2', backgroundColor: [1, 1, 1, 1] })
await nv1.attachToCanvas(gl1)

for (const shader of nv1.meshShaders) {
const option = document.createElement('option')
option.value = shader
option.textContent = shader.charAt(0).toUpperCase() + shader.slice(1)
shaderSelect.appendChild(option)
}
for (const cmap of nv1.colormapNames('continuous')) {
const option = document.createElement('option')
option.value = cmap
option.textContent = cmap.charAt(0).toUpperCase() + cmap.slice(1)
if (cmap.toLowerCase() === 'gray') {
option.selected = true
}
colormapSelect.appendChild(option)
}

//nv1.setClipPlaneDepthAziElev(0, 0, 0)
nv1.volumeIsAlphaClipDark = true
gradSlider.oninput()
await meshSelect.onchange()
//await volumeSelect.onchange()
clipSelect.onchange()
//nv1.setClipPlane([-0.15, 180, 30])
//nv1.setClipPlane([0, -90, 0])
await volumeSelect.onchange()
//nv1.setClipPlane([0.2, 0, 0])
checkCutaway.onclick()
//nv1.setCrosshairPos([25, 33, 48])
//nv1.colorSelect.onchange()
colorSelect.onchange()
await connectomeSlider.oninput()
await nv1.addMesh({
url: '/meshes/TR_S_R.tt.gz',
rgba255: [0, 255, 255, 255],
tractOptions: { fiberRadius: 0.5 },
})