Web Viewer Embedding
Orbitron viewers embed anywhere a browser runs. There are three ways to do it, from quickest to most self-contained:
- Hosted
<iframe>— point an iframe at a smallembed.htmlthat loads a.orbpackbundle. This is exactly what the front page uses; each iframe gets its own isolated WASM instance, so many viewers coexist on one page. <orbitron-viewer>web component — drop the element on your own page with the shared WASM bundle. Best when you control the page and want one viewer. Ten attributes are observed for live changes —scene,appearance,lighting,camera,controls,interactive,shortcuts(the last three default to on, set any to"false"to turn it off), and the pointer-sensitivity trioorbit-sens,zoom-sens, andpan-sens. Three more are read at startup only:delta-url,delta-msanddelta-strict, which point the element at a delta-envelope feed and set its poll interval. The element firesscene-loaded,scene-error, andrender-errorevents. Withcontrolson, a small overlay sits over the canvas with Reset, Smart Pose, Focus, Clear Sel., and−/+zoom buttons. A scene carrying frames or vibrational modes adds a mode picker and a playback strip below them, and a Frames button that folds both away when you want the structure back; the button appears only when there is something to fold. Only one element may be active per page: the WASM runtime keeps a single global render state. The reference table lives in the element’s own doc comment,viewer/wasm/ts/src/orbitron-viewer.ts.- Self-contained HTML — File ▸ Export ▸ Export Web View… in the desktop app,
orbitron web <source> -o page.htmlon the command line (ororbitron.export_htmlin Python) writes one.htmlwith the scene, meshes, and WASM all inlined.orbitron webtakes an.orbpackas it is, or any file Orbitron reads, and accepts--title,--appearance FILE(an appearance bundle exported from the desktop) and--force. Size follows the runtime and scene; a page for benzene was 10.9 MB on 2026-10-21, nearly all of it the viewer.
An exported page carries what the desktop was showing, not just the atoms: cartoon ribbons, the isosurfaces on the Surfaces tab, the atom-colouring halo, the cell and periodic overlays, the plane of the Surfaces tab’s 2D slice, and the vibrational modes. The 2D slice becomes a coloured plane sitting where the cut actually is, rather than the desktop’s flat panel — a 3D canvas has nowhere to put a panel, and the colours come from the same ramps either way. Modes get a picker in the control overlay: choosing one generates its oscillation and plays it, and Stop hands the frames back to whatever trajectory the file had. A bundle also carries the render mode, described below.
For self-hosting, copy the four staged viewer assets described below. For a portable one-scene file, generate a self-contained HTML from the scene or its .orbpack rather than relying on a generic viewer page plus a second file.
Live example
This is a hosted <iframe> — the same mechanism as option 1:
<iframe src="/viewer/embed.html?scene=/viewer/c60.orbpack"
style="width:100%; height:360px; border:0;"></iframe>embed.html accepts scene, delta (a delta-envelope JSON of built meshes), appearance, lighting, zoom (log-scale; + out / − in, so 0.7 is about twice the distance and 0.16 a sixth farther), orbit=yaw,pitch (radians), pan=x,y with pan-mobile=x,y (the mobile pair is used below 900 px viewport width, the desktop pair above it), and controls=false to hide the control overlay. To self-host it, copy embed.html plus orbitron-viewer.js, orbitron-viewer-wasm.js and orbitron-viewer-wasm_bg.wasm, and the bundle itself (all served under viewer/ on this site) to your own server. embed.html imports /viewer/orbitron-viewer.js and points wasmJsUrl at /viewer/orbitron-viewer-wasm.js by absolute path, so serve the files at /viewer/ or edit those two lines.
A bundle is read as a container rather than as a bare structure, so its trajectory comes with it. Two further parameters drive that: frames=play animates the frames at fps (10 by default), and frames=<n> shows one frame and holds it. Playback advances on the render tick, so a viewer scrolled out of view stops rather than burning a frame budget offscreen.
mode=<n> animates a vibrational mode instead, generating one oscillation from the bundle’s frequency section. amplitude scales the displacement in Å.
The 69 steps of a Cu(II) geometry optimisation, played from a 337 KB bundle:
<iframe src="/viewer/embed.html?scene=/viewer/cu-opt.orbpack&frames=play&fps=12"
width="100%" height="360" style="border:0"></iframe>The antisymmetric O–H stretch of water at 4006 cm⁻¹, from a 4.7 KB bundle:
From your own page, the element exposes the same thing as methods: frameCount(), currentFrame(), setFrame(i), playFrames(fps), modeCount(), modeFrequencies(), animateMode(i) and showTrajectory() — enough to wire a slider, a play button or a mode picker in a few lines.
Both prebuilt versions below provide the same interactive viewer experience (rotation, zoom, selection, measurements) but differ in packaging and deployment workflow.
10.1 Download the Web Viewer
From GitHub Releases:
Visit the Orbitron releases page and download: - orbitron-viewer-web-v0.8.0.zip (for websites) - orbitron-viewer-standalone-v0.8.0.html (for presentations)
Build from source:
# Both (requires trunk, and esbuild from viewer/wasm/ts for the standalone page);
# --zip-only or --standalone-only builds one
./scripts/build-wasm-viewer.sh
# Output:
# dist/wasm-viewer/orbitron-viewer-web-v0.8.0.zip
# dist/wasm-viewer/orbitron-viewer-standalone-v0.8.0.htmlThe script reads the version from the workspace Cargo.toml, so the names follow a version bump on their own. Set VERSION in the environment to override it.
The standalone is index.html itself with the JavaScript bundled by esbuild and the WebAssembly inlined, so install its dependencies once with (cd viewer/wasm/ts && npm ci). See scripts/build-wasm-viewer.sh and scripts/build-wasm-standalone.sh for the rest of the requirements and options.
10.2 Embedding in Websites
Basic Setup
Extract the ZIP bundle:
unzip -d orbitron/ orbitron-viewer-web-v0.8.0.zip cd orbitronThe ZIP holds the viewer files at its root, with no wrapping directory, so unzipping it without
-dscatters them into the current directory.Add your molecule:
- Export your structure to a bundle (see §10.6)
- Place
molecule.orbpackin the extracted directory
Upload to your website:
your-website/ ├── orbitron/ │ ├── index.html │ ├── app.js │ ├── orbitron-viewer-wasm_bg.wasm │ ├── orbitron-viewer-wasm.js │ └── molecule.orbpackLink to the viewer:
<a href="/orbitron/index.html?scene=molecule.orbpack">View Benzene Structure</a>
Iframe Embedding
Embed the viewer directly in your page:
<iframe
src="/orbitron/index.html?scene=molecule.orbpack"
width="800"
height="600"
style="border: 1px solid #ccc; border-radius: 4px;">
</iframe>Responsive iframe:
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
<iframe
src="/orbitron/index.html?scene=molecule.orbpack"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;">
</iframe>
</div>Multiple Molecules
Host multiple structures by using different bundles:
<h2>Benzene</h2>
<iframe src="/orbitron/index.html?scene=benzene.orbpack" width="400" height="300"></iframe>
<h2>Ethanol</h2>
<iframe src="/orbitron/index.html?scene=ethanol.orbpack" width="400" height="300"></iframe>Advanced: Use JavaScript to switch scenes dynamically:
<select id="molecule-select">
<option value="benzene.orbpack">Benzene</option>
<option value="ethanol.orbpack">Ethanol</option>
<option value="water.orbpack">Water</option>
</select>
<iframe id="viewer" src="/orbitron/index.html?scene=benzene.orbpack" width="800" height="600"></iframe>
<script>
document.getElementById('molecule-select').addEventListener('change', (e) => {
const iframe = document.getElementById('viewer');
iframe.src = `/orbitron/index.html?scene=${e.target.value}`;
});
</script>Working example: See examples/web-embedding/simple-website/ for a complete multi-molecule demo.
The <orbitron-viewer> element
Every embed above points an iframe at the viewer’s own index.html. On a page you control, put the viewer in the page itself. Three files travel with it, and the shim fetches the .wasm from beside itself, so keep those two together:
your-website/
├── index.html
└── orbitron/
├── orbitron-viewer.js # the custom element
├── orbitron-viewer-wasm.js # wasm-bindgen shim
├── orbitron-viewer-wasm_bg.wasm
└── benzene.orbpack
The two -wasm files come out of trunk build --release. The element bundle, orbitron-viewer.js, is not in the release ZIP — that ZIP is the standalone viewer page — so take it from the Python wheel’s orbitron/viewer_assets/, from /viewer/orbitron-viewer.js on this site, or build it with npm run build in viewer/wasm/ts.
A complete page:
<!doctype html>
<meta charset="utf-8" />
<style>
orbitron-viewer { display: block; width: 100%; height: 420px; }
</style>
<orbitron-viewer
scene="/orbitron/benzene.orbpack"
appearance="presentation-dark"
lighting="Studio"
></orbitron-viewer>
<script type="module">
import "/orbitron/orbitron-viewer.js";
</script>Importing the module registers the element, so the markup upgrades as soon as it loads and nothing else has to run. Give the element a height — it has no intrinsic size, and a zero-height canvas draws nothing while raising no error. appearance takes any preset slug: signature, ink, glow, classic (or default), publication, licorice, spacefill, presentation, print, lineart, sketch, comic or grayscale, with aliases such as presentation-dark and presentation-light; lighting takes Studio, Balanced, Ambient, HighKey or Backlit.
The element looks for the shim at ./orbitron-viewer-wasm.js. Serving it from anywhere else means setting wasmJsUrl before the element connects, which in turn means creating the element in script rather than writing it in markup:
<div id="viewer-host"></div>
<script type="module">
import "/assets/js/orbitron-viewer.js";
const viewer = document.createElement("orbitron-viewer");
viewer.wasmJsUrl = "/assets/wasm/orbitron-viewer-wasm.js";
viewer.setAttribute("scene", "/data/benzene.orbpack");
viewer.addEventListener("scene-loaded", () => console.log("ready"));
viewer.addEventListener("scene-error", (e) => console.error(e.detail.error));
document.querySelector("#viewer-host").append(viewer);
</script>Get that path wrong and the failure is a 404 in the network panel with nothing on the page to explain it, so it is the first thing to check when the canvas stays blank. viewer/wasm/ts/README.md documents the rest of the element’s methods.
One element per page. The WASM runtime keeps a single global render state bound to the fixed DOM ids #orbitron-root and #orbitron-canvas, so a second element shares it and the scene loaded last appears in both canvases with the other slot blank. A gallery therefore gives each viewer its own iframe, which is what examples/web-embedding/simple-website/ does.
Serving the assets from Python
The wheel carries the viewer assets (index.html, orbitron-viewer.js, the wasm shim and the .wasm, plus a MANIFEST.json), so a Python install can host them without a checkout or a build. That is the quickest way to look at the element locally, and what a classroom demo can run on:
python -m orbitron.serve --port 8080
# Orbitron viewer: http://127.0.0.1:8080
# index.html: http://127.0.0.1:8080/index.html
# bundle: http://127.0.0.1:8080/orbitron-viewer.jsIt is a stdlib ThreadingHTTPServer rooted at the wheel’s viewer_assets/, with one addition: scenes can be pushed from Python and served over HTTP rather than staged on disk.
from orbitron import serve
server = serve.ensure_server() # lazy singleton, free port, daemon thread
url = server.register_scene("benzene.orbpack", open("benzene.orbpack", "rb").read())
print(server.base_url, url) # feed `url` to a `scene=` attributeScene._repr_html_() uses exactly this, which is why a scene displays inline in a notebook with no CDN and no network access. For a notebook viewer you drive from Python instead of one you look at once, orbitron.ViewerDomWidget is an ipywidget wrapping the same server in an iframe and pushing scene, appearance, lighting and overlay changes through traitlets:
widget = orbitron.ViewerDomWidget.from_scene(scene)
widget # renders inlineSync is one-way, Python to viewer. Pass base_url= to point it at a viewer host you run yourself.
10.3 PowerPoint Integration
Generate one scene-bearing HTML with File ▸ Export ▸ Export Web View… or orbitron.export_html. Keep that file beside the presentation or publish it to an HTTPS site.
PowerPoint does not have a portable built-in path that turns an arbitrary local HTML file into a live slide. Microsoft’s supported live-page route is the LiveWeb add-in, which loads a URL during the slide show. Use one of these handoffs:
- Link a shape or screenshot to the local
.htmlfile. The click opens the interactive view in the default browser and works without a network connection. - Host the HTML and use LiveWeb where that add-in is allowed.
- Export a PNG for a slide that must render identically on every machine, and attach the HTML as a linked interactive companion.
PowerPoint for the web and Google Slides should use the hosted-page or external link pattern. Do not document an Insert → Object → HTML File workflow as an inline browser; Office object insertion depends on a registered OLE handler and does not establish a general HTML rendering contract.
See examples/web-embedding/powerpoint-example/ for the tested HTML-generation, link, and PNG workflows.
10.4 HTML Presentations (Reveal.js, Marp)
Reveal.js
The tested deck gives each slide its own self-contained HTML file and points a plain <iframe> at it. Generate one page per molecule with export_html, put them beside slides.html, and the deck works offline with no server and no CDN at view time:
<section>
<h2>Benzene</h2>
<iframe src="benzene.html"
width="900" height="540"
allow="cross-origin-isolated"
style="border:0; border-radius:12px;"></iframe>
</section>Iframes rather than inline <orbitron-viewer> elements, because Reveal.js re-renders slides aggressively and the WASM runtime keeps a single global render state bound to fixed DOM IDs — two elements on one page fight over it.
<!-- background variant: the viewer fills the slide -->
<section data-background-iframe="benzene.html">
<h1 style="background: rgba(0,0,0,0.7); padding: 20px;">Benzene</h1>
</section>If you are hosting the viewer instead of exporting a page per molecule, the same iframes take the hosted form, src="/orbitron/index.html?scene=benzene.orbpack", at the cost of needing the server at view time.
Working example: examples/web-embedding/reveal-js-slides/ is a complete deck; examples/web-embedding/standalone/generate.py writes the per-slide pages it embeds.
Marp
Marp supports HTML iframes in Markdown slides:
---
marp: true
theme: default
---
# Benzene Structure
<iframe src="/orbitron-viewer-standalone-v0.8.0.html?scene=benzene.orbpack" width="100%" height="500px"></iframe>
Interactive 3D molecular viewer powered by Orbitron + WebGPU
---
# Ethanol Structure
<iframe src="/orbitron-viewer-standalone-v0.8.0.html?scene=ethanol.orbpack" width="100%" height="500px"></iframe>The standalone file is the release asset orbitron-viewer-standalone-v<version>.html; use the version you downloaded in the src.
Note: Marp requires the html option enabled in marp.config.js:
module.exports = {
html: true,
// ... other options
};10.5 Loading .orbpack bundles
A bundle is what the CLI and the Python API already write, and it carries provenance and a section list rather than being a bare blob of geometry. The viewer reads one directly.
Most pages never need this — <orbitron-viewer scene="…orbpack"> and the iframe’s ?scene= both take a bundle URL. Call the module directly when the bytes are already in hand, a file the user dropped on the page, say:
// The wasm module is a page-level singleton. The element has already
// initialised it, so importing the shim returns that same instance.
const wasm = await import("/orbitron/orbitron-viewer-wasm.js");
await wasm.default(); // no-op once the element has run it
const bytes = new Uint8Array(await file.arrayBuffer());
wasm.load_bundle(bytes);
// A bundle is deliberately partial, so ask what it actually carries.
console.log(wasm.bundle_sections(bytes)); // "structure,trajectory,frequency,…"Two namespaces, two spellings. The element’s own methods are camelCase — viewer.setFrame(i), viewer.applyDelta(json), viewer.exportPNG() — while the wasm module’s exports keep their Rust names in snake_case: set_frame, load_bundle, bundle_sections, camera_reset. The element is a thin wrapper over the module, so both drive the same render state.
No parser ships in the browser build — a bundle is already-parsed data, so the viewer only deserializes it. See Bundles for how to produce one.
Loading one also draws whatever volumetric grid it carries, as bundle-grid-N overlays at the default isovalue for the grid’s kind. Grids above 32 M points are skipped with a console warning rather than blocking the tab; nothing an orbital export produces comes close.
A bundle exported from the desktop also carries the view it was posed at, and loading it restores the camera, halos, cell overlays, theme, background, the render mode, and which layers were showing — cartoon, atoms, and the cell outline each independently.
setRenderMode(mode) changes it afterwards, taking the same spellings a bundle records: BallAndStick, Sticks (licorice), SpaceFill or AtomLabels. An unrecognised name is refused and logged rather than quietly drawing something else. Until 2026-09-05 the render mode was the one recorded field this viewer skipped, so a space-filling or licorice figure arrived as ball-and-stick.
showSlice(a, b, c) cuts a plane through three atoms (0-based indices) and colours it by the grid the bundle carries, using the desktop’s ramps — white at zero, red and blue for the two signs of an orbital, white to deep blue for a density. Where the desktop shows a flat panel, the browser draws the plane where it is, cutting through the molecule; clearSlice() removes it. It returns 0 when there is nothing to draw, which covers both a bundle with no grid and three atoms too close to collinear to define a plane.
Values off the edge of the grid read as zero, so a plane wider than the exported box fades to white rather than failing. A slice through a nodal plane is white for the same reason it should be — pyrazine’s π* LUMO is exactly zero in the ring plane.
A bundle saved while showing a cartoon draws one: showCartoon(true) traces the ribbon from the scene, and a bundle that recorded the intent does it on load. The mesh is never carried — a protein’s is megabytes and the instruction is one boolean — so this works from a file of a few kilobytes. The ribbon appears in the overlay list alongside any orbital surfaces, where it can be toggled.
A bundle with a frequency section can animate its normal modes: modeCount() and modeFrequencies() describe them, animateMode(i) generates one oscillation and plays it, and showTrajectory() puts the run’s own frames back. The displacement comes from the same orbitron-backbone call the desktop’s GIF export uses, so a mode looks the same in a browser as in a figure.
A bundle is also the only source the viewer can animate, since it carries the run’s frames: set_frame(i) steps, play_frames(fps) runs, and both pages accept frames/fps as URL parameters.
10.6 Creating scene bundles
The viewer loads .orbpack bundles. One is what the CLI and the Python API already write, and unlike a bare scene it carries the trajectory, the orbitals and the frequency data along with the structure — so the same file that shows a molecule can also animate its optimisation or draw a surface.
Using CLI
# A structure
orbitron canonical export molecule.xyz -o molecule.orbpack
# An optimisation, frames and all
orbitron canonical export gaussian.log -o run.orbpack
# One orbital, evaluated onto a grid at export
orbitron canonical export run.out --mo 48 -o lumo.orbpackAdd --appearance signature to record how it should look, and --show/--hide over cartoon, atoms and cell to record which layers are drawn. A viewer reads all of that from the file, so the embed URL needs nothing beyond scene. See Bundles for the full flag list.
Using Python
from orbitron import Orbitron
orb = Orbitron()
orb.export_bundle("molecule.xyz", "molecule.orbpack")Batch conversion:
from pathlib import Path
from orbitron import Orbitron
orb = Orbitron()
output_dir = Path("website/orbitron/")
for source in Path("structures/").glob("*.xyz"):
bundle = output_dir / f"{source.stem}.orbpack"
orb.export_bundle(str(source), str(bundle))
print(f"Exported {source.name} \u2192 {bundle.name}")To display one in a notebook rather than on a page, orbitron.view(bundle) hands the container to the inline viewer intact. Loading it with Orbitron.load gives you a Scene, which is structure alone — the grids, frames and recorded view do not survive that trip.
Working example: the loop above, or orbitron canonical export from the CLI; examples/hpc-bundle-preparation/ has batch and Slurm-array versions. (examples/converting-scenes/batch-convert.py writes .bin scene bytes, not bundles.)
Supported Input Formats
The viewer can display any format supported by Orbitron’s I/O pipeline: - XYZ, PDB, CIF (structures) - Gaussian LOG/FCHK, NWChem OUT, Molpro OUT (quantum chemistry) - VASP vasprun.xml (periodic systems) - SDF/MOL (connection tables)
10.7 Viewer URL Parameters
The viewer (index.html) accepts query parameters to customize behavior:
| Parameter | Example | Description |
|---|---|---|
scene |
?scene=molecule.orbpack |
Path to the scene to load (required). Bundles are told from bare scenes by their bytes, not their name |
frames |
?frames=play |
Bundles only: play animates the trajectory, <n> shows one frame |
fps |
?fps=12 |
Playback rate for frames=play or mode. Default 10 |
mode |
?mode=2 |
Bundles only: animate vibrational mode <n> instead of the trajectory |
amplitude |
?amplitude=0.4 |
Displacement scale for mode, in Å. Default 0.3 |
lighting |
?lighting=studio |
Apply a named lighting preset on load |
appearance |
?appearance=presentation |
Apply a named appearance preset on load |
overlay |
?overlay=ligand.orbpack |
URL of an additional scene to load as an overlay (refine with overlay_id, overlay_opacity, overlay_enabled) |
mesh |
?mesh=homo.json |
URL of a JSON mesh overlay to load (refine with mesh_id, mesh_opacity, mesh_enabled) |
perf |
?perf=1 |
Show the performance HUD (1 / true) |
perf_dump, perf_burst, perf_burst_interval |
?perf_burst=1 |
Frame-timing capture for profiling runs |
export |
?export=1 |
Auto-export a PNG once the scene loads (set the filename with export_name) |
delta |
?delta=delta.json |
URL polled for live scene-delta updates (refine with delta_strict, delta_ms) |
delta_reset |
?delta_reset=1 |
Clear accumulated delta state on load (only takes effect alongside delta) |
overlay_labels |
?overlay_labels=0 |
Turn the overlay label layer on (1) or off (0) |
overlay_label_offset |
?overlay_label_offset=1.5 |
Vertical offset applied to overlay labels |
overlay_label, mesh_label |
?overlay_label=overlay-1:1 |
Per-overlay or per-mesh label toggle, <id>:<0\|1>. Repeatable |
overlay_label_highlight |
?overlay_label_highlight=%23ff0 |
Highlight colour for overlay labels |
zoom_sens |
?zoom_sens=0.002 |
Log-scale zoom per wheel pixel. Default 0.001 (≈10% per notch); values below the 0.0001 floor are clamped |
orbit_sens |
?orbit_sens=0.005 |
Orbit radians per pointer pixel. Default 0.01, floor 0.001 |
pan_sens |
?pan_sens=0.004 |
Pan units per pointer pixel. Default 0.002, floor 0.0001 |
shortcuts |
?shortcuts=0 |
Disable the page’s keyboard shortcuts (0; t r 0 p f l Esc ?) |
scale_bar |
?scale_bar=1 |
Show a scale bar, remeasured from the camera every frame so it stays true as the reader zooms |
?scale_bar=1 computes the bar from this page’s camera each frame. It is deliberately not carried in an exported scene: a bar baked in would state a distance for whatever camera produced the export, and the reader’s camera moves independently, so it would begin lying on the first zoom.
It uses the same arithmetic as orbitron render --scale-bar and the desktop — a round number of Ångström, 1, 2 or 5 times a power of ten, filling at most a fifth of the canvas — so a bar in a headless figure and one in the browser round the same way and word the label the same way.
Under a perspective projection no single bar is true across the whole frame; this measures at the point the camera looks at, as every other surface does.
Examples:
<!-- Load with a named appearance preset -->
<iframe src="index.html?scene=benzene.orbpack&appearance=presentation"></iframe>
<!-- Load with a lighting preset and the performance HUD -->
<iframe src="index.html?scene=ethanol.orbpack&lighting=studio&perf=1"></iframe>
<!-- Base scene plus an orbital mesh overlay -->
<iframe src="index.html?scene=protein.orbpack&mesh=homo.json"></iframe>Multiple parameters:
<iframe src="index.html?scene=molecule.orbpack&appearance=presentation&lighting=studio&perf=1"></iframe>Note: URL parameters are case-sensitive. Use scene=file.orbpack, not Scene=file.orbpack.
10.8 Browser Compatibility
The viewer runs in every current browser — no configuration needed. At startup it feature-detects WebGPU (via navigator.gpu.requestAdapter()); when a usable WebGPU adapter is present it renders on WebGPU for best performance, and otherwise it automatically falls back to WebGL2, which every current browser supports. You don’t pick a backend — the viewer does, per-browser.
| Browser | Baseline (WebGL2) | WebGPU fast-path |
|---|---|---|
| Chrome / Edge | ✅ Supported | ✅ 113+ |
| Firefox | ✅ Supported | ✅ 141+ (earlier: enable dom.webgpu.enabled) |
| Safari | ✅ Supported | ✅ 26+ / macOS Tahoe (earlier renders on WebGL2) |
| Mobile Chrome / Safari | ✅ Supported | ✅ on recent devices |
The only hard requirement is a working WebGL2 context (hardware acceleration enabled). If no compatible adapter is available at all, the viewer surfaces an error on the canvas rather than rendering blank.
The browser build accepts at most 100,000 atom instances and 200,000 bond instances in one composed scene. The same cap applies on WebGPU and WebGL2. Above it, the component fires scene-error during initial load, or exposes a visible data-render-error status if overlays or live deltas push an already-loaded view past the cap. Its message recommends opening the complete scene in the desktop viewer or exporting a smaller selection for the browser. The selected backend is available on the canvas as data-render-backend="webgpu" or "webgl2" for diagnostics.
This is a safety ceiling, not a promise that every scene below it will be interactive. Backend choice matters much more than JavaScript time. On a physical 32 GiB M4 MacBook Air at 1280x720, Chromium hardware WebGPU measured a 6.94 ms median frame interval for 100,000 atoms and 6.93 ms for 100,000 atoms plus 200,000 bonds. First paint was 155 ms and 273 ms respectively. The complete browser process groups peaked at about 1.31 GB and 1.62 GB RSS.
The same machine’s SwiftShader software-WebGL2 path measured 956 ms per frame for 100,000 atoms and 2.54 seconds per frame at the bond cap, about 1.05 FPS and 0.39 FPS. A 2,500-atom software case measured 32.8 ms per frame. These results are why the WebGPU and WebGL2 builds still share the conservative ceiling. A higher WebGPU-only cap needs reliable backend-specific policy rather than a blanket increase.
Do not gate the viewer on navigator.gpu. Older code sometimes hid the viewer and showed a static image when WebGPU was absent — that hides a viewer that works fine on WebGL2. If you want a fallback image for the rare no-WebGL2 case, key it on the canvas’s data-render-error attribute (set by the viewer when initialization fails), not on WebGPU support.
Performance tips: - Scene size: Use a smaller selection for browser interaction when a complete system approaches the browser cap; use the desktop for the full system - File size: Keep bundles under 5 MB for fast loading; --mo exports one orbital instead of a whole cube, which is usually what makes the difference - Hosting: Use CDN or static hosting (GitHub Pages, Netlify, Vercel) for best delivery - Caching: Set appropriate Cache-Control headers for .wasm and .orbpack files - Compression: Enable gzip/brotli compression on your web server for .wasm files
Security notes: - The viewer runs in the browser sandbox (no file system access) - Bundles are loaded via fetch() and must respect CORS policies - For cross-origin scenes, ensure your server sends Access-Control-Allow-Origin headers
Working examples: See examples/web-embedding/ for complete integration demos covering all deployment scenarios.
Need help? File an issue on GitHub or consult the Developer Guide’s Extensions page for architecture details and customization options.