CrystalViewer — binding points for a custom GUI
<cifview-widget> is one reference implementation of a GUI around CrystalViewer; you can build your own directly against the same public surface. The relevant parts of src/lib/widget.js to read as a worked example are connectedCallback (construct viewer, bind selection, load), attributeChangedCallback (toggle filters), and disconnectedCallback (dispose).
import { CrystalViewer } from 'cifvis';
const viewer = new CrystalViewer(container, {
renderMode: 'onDemand', // call viewer.requestRender() after external state changes
atomLabels: {
show: 'non-hydrogen',
placementMode: 'auto-omit', // adaptive; quality, performance, and maximum coverage are available
calloutPlacement: 'structure', // compact; 'viewport' permits wider spreading
},
});
const result = await viewer.loadCIF(cifText); // or viewer.loadCIF(cifText, 'blockName')
if (!result.success) {
console.error(result.error);
}The structure is painted before label layout begins. Collision placement uses a Web Worker when available, and stale label frames are cleared during rotation rather than ghosting over the new pose. For the full options schema (camera, selection, rendering, atoms, bonds, elements), see the Options Reference.
The GUI-facing surface
| Member | Use |
|---|---|
viewer.modifiers | Map of the six Filters. Set .mode, then re-render. |
viewer.cycleModifierMode(name) | Advances a filter to its next applicable mode and re-renders correctly — the simplest way to wire a toggle button. |
viewer.numberModifierModes(name) | Number of applicable modes for the current structure; use it to decide whether to show a toggle at all. |
viewer.updateStructure() | Re-runs the filter pipeline and re-renders, preserving the current rotation. Call after directly mutating a modifier's .mode. |
viewer.selections.onChange(callback) | Selection event hook. Callback receives [{type: 'atom'|'bond'|'hbond', data, color}]. |
viewer.selectAtoms(labels) | Programmatic selection by atom label. |
viewer.setAtomLabels(show) | Shows all, non-hydrogen, or selected atom labels. Selection entries may override text and priority. |
viewer.updateAtomLabelOptions(options) | Updates label appearance/layout without rebuilding the structure. |
viewer.clearAtomLabels() | Hides atom labels. |
viewer.getAtomLabelLayout() | Returns placed labels and labels omitted because no space was available. |
coupleViewerInteractions(...viewers) | Bidirectionally couples modes plus the complete molecular transform, pan, absolute zoom/framing, and camera reset. See Coupled viewers. |
viewer.loadDifferenceDensity(text, block, options) | Loads explicit FCF/custom coefficients or observed reflections plus IAM phases and progressively displays the map. |
viewer.loadCube(text, options) | Loads a cell-matched Gaussian Cube grid in the density worker and progressively displays its isosurface. |
viewer.addScalarField(field, options) | Adds an already calculated ScalarFieldGrid. |
viewer.loadScalarFieldSources(sources) | Loads an ordered mixture of difference-density, Cube, and direct scalar-field definitions. |
viewer.getScalarFields() | Returns renderer-independent metadata for the loaded field collection. |
viewer.setActiveScalarField(indexOrId) | Selects a field while restoring its own contour and appearance settings. |
viewer.cycleScalarField() | Advances through the loaded fields and one hidden state. |
viewer.onScalarFieldUpdate(callback) | Subscribes to source-independent started, update, complete, display, visibility, cleared, cancelled, and error events. |
viewer.updateIsosurfaceOptions(options) | Changes contours/appearance while retaining the scalar grid. A visibility-only update does not rebuild surfaces. |
viewer.updateContourLineOptions(options) | Enables/disables a line-only planar section or changes its plane, levels, and line appearance while retaining the scalar grid. |
viewer.setIsosurfaceVisibility(visible) | Shows or hides the existing surface meshes without source parsing, FFT, or marching-cubes work. |
viewer.clearScalarField(indexOrId) | Discards one field, defaulting to the active entry. |
viewer.clearScalarFields() | Discards the complete field collection. |
viewer.captureImage(options) | Renders the current view (scene + atom labels) to a standalone <canvas> at an arbitrary resolution. Options: scale (multiplier over the on-screen size, default 2), longEdge (target px for the longer edge, overrides scale), background ('transparent' or any CSS colour), includeLabels (default true). |
viewer.captureImageBlob(options) | As captureImage, but resolves to a PNG (or type) Blob ready to download. |
viewer.requestRender() | Forces a redraw when renderMode: 'onDemand'. |
viewer.getViewState() | Returns the live external-XYZ Cartesian rotation, projection-aware framing, and gesture-lock state. |
viewer.setViewState(state) | Applies an external-XYZ rotation and/or orthographic view size or perspective distance without rebuilding. |
viewer.setInteractionLocks({rotation, zoom}) | Enables/disables user-gesture rotation and zoom locks; explicit and coupled view updates still apply. |
viewer.controls.handleResize() | Call after any layout change that resizes the container. |
viewer.dispose() | Releases Three.js/GPU resources and event listeners. Required on teardown. |
Camera updates when filters change
Some filters need more than a re-render when their mode changes — growing symmetry or removing atoms can change the structure's extent, so the camera/orientation needs to reset too. Each filter exposes this via requiresCameraUpdate (currently true for symmetry and removeatoms, false for the rest). cycleModifierMode already checks this for you; if you're setting .mode directly instead, call viewer.loadStructure() (no argument needed — it defaults to the viewer's current base structure) for filters where requiresCameraUpdate is true, or viewer.updateStructure() otherwise:
viewer.modifiers.symmetry.mode = 'fragment';
await viewer.loadStructure(); // requiresCameraUpdate === true for SymmetryGrowerContainers that start hidden
A viewer measures its container to size the canvas. Tabs, accordions, modals, carousels and slide frameworks all create their panes hidden, so a viewer built into one starts at zero size.
CifVis watches the container with a ResizeObserver and resizes itself once the container is laid out, so this works without special handling:
// inside a tab that is not the active one
const viewer = new CrystalViewer(hiddenContainer);
await viewer.loadCIF(cifText);
// ...the canvas picks up its real size when the tab is shownTwo things are still worth doing when panes appear and disappear:
- Give the container a size in CSS, not just its content. A container whose height comes only from the canvas has nothing to measure.
- Create viewers lazily if there are many of them. Each holds a WebGL context, and browsers cap how many can exist at once — roughly 8 to 16. A deck or dashboard with a dozen structures should build each viewer when its pane is first shown and
dispose()those it no longer needs, rather than constructing them all up front.
// only once, when the pane is first revealed
if (!viewer && pane.clientHeight) {
viewer = new CrystalViewer(pane);
await viewer.loadCIF(cifText);
}dispose() releases the WebGL context, the resize observer and every listener.
Try it live
A minimal custom GUI: plain buttons wired directly to viewer.modifiers, no widget involved.
Click an atom or bond in the viewer above to see selection data here.