SurfView.js
A modular Three.js-based brain surface visualization library for neuroimaging applications.
Features
- High-performance 3D brain surface rendering
- Multiple layer support with blending modes
- Customizable colormaps for data visualization
- React component support
- Interactive controls with Tweakpane UI
- Support for GIFTI format
- TypeScript support
Installation
Install the library with its peer dependencies (Three.js + Tweakpane). React bindings need React 18+.
npm install surfview three tweakpane
# React apps
npm install react react-dom
# Optional Tweakpane extras
npm install @tweakpane/plugin-essentialsQuick Start
Basic Usage (Vanilla JS)
import { NeuroSurfaceViewer, SurfaceGeometry, ColorMappedNeuroSurface } from 'surfview';
const container = document.getElementById('viewer-container');
const viewer = new NeuroSurfaceViewer(container, 800, 600, { showControls: true });
// Typed arrays for vertices (xyz) and faces (triangle indices)
const geometry = new SurfaceGeometry(
myVerticesFloat32Array,
myFacesUint32Array,
'left' // hemisphere tag
);
const surface = new ColorMappedNeuroSurface(
geometry,
null,
myActivationDataFloat32Array,
'viridis'
);
viewer.addSurface(surface, 'brain');
viewer.startRenderLoop();Demo Hub
Run a unified, menu-driven set of visual checks:
npm run demoThis starts a Vite-powered demo app under demo/ with scenarios for quick-start rendering, multi-layer compositing, lighting/material tuning, hemisphere layouts, and file loading (using fixtures in tests/data). Use it for quick sanity passes before releases.
React Usage
import React, { useRef } from 'react';
import NeuroSurfaceViewerReact, { useNeuroSurface } from 'surfview/react';
function BrainViewer() {
const viewerRef = useRef();
const { addSurface } = useNeuroSurface(viewerRef);
const handleReady = () => {
addSurface({
type: 'multi-layer',
vertices: vertexData,
faces: faceData,
hemisphere: 'left',
config: { baseColor: 0xdddddd }
});
};
return (
<NeuroSurfaceViewerReact
ref={viewerRef}
width={window.innerWidth}
height={window.innerHeight}
config={{
showControls: true,
ambientLightColor: 0x404040
}}
viewpoint="lateral"
onReady={handleReady}
/>
);
}Core Components
NeuroSurfaceViewer
The main viewer class that manages the Three.js scene, camera, and rendering.
Surface Types
- NeuroSurface: Basic surface with solid color
- ColorMappedNeuroSurface: Surface with data-driven colormapping
- VertexColoredNeuroSurface: Surface with per-vertex colors
- MultiLayerNeuroSurface: Surface supporting multiple data layers
Layer System
Layers allow you to overlay multiple data visualizations on the same surface:
- BaseLayer: The foundational surface layer
- DataLayer: Scalar data with colormap
- RGBALayer: Pre-computed RGBA colors per vertex
- OutlineLayer: ROI boundary outlines
- LabelLayer: Discrete region labels
// Add a data layer to existing surface
surface.addLayer(new DataLayer(
'activation',
activationData,
{
colorMap: 'hot',
range: [-5, 5],
opacity: 0.7,
blendMode: 'additive'
}
));Layer management quick hits
- Add:
surface.addLayer(layer)wherelayerisBaseLayer,DataLayer,RGBALayer,OutlineLayer, orLabelLayer. - Update:
surface.updateLayer(id, updates)for single-layer tweaks orsurface.updateLayers([{ id, ...updates }])for batches (notyperequired when updating). - Order:
surface.setLayerOrder(['base', 'activation', 'roi']). - Clear:
surface.clearLayers()removes all non-base layers; pass{ includeBase: true }to drop the base too. - CPU vs GPU compositing: pass
useGPUCompositing: trueinMultiLayerNeuroSurfaceconfig to enable WebGL2-based blending; callsurface.setWideLines(false)if your platform dislikes wide-line outlines.
Available Colormaps
The library includes many standard scientific colormaps:
- Sequential:
viridis,plasma,inferno,magma,hot,cool - Diverging:
RdBu,bwr,coolwarm,seismic,Spectral - Qualitative:
jet,hsv,rainbow - Monochrome:
greys,blues,reds,greens
Loading Data
GIFTI Format
import { loadSurface, ColorMappedNeuroSurface } from 'surfview';
const geometry = await loadSurface('path/to/surface.gii', 'gifti', 'left');
const surface = new ColorMappedNeuroSurface(geometry, null, dataArray, 'coolwarm');
viewer.addSurface(surface, 'lh-brain');
// Node/SSR: loadSurface will auto-use jsdom if present; otherwise supply a DOMParser:
// const domParser = new (await import('jsdom')).JSDOM().window.DOMParser;
// const geometry = parseGIfTISurface(giftiXml, domParser);Custom Data Format
const surfaceData = {
vertices: Float32Array, // x,y,z coordinates
faces: Uint32Array, // triangle indices
data: Float32Array // optional per-vertex data
};API Reference
NeuroSurfaceViewer
Constructor
new NeuroSurfaceViewer(container: HTMLElement, width: number, height: number, config?: ViewerConfig, viewpoint?: Viewpoint)
Config Options
interface ViewerConfig {
showControls?: boolean;
useControls?: boolean; // leave false to tree-shake Tweakpane
allowCDNFallback?: boolean; // opt-in CDN fetch if tweakpane peer is missing
backgroundColor?: number;
ambientLightColor?: number;
directionalLightColor?: number;
directionalLightIntensity?: number;
rotationSpeed?: number;
initialZoom?: number;
ssaoRadius?: number;
ssaoKernelSize?: number;
rimStrength?: number;
metalness?: number;
roughness?: number;
useShaders?: boolean;
controlType?: 'trackball' | 'surface';
preset?: 'default' | 'presentation';
linkHemispheres?: boolean;
hoverCrosshair?: boolean;
hoverCrosshairColor?: number;
hoverCrosshairSize?: number;
clickToAddAnnotation?: boolean;
}
type Viewpoint = 'lateral' | 'medial' | 'ventral' | 'posterior' | 'anterior' | 'unknown_lateral';Methods
addSurface(surface, id?): Add a surface to the sceneremoveSurface(id): Remove a surfaceclearSurfaces(): Remove all surfacescenterCamera(): Center camera on all surfacesresetCamera(): Reset camera distance/upsetViewpoint(viewpoint): Set camera viewpointstartRenderLoop(): Begin the animation/render loopresize(width, height): Resize renderer + controlstoggleControls(show?): Show/hide Tweakpane UIaddLayer(surfaceId, layer),updateLayer(surfaceId, layerId, updates),removeLayer(surfaceId, layerId),clearLayers(surfaceId, { includeBase? })pick({ x, y }): Ray-pick a surface/vertex under screen coordinatesdispose(): Clean up resourcesshowCrosshair(surfaceId, vertexIndex, { size?, color? }): Draw a 3-axis crosshair on a vertexhideCrosshair(): Remove the crosshairtoggleCrosshair(surfaceId?, vertexIndex?, { size?, color? }): Toggle the crosshair (reuses last target if omitted)addAnnotation(surfaceId, vertexIndex, data?, options?): Add a small marker spherelistAnnotations(surfaceId?),moveAnnotation(id, vertexIndex),removeAnnotations(surfaceId): Manage markers in bulk
Minimal pick-to-crosshair example:
const hit = viewer.pick({ x: event.clientX, y: event.clientY });
if (hit.surfaceId && hit.vertexIndex !== null) {
viewer.showCrosshair(hit.surfaceId, hit.vertexIndex, { size: 2, color: 0xffcc00 });
}Interaction helpers
- Set
config.hoverCrosshair = trueto show a lightweight hover crosshair (throttled). - Set
config.clickToAddAnnotation = trueto drop an annotation + activate it on click. onSurfaceClickis now fired from the core viewer after a successful pick.- Controls are opt-in: set
config.useControls = true/showControls = true(and optionallyallowCDNFallback = true) and install thetweakpanepeer if you want the built-in UI.
ColorMap
Creating Custom Colormaps
import { ColorMap } from 'surfview';
const customColormap = new ColorMap([
[0, 0, 1], // blue
[0, 1, 0], // green
[1, 1, 0], // yellow
[1, 0, 0] // red
], {
range: [0, 100],
threshold: [10, 90]
});Browser Support
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
Requires WebGL 2.0 support.
Performance Cheatsheet
- Disable SSAO/shadows and tonemapping for maximum FPS (
useShaders=false, shadows off by default in current build). - Reduce render size on high-DPI displays (
renderer.setPixelRatio(1)or pass a smaller width/height toresize). - Prefer flat colors over PBR materials for large meshes.
- Keep GPU compositing off unless you really need multi-layer blending.
Troubleshooting
- If you see “WebGL is not available”, confirm hardware acceleration is enabled and the browser supports WebGL 2.
- For SSR/Node environments, only construct
NeuroSurfaceViewerin the browser (e.g., inside auseEffectin React). - If you must import on the server, use the provided
NoopNeuroSurfaceViewerandhasDOMhelpers to avoid touching the DOM/GL.tsimport { hasDOM, NoopNeuroSurfaceViewer, NeuroSurfaceViewer } from 'surfview'; const Viewer = hasDOM() ? NeuroSurfaceViewer : NoopNeuroSurfaceViewer; const viewer = new Viewer(container, 800, 600); - Next.js/Remix SSR guard for React:jsx
import dynamic from 'next/dynamic'; const SSRSafeViewer = dynamic(() => import('surfview/react').then(m => m.NeuroSurfaceViewerReact), { ssr: false });
Events you can listen for
surface:added|surface:removed|surface:variantlayer:added|layer:removed|layer:updated|layer:colormap|layer:intensity|layer:threshold|layer:opacitysurface:click(pick result),render:before|render:after,render:neededannotation:added|annotation:moved|annotation:removed|annotation:reset|annotation:activatedviewpoint:changed,controls:changed|controls:error
Example:
viewer.on('layer:intensity', ({ layerId, range }) => {
console.log('Layer', layerId, 'intensity changed to', range);
});Development
# Install dependencies
npm install
# Development server
npm run dev
# Build library
npm run build
# Type checking
npm run type-check
# Playwright smoke (run after installing browsers with `npx playwright install chromium`)
npm run test:playwrightLicense
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Acknowledgments
Built with: