threejs-interaction — quality + safety report
In the Skillier index (antigravity__threejs-interaction) · scanned 2026-06-03 · engine: builtin+triage
✓ Clean — no heuristic safety flags surfaced.
Heuristic flags from the builtin scanner, which is known to over-flag (it trips on legitimate env-reading integrations, security skills, and library .eval calls). This is NOT an authoritative malicious verdict — re-scan with SkillSpector for the authoritative result. Run the authoritative scan →
📇 This skill is in the Skillier index (curated · deduped · quality-filtered). Install Skillier to route & load it into your AI client.
Quality notes
About this skill
Three.js interaction - raycasting, controls, mouse/touch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.
📄 Read the SKILL.md
---
name: threejs-interaction
description: Three.js interaction - raycasting, controls, mouse/touch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.
risk: unknown
source: community
---
# Three.js Interaction
## When to Use
- You need user interaction inside a Three.js scene.
- The task involves raycasting, object picking, pointer handling, touch input, or camera controls.
- You are building an interactive 3D experience rather than a passive render.
## Quick Start
```javascript
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// Camera controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Raycasting for click detection
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
console.log("Clicked:", intersects[0].object);
}
}
window.addEventListener("click", onClick);
```
## Raycaster
### Basic Raycasting
```javascript
const raycaster = new THREE.Raycaster();
// From camera (mouse picking)
raycaster.setFromCamera(mousePosition, camera);
// From any origin and direction
raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3
// Get intersections
const intersects = raycaster.intersectObjects(objects, recursive);
// intersects array contains:
// {
// distance: number, // Distance from ray origin
// point: Vector3, // Intersection point in world coords
// face: Face3, // Intersected face
// faceIndex: number, // Face index
// object: Object3D, // Intersected object
// uv: Vector2, // UV coordinates at intersection
// uv1: Vector2, // Second UV channel
// normal: Vector3, // Interpolated face normal
// instanceId: number // For InstancedMesh
// }
```
### Mouse Position Conversion
```javascript
const mouse = new THREE.Vector2();
function updateMouse(event) {
// For full window
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
// For specific canvas element
function updateMouseCanvas(event, canvas) {
const rect = canvas.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
```
### Touch Support
```javascript
function onTouchStart(event) {
event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(clickableObjects);
if (intersects.length > 0) {
handleSelection(intersects[0]);
}
}
}
renderer.domElement.addEventListener("touchstart", onTouchStart);
```
### Raycaster Options
```javascript
const raycaster = new THREE.Raycaster();
// Near/far clipping (default: 0, Infinity)
raycaster.near = 0;
raycaster.far = 100;
// Line/Points precision
raycaster.params.Line.threshold = 0.1;
raycaster.params.Points.threshold = 0.1;
// Layers (only intersect objects on specific layers)
raycaster.layers.set(1);
```
### Efficient Raycasting
```javascript
// Only check specific objects
const clickables = [mesh1, mesh2, mesh3];
const intersects = raycaster.intersectObjects(clickables, false);
// Use layers for filtering
mesh1.layers.set(1); // Clickable layer
raycaster.layers.set(1);
// Throttle raycast for hover effects
let lastRaycast = 0;
function onMouseMove(event) {
const now = Date.now();
if (now - lastRaycast < 50) return; // 20fps max
lastRaycast = now;
// Raycast here
}
```
## Camera Controls
### OrbitControls
```javascript
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement);
// Damping (smooth movement)
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Rotation limits
controls.minPolarAngle = 0; // Top
controls.maxPolarAngle = Math.PI / 2; // Horizon
controls.minAzimuthAngle = -Math.PI / 4; // Left
controls.maxAzimuthAngle = Math.PI / 4; // Right
// Zoom limits
controls.minDistance = 2;
controls.maxDistance = 50;
// Enable/disable features
controls.enableRotate = true;
controls.enableZoom = true;
controls.enablePan = true;
// Auto-rotate
controls.autoRotate = true;
controls.autoRotateSpeed = 2.0;
// Target (orbit point)
controls.target.set(0, 1, 0);
// Update in animation loop
function animate() {
controls.update(); // Required for damping and auto-rotate
renderer.render(scene, camera);
}
```
#### OrbitControls Programmatic Methods (r183)
```javascript
// Programmatic camera movement
controls.dolly(1.5); // Dolly in/out (zoom for perspective cameras)
controls.pan(deltaX, deltaY); // Pan the camera
controls.rotate(deltaAzimuth, deltaPolar); // Rotate around target
// Cursor style (r183)
controls.cursorStyle = { orbit: "grab", pan: "move", dolly: "zoom-in" };
```
### FlyControls
```javascript
import { FlyControls } from "three/addons/controls/FlyControls.js";
const controls = new FlyControls(camera, renderer.domElement);
controls.movementSpeed = 10;
controls.rollSpeed = Math.PI / 24;
controls.dragToLook = true;
// Update with delta
function animate() {
controls.update(clock.getDelta());
renderer.render(scene, camera);
}
```
### FirstPersonControls
```javascript
import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.js";
const controls = new FirstPersonControls(camera, renderer.domElement);
controls.movementSpeed = 10;
controls.lookSpeed = 0.1;
controls.lookVertical = true;
controls.constrainVertical = true;
controls.verticalMin = Math.PI / 4;
controls.verticalMax = (Math.PI * 3) / 4;
function animate() {
controls.update(clock.getDelta());
}
```
### PointerLockControls
```javascript
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
const controls = new PointerLockControls(camera, document.body);
// Lock pointer on click
document.addEventListener("click", () => {
controls.lock();
});
controls.addEventListener("lock", () => {
console.log("Pointer locked");
});
controls.addEventListener("unlock", () => {
console.log("Pointer unlocked");
});
// Movement
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
const moveForward = false;
const moveBackward = false;
document.addEventListener("keydown", (event) => {
switch (event.code) {
case "KeyW":
moveForward = true;
break;
case "KeyS":
moveBackward = true;
break;
}
});
function animate() {
if (controls.isLocked) {
direction.z = Number(moveForward) - Number(moveBackward);
direction.normalize();
velocity.z -= direction.z * 0.1;
velocity.z *= 0.9; // Friction
controls.moveForward(-velocity.z);
}
}
```
### TrackballControls
```javascript
import { TrackballControls } from "three/addons/controls/TrackballControls.js";
const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 2.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.staticMoving = true;
function animate() {
controls.update();
}
```
### MapControls
```javascript
import { MapControls } from "three/addons/controls/MapControls.js";
const controls = new MapControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.maxPolarAngle = Math.PI / 2;
```
## TransformControls
Gizmo for moving/rotating/scaling objects.
```javascript
import { TransformControls } from "three/addons/controls/TransformControls.js";
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
// Attach to object
transformControls.attach(selectedMesh);
// Switch modes
transformControls.setMode("translate"); // 'translate', 'rotate', 'scale'
// Change space
transformControls.setSpace("local"); // 'local', 'world'
// Size
transformControls.setSize(1);
// Events
transformControls.addEventListener("dragging-changed", (event) => {
// Disable orbit controls while dragging
orbitControls.enabled = !event.value;
});
transformControls.addEventListener("change", () => {
renderer.render(scene, camera);
});
// Keyboard shortcuts
window.addEventListener("keydown", (event) => {
switch (event.key) {
case "g":
transformControls.setMode("translate");
break;
case "r":
transformControls.setMode("rotate");
break;
case "s":
transformControls.setMode("scale");
break;
case "Escape":
transformControls.detach();
break;
}
});
```
## DragControls
Drag objects directly.
```javascript
import { DragControls } from "three/addons/controls/DragControls.js";
const draggableObjects = [mesh1, mesh2, mesh3];
const dragControls = new DragControls(
draggableObjects,
camera,
renderer.domElement,
);
dragControls.addEventListener("dragstart", (event) => {
orbitControls.enabled = false;
event.object.material.emissive.set(0xaaaaaa);
});
dragControls.addEventListener("drag", (event) => {
// Constrain to ground plane
event.object.position.y = 0;
});
dragControls.addEventListener("dragend", (event) => {
orbitControls.enabled = true;
event.object.material.emissive.set(0x000000);
});
```
## Selection System
### Click to Select
```javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObject = null;
function onMouseDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(selectableObjects);
// Deselect previous
if (selectedObject) {
selectedObject.material.emissive.set(0x000000);
}
// Select new
if (intersects.length > 0) {
selectedObject = intersects[0].object;
selectedObject.material.emissive.set(0x444444);
} else {
selectedObject = null;
}
}
```
### Box Selection
```javascript
import { SelectionBox } from "three/addons/interactive/SelectionBox.js";
import { SelectionHelper } from "three/addons/interactive/SelectionHelper.js";
const selectionBox = new SelectionBox(camera, scene);
const selectionHelper = new SelectionHelper(renderer, "selectBox"); // CSS class
document.addEventListener("pointerdown", (event) => {
selectionBox.startPoint.set(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0.5,
);
});
document.addEventListener("pointermove", (event) => {
if (selectionHelper.isDown) {
selectionBox.endPoint.set(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0.5,
);
}
});
document.addEventListener("pointerup", (event) => {
selectionBox.endPoint.set(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0.5,
);
const selected = selectionBox.select();
console.log("Selected objects:", selected);
});
```
### Hover Effects
```javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredObject = null;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.int
… (truncated)Want a live grade + an embeddable README badge? Run your skill through the free scanner.
Graded independently by Skillproof — nothing to sell the author. Quality is mechanical + corpus-grounded; safety flags are heuristic (builtin+triage), not a malicious verdict.