scene-bridge-3d
v1.0.5
Published
A Three.js Scene extension to sync 3D objects with DOM elements.
Maintainers
Readme
Scene Bridge 3D
Sync real DOM layout → live Three.js objects. Pixel mapping, scaling, anchoring, depth & rotation — all driven by the HTML/CSS you already write.
scene-bridge-3d is a lightweight extension of THREE.Scene that lets you attach any Object3D to a DOM element and keep it perfectly aligned in 3D space. It converts DOM position & size into world coordinates, supports multiple projection/target modes, anchors, responsive rescaling, optional depth & rotation via attributes, and even raycasting to arbitrary geometry.
🚀 Why Use This?
- Zero manual math for pixel → world conversions
- Drive 3D transforms directly from CSS layout or animated attributes
- Works with resizes, DPR changes (optional), and dynamic DOM movement
- Flexible projection targets: object depth or raycast hits
- Extend/opt-out per object with granular options
✨ Features
- 🔗 Attach any
THREE.Object3Dto a DOM element:scene.add(mesh, element[, options]) - 🎯 Anchor control:
center,top-left, or custom[ax, ay](0–1) - 🎥 Projection target modes:
objectZorraycast - 🧲 Raycast mode: project DOM point onto arbitrary meshes (e.g. terrain / model surface)
- 📐 Automatic scale mapping: element width/height → object scale (non‑uniform supported)
- 🧭 Optional depth & rotation via data attributes (
data-z,data-rot-x|y|z) - 🧬 Maintains geometry center offset (avoids visual drift)
- 🪄 Dynamic reassignment:
scene.setTracking(obj, newElement)or detach withnull - 🛡️
preserveZlets you keep depth while syncing only X/Y - 🖥️ Optional DPR-aware scaling (
applyDPR) - 🪶 Minimal overhead: only updates tracked objects
- 🧪 TypeScript friendly — full type exports
📦 Installation
Peer dependency: three@^0.160.0
npm install three scene-bridge-3d
# or
yarn add three scene-bridge-3d
# or
pnpm add three scene-bridge-3dESM / TypeScript:
import * as THREE from 'three';
import Scene from 'scene-bridge-3d';CommonJS:
const THREE = require('three');
const Scene = require('scene-bridge-3d').default;⚡ Quick Start
import * as THREE from 'three';
import Scene from 'scene-bridge-3d';
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
const scene = new Scene(camera);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geo, mat);
const el = document.querySelector('#card'); // some absolutely or relatively positioned element
scene.add(mesh, el, { anchor: 'center' });
function loop(t?: number) {
requestAnimationFrame(loop);
scene.update();
renderer.render(scene, camera);
}
loop();
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});HTML:
<div id="card" style="position:absolute;left:40vw;top:30vh;width:160px;height:200px;">
Hover me
</div>Animate with CSS or GSAP — the 3D mesh follows automatically.
🛠 Advanced Usage
Data Attribute Augmentation
<div id="panel" data-z="0.5" data-rot-x="30" data-rot-y="-20" data-rot-z="15"></div>data-z: depth offset (scaled to world units)data-rot-*: Euler rotations in degrees
Reassign / Detach Tracking
scene.setTracking(mesh, document.querySelector('#other'));
scene.setTracking(mesh, null); // stop syncingPreserve Existing Z
scene.add(mesh, el, { preserveZ: true }); // only x/y & scale sync
mesh.position.z = 2; // you control depth manuallyCustom Anchor
scene.add(mesh, el, { anchor: [0, 1] }); // left-bottom corner of elementTarget Modes
// (default) keep object's current Z plane
scene.add(mesh, el, { target: 'objectZ' });
// Raycast to arbitrary objects (first hit wins)
scene.add(mesh, el, { target: 'raycast', raycastObjects: [terrain, model] });Raycast Projection Example
scene.add(markerMesh, domPointEl, {
target: 'raycast',
raycastObjects: [terrainMesh],
anchor: 'center'
});The DOM element's screen position is projected as a ray; the first intersection point becomes the mesh position.
DPR-Aware Scaling
scene.add(mesh, el, { applyDPR: true });Useful when using high pixel density canvases.
🔍 API Reference
class Scene extends THREE.Scene
Constructor
new Scene(camera: THREE.PerspectiveCamera);Stores a reference to camera for projections.
Overloaded add
scene.add(object: THREE.Object3D, domElement: Element, options?: DomObjectOptions): Scene;
scene.add(...objects: THREE.Object3D[]): Scene; // native passthroughAssociates a DOM element with the object (first signature) or behaves like the normal Three.js Scene.add (second signature).
setTracking(object, el, options?)
scene.setTracking(obj, element); // start or swap
scene.setTracking(obj, null); // detach
scene.setTracking(obj, element, { anchor: 'top-left' });Adds tracking if not tracked; swapping element updates mapping. Passing null removes from internal sync list.
update(dt?)
Call each frame (typically before renderer.render). Optional dt is ignored internally today (reserved for future easing/tween helpers).
DomObjectOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| anchor | 'center' | 'top-left' | [number, number] | 'center' | Anchor point inside DOM element (0..1 for custom). |
| applyDPR | boolean | false | Multiply DOM pixel size by devicePixelRatio before world conversion. |
| target | 'objectZ' | 'raycast' | 'objectZ' | How to resolve world position along Z. |
| raycastObjects | THREE.Object3D[] | undefined | Required when target='raycast' — intersection candidates. |
| preserveZ | boolean | false | Keep object's existing Z (only X/Y updated). |
Non-uniform scaling is applied; Z scale is derived from Y to preserve proportions.
📏 How Mapping Works (Conceptual)
- DOM rect → normalized device coordinates (NDC)
- NDC unprojected through perspective to a 3D ray
- Intersect ray with: plane at objectZ, or meshes (raycast)
- Compute pixel→world scale using camera frustum height at object depth
- Apply scale & positional anchor offset
- Optionally apply depth & rotation attributes
This keeps layout fidelity even when resizing or moving elements.
🧠 Performance Notes
- O(N) per tracked element, lightweight math only
- Raycast mode adds intersection cost — keep
raycastObjectssmall or pre-grouped - Only reads attributes if present (
data-*checks are conditional) - Avoid excessive DOM reflow triggers; using transforms for movement is ideal
🗺 Roadmap / Ideas
- Optional texture auto-binding from
<img>/ CSS background - Built-in tween helpers using
dt - Support for perspective-correct billboarding mode
- Utility to batch-manage multiple element/object pairs declaratively
Have a feature request? Open an issue: https://bit.ly/scene-bridge-3d-issues
🤝 Contributing
- Fork & clone
- Install deps
- Build in watch mode:
npm install
npm run build:watch- Open a PR with a clear description & before/after visuals if UI-related.
Repo: https://bit.ly/scene-bridge-3d-git
📜 License
Released under the ISC License. See LICENSE.
🙌 Acknowledgements
- Three.js
- Inspiration from various DOM <-> 3D projection techniques in the community
👤 Author
![]()
Aayush Chouhan
GitHub: @aayushchouhan24
🧪 Minimal TypeScript Types Glimpse
import Scene, { DomObjectOptions } from 'scene-bridge-3d';🆘 FAQ
Do I have to use absolutely positioned elements?
No, any element with a stable layout box works; predictable coordinates help.
Why is my mesh distorted?
Check geometry bounds; zero-sized geometry axes cause large scale factors.
Can I animate with GSAP?
Yes — animate CSS left/top/width/height or data attributes; call scene.update() each frame.
Does it support OrthographicCamera?
Currently tuned for PerspectiveCamera. An orthographic variant could be added (PR welcome).
Enjoy building hybrid interfaces! If this helps you, a star ⭐ on GitHub means a lot.
