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/spinYmoveX / 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.