@domphy/three
@domphy/three is a declarative three.js scene graph for Domphy — a 1-1 functional port of @react-three/fiber's core (reconciler, raycast pointer events, demand frameloop), translated from React idioms to Domphy idioms.
A scene is a plain object tree, the same way a Domphy DOM tree is: no JSX, no virtual DOM, no build step required. Reactivity comes from @domphy/core's listener-based State, exactly like the rest of Domphy.
It never imports from three/addons or three/examples — no drei port, no helper wrappers. Anything outside the three core namespace (OrbitControls, post-processing passes, custom shaders, ...) enters user-land through extend().
Install
npm install @domphy/three threePeer dependencies: @domphy/core and three >= 0.156.0.
Quick start
Apply the three() patch to a div with an explicit height — it creates the canvas, the renderer, and the render loop, and mounts your scene tree into it. onFrame on a scene node is the useFrame() analog: it runs every rendered frame as (root, delta, self).
The host div needs an explicit width and height — the canvas fills its container, and a container with no size renders nothing.
Drive a prop from outside the scene by passing a State through a reactive (pierced) prop instead of mutating inside onFrame:
import { toState } from "@domphy/core"
import { three } from "@domphy/three"
const spin = toState(0)
setInterval(() => spin.set(spin.get() + 0.01), 16)
const App = {
div: null,
style: { width: "100%", height: "420px" },
$: [
three({
scene: [
{
mesh: [{ boxGeometry: null }, { meshStandardMaterial: { color: "orange" } }],
"rotation-y": (l) => spin.get(l), // pierced prop, reactive
},
{ ambientLight: null, intensity: 0.5 },
],
}),
],
}JSX → Domphy
If you know @react-three/fiber, the translation is mechanical:
JSX (@react-three/fiber) | Domphy (@domphy/three) |
|---|---|
| { mesh: [...], position: [1, 2, 3] } |
| { mesh: null, onClick: handleClick } |
useFrame((state, delta) => { ... }) | onFrame: (root, delta, self) => { ... } on a scene node |
| { primitive: [], object: existingObject } |
extend({ MyClass }); | extend({ MyClass }); { myClass: null } |
useLoader(GLTFLoader, url) | loadAsset(GLTFLoader, url) → { data, error, promise } |
The first key of a scene object is its tag (the camelCase of a THREE class, or a name registered via extend()) — the three.js equivalent of core's "first key = HTML tag". Every other key is a prop.
Next steps
- Scene Grammar — tags,
args,attach,primitive,dispose: null,_key, the function-prop rules, pierced props,extend() - Events — raycast pointer events (
onClick,onPointerOver, ...) - Animation & Loop —
onFrame,frameloopmodes,invalidate() - Loading Assets —
loadAsset/preloadAsset/clearAsset - Recipes — OrbitControls, loaders, and other common patterns
- Examples — full running scenes