Just track it
import { ThreeDOM } from "portalgl/three"
// renderer is initialized; container and element are DOM nodes.
const views = new ThreeDOM({ renderer, container })
const view = views.addView(element, { scene, camera })views is a ThreeDOM instance. It connects your Three.js renderer to the page. addView attaches a scene and camera to an HTML element.
React example
Start with an initialized Three.js WebGPURenderer using alpha: true, your scene, and a perspective camera. PortalGL manages DOM measurements. This component creates ThreeDOM, registers the element, and runs the render loop.
"use client"
import { useEffect, useRef } from "react"
import { ThreeDOM } from "portalgl/three"
import type { WebGPURenderer, Scene, PerspectiveCamera } from "three/webgpu"
type Props = {
renderer: WebGPURenderer // Already initialized with await renderer.init()
scene: Scene
camera: PerspectiveCamera
}
export default function TrackedView({ renderer, scene, camera }: Props) {
const container = useRef<HTMLDivElement>(null)
const element = useRef<HTMLDivElement>(null)
useEffect(() => {
const views = new ThreeDOM({
renderer, container: container.current!,
})
views.addView(element.current!, { scene, camera })
renderer.setAnimationLoop(time => {
views.advance(time)
views.render()
})
return () => {
renderer.setAnimationLoop(null)
views.destroy()
}
}, [renderer, scene, camera])
return (
<div ref={container} className="relative">
<div ref={element} className="h-64 w-full" />
</div>
)
}Use the outer div as the full-page wrapper, starting at the document’s top-left. Keep the renderer, scene, and camera stable between renders. This component owns the renderer’s animation loop while mounted; its cleanup stops that loop and releases tracking. The caller still owns the renderer and scene resources.
Manual JavaScript: scene setup
To use your own Three.js scene directly, run this in the browser after the elements mount. Create your scene, camera, and a WebGPURenderer with alpha: true first.
<div id="demo" style="position: relative">
<div id="cube" style="width: 75%; height: 256px; margin: auto"></div>
</div>import { ThreeDOM } from "portalgl/three"
await renderer.init()
// A positioned wrapper containing your page and tracked elements.
const container = document.querySelector<HTMLElement>("#demo")!
const element = document.querySelector<HTMLElement>("#cube")!
const views = new ThreeDOM({ renderer, container })
const view = views.addView(element, { scene, camera })
await views.prepare()
renderer.setAnimationLoop(time => {
views.advance(time)
views.render()
})Manual JavaScript: bounds and cleanup
The React effect above stops the loop and releases ThreeDOM and its measurements. With manual setup, perform the same cleanup below, then dispose the Three.js resources you own.
// Inside your loop, after views.advance(time):
const rect = view.tracked.rect // { top, left, width, height, ... } or null
// On teardown:
renderer.setAnimationLoop(null)
views.destroy()
// Dispose your geometries, materials, textures, and renderer too.