@revanta/mumol
v0.1.1
Published
A modern protein and molecule visualization library.
Maintainers
Readme
MuMol
MuMol is a small, framework-agnostic TypeScript library for parsing and visualizing proteins and molecules in the browser. Its renderer is built exclusively on Three.js WebGPU and the Three.js Shading Language (TSL): there is no WebGL renderer, shader string, or non-node material fallback.
MuMol is at an early
0.xstage. Its API can change while the core rendering and structure model settle.
Features
- Strict WebGPU initialization with an explicit GPU adapter and device
- Physical, toon, Phong, flat, and x-ray TSL node-material shaders
- Plastic, glass, metal, ceramic, and velvet presets with live material controls
- Cartoon ribbons, backbone, wireframe, ball-and-stick, and space-filling views
- Structure, chain, CPK element, B-factor, rainbow, and pLDDT colors
- Instanced atom and split-color bond geometry, including multiple bond orders
- PDB and mmCIF parsing with models, alternate locations, and secondary structure
- XYZ parsing and spatially partitioned covalent-radius bond inference
- TSL depth of field, bloom, ambient occlusion, silhouette outline, and GPU ASCII post-processing
- Orthographic and perspective cameras, switchable at runtime
- Camera-following studio light rig with room-environment image-based lighting
- Click/API selection at atom, residue, and chain levels with highlight and focus
- Background-colored depth cueing with tighter fog for selection focus
- Mouse, touch, and trackpad orbit, zoom, and pan controls
- Demand-driven rendering that sleeps while a static view is idle, with a configurable frame-rate cap and reduced pixel ratio during interaction
The feature model follows
001TMF/ProteinView, and the cartoon
geometry semantics (spline tension, direction frames, and ribbon dimensions)
follow molstar. Shallow reference
checkouts are kept locally under vendor/ during development.
Requirements
- A browser that exposes WebGPU through
navigator.gpu - A secure context (
https://orlocalhost) three0.178 or newer as a peer dependency- Node.js 22.13 or newer for development
MuMol deliberately fails with WebGpuUnavailableError when WebGPU is not
available. It acquires the GPU device itself and disables Three.js's optional
WebGL backend switch before renderer initialization.
Install
npm install mumol three
npm install -D @types/three # TypeScript projectsThree.js is a peer dependency so applications control its version and bundlers
ship exactly one copy. @types/three is an optional peer for TypeScript
consumers; match its version to the installed three.
Quick start
<canvas id="molecule" style="width: 640px; height: 480px"></canvas>import { MuMolViewer, parsePdb } from "mumol";
const response = await fetch("/structures/1crn.pdb");
const molecule = parsePdb(await response.text());
const canvas = document.querySelector<HTMLCanvasElement>("#molecule");
if (canvas === null) {
throw new Error("Missing molecule canvas");
}
const initialization = new AbortController();
const viewer = await MuMolViewer.create(
canvas,
{
representation: "cartoon",
colorScheme: "structure",
},
{ signal: initialization.signal },
);
viewer.setMolecule(molecule, {
fit: { padding: 0.15, rotationSafe: true },
view: { rotation: [-0.3, 0.5, 0], zoom: 1.2 },
});
// Later, when the surrounding UI unmounts:
viewer.dispose();MuMolViewer.create() is the recommended construction path. It resolves only
after the WebGPU renderer is ready, so adapter, device, and renderer failures
reject through a normal try/catch. If its signal is aborted during
initialization, it rejects with the signal's reason and disposes the partially
constructed viewer. Once create() resolves, call dispose() to end the
viewer's lifetime as usual.
The constructor remains available for applications that need to prepare the
viewer while the browser acquires its GPU device. In that form, await ready
explicitly and dispose after an initialization failure:
const viewer = new MuMolViewer(canvas, options);
try {
viewer.setMolecule(molecule);
await viewer.ready;
} catch (error) {
viewer.dispose();
throw error;
}Representations and colors
viewer.setRepresentation("ball-and-stick");
viewer.setColorScheme("element");
viewer.setSheetArrowheads(false);
viewer.setShaderStyle("toon");
viewer.setMaterialPreset("glass");
viewer.setMaterialSettings({ roughness: 0.12, transmission: 0.8 });
viewer.setAutoRotate(true);Representations are cartoon, backbone, wireframe, ball-and-stick, and
spacefill. Color schemes are structure, chain, element, b-factor,
rainbow, and plddt.
Camera, lighting, and performance
viewer.setCameraMode("perspective"); // or "orthographic" (default)
viewer.setBackground("#0f172a");
viewer.setEnvironmentIntensity(0.45); // room-environment IBL; 0 disables it
viewer.setMaxFrameRate(60); // Number.POSITIVE_INFINITY uncapsA hemisphere/key/fill light rig follows the camera, so shading stays consistent
from every orbit angle. Physical materials additionally pick up reflections from
a generated room environment scaled by environmentIntensity. While the user
orbits, zooms, or auto-rotation is active, rendering drops to
interactionPixelRatio (default 1.5) and is capped at maxFrameRate (default
60); a static view stops rendering entirely.
Effects and selection
All effects are assembled with Three.js's WebGPU RenderPipeline and TSL nodes:
viewer.setPostProcessingSettings({
depthOfField: 0.35,
bokehScale: 2,
bokehQuality: "high",
bloomStrength: 0.2,
outlineThickness: 0.002,
aoIntensity: 0.6,
asciiEnabled: true,
asciiCellSize: 14,
});
const stopFrameRateUpdates = viewer.onFrameRateChange((framesPerSecond) => {
console.log(
framesPerSecond === 0
? "Rendering is idle"
: `${Math.round(framesPerSecond)} FPS`,
);
});
viewer.setSelectionMode("residue");
viewer.setSelectionHighlightStyle("glow");
const selected = viewer.selectResidues([{ chain: "A", sequence: 42 }]);
if (selected.residues.length === 0) {
console.warn("Residue A:42 was not found");
}
viewer.zoomToSelection();
const selectionController = new AbortController();
viewer.onSelectionChange(
({ selection, previousSelection, source }) => {
console.log(source, previousSelection.atomIndices, selection.atomIndices);
},
{ signal: selectionController.signal },
);
// Later, during cleanup:
selectionController.abort();
stopFrameRateUpdates();Bokeh quality can be balanced (75% blur resolution), high (full resolution),
or ultra (full-resolution dual reconstruction). balanced is the default;
ultra offers the smoothest bokeh at the highest GPU cost.
Effects are only wired into the render pipeline while active, so disabled
effects cost nothing. outlineThickness draws a screen-space silhouette ink
from depth discontinuities, and aoIntensity enables ground-truth ambient
occlusion (GTAO) with a denoise pass; while ambient occlusion is active the
scene pass renders without multisampling, the usual trade for screen-space AO.
Selections can also be set with selectAtoms(), selectChains(), or the
general setSelection() query API. Clicking the canvas uses the active
selection mode. Selection callbacks and the mumolselectionchange canvas event
report the previous and resulting selections plus a pointer, api, or
molecule source. Selection methods return the resulting frozen selection, so
an empty match is directly detectable. setFog() controls background-colored
depth cueing across the structure, with a tighter fog range for close-up
selection views.
The initial fit, selection, and view can be applied atomically when replacing the molecule. Invalid nested options leave the previous molecule in place, and selection listeners are notified only once with the resulting selection:
viewer.setMolecule(molecule, {
fit: { padding: 0.15, rotationSafe: true },
selection: { residues: [{ chain: "A", sequence: 42 }] },
view: { zoom: 0.9 },
});padding is a non-negative fractional margin. rotationSafe uses an enclosing
sphere rather than the current projected bounds, trading a looser fit for one
that remains fully visible while orbiting.
Recording a turntable video
const video = await viewer.recordTurntable({
durationSeconds: 12,
fps: 60,
onProgress: (fraction) => {
console.log(`${String(Math.round(fraction * 100))}%`);
},
});
// `video` is a `video/mp4` Blob ready to download or upload.recordTurntable() renders one full rotation frame by frame and encodes it with
WebCodecs into an H.264 MP4. Because every frame is rendered at an exact angle
and encoded with an exact timestamp, the output has no dropped or duplicated
frames and loops seamlessly. Each frame renders and is captured inside one
animation-frame callback — the only phase where a WebGPU canvas is reliably
readable — so a recording takes frames / refresh rate of wall-clock time
(faster than real time on high-refresh displays) and the tab must stay visible
while it runs. Recording always uses the full pixel ratio (pass pixelRatio to
go higher, up to 4), and user input is suspended until the returned promise
settles. The mp4-muxer dependency is loaded on demand, so applications that
never record never ship it.
Parsing without a viewer
The structure model and parsers do not touch browser APIs, so they can be used in Node.js:
import { getMoleculeBounds, parseMmcif, parseXyz } from "mumol";
const water = parseXyz(`3
water
O 0 0 0
H 0.9572 0 0
H -0.239987 0.927297 0
`);
console.log(water.bonds, getMoleculeBounds(water));
const response = await fetch("https://example.test/structure.cif");
const cifMolecule = parseMmcif(await response.text());All parser results are immutable snapshots. PDB topology is read from CONECT records when present and inferred from covalent radii otherwise.
Development
npm run dev
npm run test:run
npm run typecheck
npm run build
npm run example:buildThe basic example is a responsive Preact and Tailwind CSS application served at the URL printed by Vite. It exercises structure loading, every representation and color mode, TSL materials and post-processing, selection, focus, and mobile controls. Test coverage focuses on chemistry, immutable model behavior, and structure parsing; the production build verifies the WebGPU/TSL module graph and declaration output.
