Web Viewer Embedding

Orbitron viewers embed anywhere a browser runs. There are three ways to do it, from quickest to most self-contained:

  1. Hosted <iframe> — point an iframe at a small embed.html that loads a .orbpack bundle. This is exactly what the front page uses; each iframe gets its own isolated WASM instance, so many viewers coexist on one page.
  2. <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. It takes six attributes — scene, appearance, lighting, camera, controls, and interactive (the last two default to on, set either to "false" to turn it off) — and fires scene-loaded, scene-error, and render-error events. With controls on, a small overlay sits over the canvas with Reset, Smart Pose, Focus, Clear Sel., and / + zoom buttons. 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.
  3. Self-contained HTMLFile → Export Web View in the desktop app (or orbitron.export_html in Python) writes one .html with the scene, meshes, and WASM all inlined. Size follows the runtime and scene; the 2026-08-27 benzene smoke file was 8.08 MB.

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 Slice tab’s plane, 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. Render mode is the one thing not carried; the web viewer draws ball-and-stick only.

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 the three orbitron-viewer-wasm* files and the bundle itself (all served under viewer/ on this site) to your own server.

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 52 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 2.4 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.5.0.zip (for websites) - orbitron-viewer-standalone-v0.5.0.html (for presentations)

Build from source:

# ZIP bundle (requires trunk)
./scripts/build-wasm-viewer.sh

# Standalone HTML (requires trunk and base64)
./scripts/build-wasm-standalone.sh

# Output:
# dist/wasm-viewer/orbitron-viewer-web-v0.5.0.zip
# dist/wasm-viewer/orbitron-viewer-standalone-v0.5.0.html

See scripts/build-wasm-viewer.sh and scripts/build-wasm-standalone.sh for build requirements and options.

10.2 Embedding in Websites

Basic Setup

  1. Extract the ZIP bundle:

    unzip orbitron-viewer-web-v0.5.0.zip
    cd orbitron-viewer-web
  2. Add your molecule:

    • Export your structure to a bundle (see §10.6)
    • Place molecule.orbpack in the extracted directory
  3. Upload to your website:

    your-website/
    ├── orbitron/
    │   ├── index.html
    │   ├── orbitron-viewer-wasm_bg.wasm
    │   ├── orbitron-viewer-wasm.js
    │   └── molecule.orbpack
  4. Link 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.

10.3 PowerPoint Integration

Generate one scene-bearing HTML with File → 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:

  1. Link a shape or screenshot to the local .html file. The click opens the interactive view in the default browser and works without a network connection.
  2. Host the HTML and use LiveWeb where that add-in is allowed.
  3. 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

Embed the viewer in any slide:

<section>
  <h2>Benzene Structure</h2>
  <iframe 
    src="/orbitron-viewer-standalone.html?scene=benzene.orbpack"
    width="800" 
    height="600" 
    data-preload>
  </iframe>
  <p>Interactive 3D visualization with WebGPU rendering</p>
</section>

Preload multiple viewers:

<!-- Slide 1 -->
<section data-background-iframe="/orbitron/index.html?scene=benzene.orbpack">
  <h1 style="background: rgba(0,0,0,0.7); padding: 20px;">Benzene</h1>
</section>

<!-- Slide 2 -->
<section data-background-iframe="/orbitron/index.html?scene=ethanol.orbpack">
  <h1 style="background: rgba(0,0,0,0.7); padding: 20px;">Ethanol</h1>
</section>

Working example: See examples/web-embedding/reveal-js-slides/ for a complete Reveal.js deck.

Marp

Marp supports HTML iframes in Markdown slides:

---
marp: true
theme: default
---

# Benzene Structure

<iframe src="/orbitron-viewer-standalone.html?scene=benzene.orbpack" width="100%" height="500px"></iframe>

Interactive 3D molecular viewer powered by Orbitron + WebGPU

---

# Ethanol Structure

<iframe src="/orbitron-viewer-standalone.html?scene=ethanol.orbpack" width="100%" height="500px"></iframe>

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.

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,…"

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, and which layers were showing — cartoon, atoms, and the cell outline each independently. The render mode is the one thing skipped: this viewer draws ball-and-stick only.

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.orbpack

Add --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: See examples/converting-scenes/batch-convert.py.

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)
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)
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 Esc ?)

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.orbpack"></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 (§8 Viewer WASM) for architecture details and customization options.