The canvas element to render into
The parsed structure to draw
Size, spin period and rendering tunables
A handle for resizing, retiming and disposing the scene
const { createMoleculeScene } = await import('@/utils/molecule/scene');
const handle = createMoleculeScene(canvas, molecule, { size: 160, spinSeconds: 8 });
handle.setSpinSeconds(4);
handle.dispose();
export function createMoleculeScene(
canvas: HTMLCanvasElement,
molecule: Molecule,
options: MoleculeSceneOptions,
): MoleculeSceneHandle {
const atomScale = options.atomScale ?? DEFAULT_ATOM_SCALE;
const { positions, radius } = centreAtoms(molecule, atomScale);
const scene = new Scene();
addLighting(scene);
const group = new Group();
group.rotation.x = TILT_X;
scene.add(group);
const atomMesh = buildAtomMesh(molecule, positions, atomScale);
group.add(atomMesh);
const bondMesh = buildBondMesh(molecule, positions);
if (bondMesh) group.add(bondMesh);
const camera = new PerspectiveCamera(CAMERA_FOV, 1, 0.1, 1000);
camera.position.z = (radius * FIT_MARGIN) / Math.tan((CAMERA_FOV * Math.PI) / 360);
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio, 2));
renderer.setSize(options.size, options.size);
let spinSeconds = options.spinSeconds;
let elapsed = 0;
let lastFrame = performance.now();
let frameHandle = 0;
/**
* Advances the rotation for a frame and redraws.
* @param now - High-resolution timestamp supplied by `requestAnimationFrame`
* @source
*/
function renderFrame(now: number): void {
const delta = (now - lastFrame) / 1000;
lastFrame = now;
elapsed += delta;
const turns = (elapsed * 2 * Math.PI) / Math.max(spinSeconds, 0.1);
group.rotation.y = molecule.isPlanar ? Math.sin(turns) * PLANAR_SWING : turns;
renderer.render(scene, camera);
frameHandle = requestAnimationFrame(renderFrame);
}
if (options.reducedMotion) {
group.rotation.y = molecule.isPlanar ? 0 : PLANAR_SWING;
renderer.render(scene, camera);
} else {
frameHandle = requestAnimationFrame(renderFrame);
}
return {
setSpinSeconds(seconds: number): void {
// Rebase the clock so the molecule keeps its current angle instead of jumping.
const turns = (elapsed * 2 * Math.PI) / Math.max(spinSeconds, 0.1);
spinSeconds = seconds;
elapsed = (turns * Math.max(seconds, 0.1)) / (2 * Math.PI);
},
setSize(size: number): void {
renderer.setSize(size, size);
},
dispose(): void {
cancelAnimationFrame(frameHandle);
disposeMesh(atomMesh);
if (bondMesh) disposeMesh(bondMesh);
renderer.dispose();
renderer.forceContextLoss();
},
};
}
Builds a ball-and-stick scene for
moleculeoncanvasand starts animating it.A structure with real 3D coordinates rotates continuously about its vertical axis. A planar structure — every 2D record, and flat 3D ones like benzene — instead wobbles through a ±
PLANAR_SWINGyaw arc (~40°): spinning a flat molecule a full turn would take it edge-on twice per revolution, where it briefly vanishes.