Interactions
Drag, click, and hover events on scene items
Overview
Uzay has a built-in interaction system that lets users drag, click, and hover over items in the scene. Some items (like points and vectors) come with default drag behavior, but you can also attach your own event handlers to any item.
Built-in Dragging
Points and vectors are draggable by default. When a user clicks and drags on a point, it moves in 3D space:
const point = scene.create("point3d", {
coords: vec3(1, 0, 0),
color: "red",
});
// That's it, the point is already draggableDrag Constraints
The draggable property controls which axes an item can be dragged along:
// Free movement in all directions (default)
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "xyz" });
// Only horizontal movement (xz-plane)
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "xz" });
// Only vertical movement (y-axis)
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "y" });
// Not draggable at all
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "none" });Available constraints: "x", "y", "z", "xy", "xz", "yz", "xyz", "custom", "none".
Dragging requires a writable atom. If you pass a derived (read-only) atom for the position, dragging is automatically disabled even if draggable is set. The exception is "custom" mode, which skips the default drag behavior entirely.
Custom Drag Behavior
Setting draggable to "custom" tells the engine that you'll handle all drag logic yourself. The engine will still show the grab cursor and fire drag events, but it won't compute a projected world position or update the item's position. You provide your own logic via .on("drag").
This is useful when the built-in axis and plane constraints don't fit your use case, for example snapping to a grid, constraining to a curved surface, or implementing entirely custom movement rules.
const point = scene.create("point3d", {
coords: vec3(0, 0, 0),
color: "gold",
draggable: "custom",
});
// Snap to integer grid positions on the xz-plane
point.on("drag", (event) => {
if (event.phase === "start") return;
const pos = event.worldPosition;
point.coords.set(vec3(
Math.round(pos.x),
0,
Math.round(pos.z),
));
});In "custom" mode, event.worldPosition and event.delta are not updated by the engine (since there's no projection to compute them from). Use event.ray if you need to project the pointer into 3D space yourself. The ray contains the camera position (origin) and the direction through the pointer (direction), and it updates every frame as the user drags.
Event Handlers
You can listen for drag, click, and hover events on any item using .on():
You can only have a single handler function attached to an event type. Adding a new handler to the same event type with .on() will override the previous handler.
Drag Events
const point = scene.create("point3d", {
coords: vec3(0, 0, 0),
color: "gold",
});
point.on("drag", (event) => {
console.log(event.phase); // "start", "move", or "end"
console.log(event.worldPosition); // Current 3D position of the pointer
console.log(event.delta); // Movement since last event
});Drag events have three phases:
"start": fired once when the user begins dragging"move": fired continuously as the user drags"end": fired once when the user releases
Click Events
const sphere = scene.create("sphere3d", {
center: vec3(0, 0, 0),
radius: 1,
color: "tomato",
});
sphere.on("click", (event) => {
console.log(event.worldPosition); // Where the click hit in 3D
console.log(event.screenPosition); // Pixel coordinates on the canvas
});Hover Events
const sphere = scene.create("sphere3d", {
center: vec3(0, 0, 0),
radius: 1,
color: "skyblue",
});
sphere.on("hover", (event) => {
if (event.phase === "enter") {
sphere.color.set("yellow");
} else if (event.phase === "leave") {
sphere.color.set("skyblue");
}
});Hover events have three phases:
"enter": pointer enters the item"move": pointer moves while still over the item"leave": pointer leaves the item
Removing Handlers
Use .off() to remove a handler:
point.on("click", (event) => { /* ... */ });
// Later
point.off("click");Event Properties
All events include these common fields:
| Field | Type | Description |
|---|---|---|
itemId | string | ID of the item that received the event |
itemKind | string | Type of the item (e.g. "point3d") |
worldPosition | Vec3 | 3D position where the event occurred |
screenPosition | Vec2 | Pixel coordinates on the canvas |
ray | { origin: Vec3, direction: Vec3 } | Camera ray through the pointer |
Drag events additionally include:
| Field | Type | Description |
|---|---|---|
phase | "start" | "move" | "end" | Current drag phase |
startWorldPosition | Vec3 | Where the drag began |
delta | Vec3 | Movement since last event |
Pointer Events
Every item has a pointerEvents property that controls whether it participates in the interaction system:
// This grid won't block clicks on items behind it
scene.create("grid3d", {
pointerEvents: "none",
});
// This sphere will receive clicks and hovers
scene.create("sphere3d", {
center: vec3(0, 0, 0),
pointerEvents: "auto", // This is the default for most items
});"auto": the item can be clicked, hovered, and dragged (default)"none": the item is invisible to the pointer and events pass through it
This is useful for items that you don't want intercepting mouse events meant for the items behind them.