Use.GPU has DOM-like canvas events, with native support for GPU-driven picking. This supports the full set of pointer behaviors, including pointer capture and motion. It follows the familiar DOM event API, but papers over browser/platform differences similar to React.
@use-gpu/interact
provides some prefab controls that leverage this, but also exposes hooks for custom constraints and dragging.
Additionally, there are declarative hooks as a light-way mechanism, e.g. to pass UI state into shaders:
When using the hooks, the consuming component should ensure it only responds when the relevant hook state changes, as a component may be re-rendered for other reasons.
Use the <Pick>
wrapper to make pickable objects. You must also enable picking
on the surrounding <Pass>
.
<Pick>
will generate a unique id
which you pass on to shapes and layers inside:
<Pick
onPointerDown={(e: PointerEvent, index: number) => {...}}
onPointerUp={(e: PointerEvent, index: number) => {...}}
>{
({ id, hovered }) => <>
{hovered ? <Cursor cursor="move" /> : null}
<PointLayer id={id} {...} />
</>
}</Pick>
All shapes and layers with the same id
will act as one pickable surface. This allows you to decouple interaction from rendering.
Pass a lookup
/lookups
attribute to most layers to distinguish different data points or sub-surfaces. This is returned as the event's picking index
.
While onPointerEnter
/onPointerLeave
only fires when entering/leaving the pickable surface, onPointerOver
/onPointerOut
fires whenever the index
changes.
Use <Pick all ...>
to create an event handler for the canvas background.
To project a given PointerEvent
into world space, use pick(event)
from useViewContext
. This will return an [origin, ray]
pair.
const {pick} = useViewContext();
const handlePointerDown = (e: PointerEvent) => {
const [origin, ray] = pick(e);
// ...
};
The useDrag
hook wraps this mechanism to implement arbitrary 3D dragging, with e.g. a lineConstraint
or planeConstraint
.
During a pointer drag, events should target the originally clicked element, even if the pointer momentarily exits.
Use.GPU automatically performs pointer capture on the HTML canvas to ensure this. <Pick>
will then capture on a per-id
level. Use usePointerCapture
to control capturing by hand.
Use usePointerLock
to lock the mouse to the canvas, as used e.g. by <FPSControls>
.
The interface for events is DOM-like, with e.g. e.preventDefault()
and e.stopPropagation()
.
Similar to React, the Event
is actually a synthetic event, which wraps the native DOM canvas event as e.nativeEvent
.
There are a few minor differences with the DOM, intended to provide more convenience:
x
/y
and u
/v
coordinates for the canvas are directly providedleft
, right
, etc. instead of numeric flagsmoveX
/moveY
/spinX
/spinY
moveX
/ moveY
in logical pixelsIf an event has multiple target handlers, they will be dispatched in reverse tree-order:
This follows the logic that the last rendered layers should go first.
<Pick>
is merely a convenience wrapper around the useCanvasEvents
hook. This in turn just quote-yeets the event handlers to the global event reconciler, tagged with their id
.
As such, it's possible to skip <Pick>
entirely and pass handlers to the event reconciler directly:
// In a component
const handlers = useCanvasEvents(id, {
pointerDown: (e) => {...},
});
return [handlers, ...];
This is how <OrbitControls>
and <PanControls>
are implemented, despite not having a pickable surface.