Install
Terminal · npx$
npx skills add https://github.com/cloudai-x/threejs-skills --skill threejs-interactionWorks with Paperclip
How Threejs Interaction fits into a Paperclip company.
Threejs Interaction drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
S
SaaS FactoryPaired
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
$27$59
Explore packSource file
SKILL.md660 linesExpandCollapse
---name: threejs-interactiondescription: 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.--- # Three.js Interaction ## Quick Start ```javascriptimport * as THREE from "three";import { OrbitControls } from "three/addons/controls/OrbitControls.js"; // Camera controlsconst controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true; // Raycasting for click detectionconst 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 ```javascriptconst raycaster = new THREE.Raycaster(); // From camera (mouse picking)raycaster.setFromCamera(mousePosition, camera); // From any origin and directionraycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3 // Get intersectionsconst 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 ```javascriptconst 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 elementfunction 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 ```javascriptfunction 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 ```javascriptconst raycaster = new THREE.Raycaster(); // Near/far clipping (default: 0, Infinity)raycaster.near = 0;raycaster.far = 100; // Line/Points precisionraycaster.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 objectsconst clickables = [mesh1, mesh2, mesh3];const intersects = raycaster.intersectObjects(clickables, false); // Use layers for filteringmesh1.layers.set(1); // Clickable layerraycaster.layers.set(1); // Throttle raycast for hover effectslet lastRaycast = 0;function onMouseMove(event) { const now = Date.now(); if (now - lastRaycast < 50) return; // 20fps max lastRaycast = now; // Raycast here}``` ## Camera Controls ### OrbitControls ```javascriptimport { 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 limitscontrols.minPolarAngle = 0; // Topcontrols.maxPolarAngle = Math.PI / 2; // Horizoncontrols.minAzimuthAngle = -Math.PI / 4; // Leftcontrols.maxAzimuthAngle = Math.PI / 4; // Right // Zoom limitscontrols.minDistance = 2;controls.maxDistance = 50; // Enable/disable featurescontrols.enableRotate = true;controls.enableZoom = true;controls.enablePan = true; // Auto-rotatecontrols.autoRotate = true;controls.autoRotateSpeed = 2.0; // Target (orbit point)controls.target.set(0, 1, 0); // Update in animation loopfunction animate() { controls.update(); // Required for damping and auto-rotate renderer.render(scene, camera);}``` ### FlyControls ```javascriptimport { 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 deltafunction animate() { controls.update(clock.getDelta()); renderer.render(scene, camera);}``` ### FirstPersonControls ```javascriptimport { 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 ```javascriptimport { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; const controls = new PointerLockControls(camera, document.body); // Lock pointer on clickdocument.addEventListener("click", () => { controls.lock();}); controls.addEventListener("lock", () => { console.log("Pointer locked");}); controls.addEventListener("unlock", () => { console.log("Pointer unlocked");}); // Movementconst 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 ```javascriptimport { 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 ```javascriptimport { 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. ```javascriptimport { TransformControls } from "three/addons/controls/TransformControls.js"; const transformControls = new TransformControls(camera, renderer.domElement);scene.add(transformControls); // Attach to objecttransformControls.attach(selectedMesh); // Switch modestransformControls.setMode("translate"); // 'translate', 'rotate', 'scale' // Change spacetransformControls.setSpace("local"); // 'local', 'world' // SizetransformControls.setSize(1); // EventstransformControls.addEventListener("dragging-changed", (event) => { // Disable orbit controls while dragging orbitControls.enabled = !event.value;}); transformControls.addEventListener("change", () => { renderer.render(scene, camera);}); // Keyboard shortcutswindow.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. ```javascriptimport { 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 ```javascriptconst 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 ```javascriptimport { 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 ```javascriptconst 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.intersectObjects(hoverableObjects); // Reset previous hover if (hoveredObject) { hoveredObject.material.color.set(hoveredObject.userData.originalColor); document.body.style.cursor = "default"; } // Apply new hover if (intersects.length > 0) { hoveredObject = intersects[0].object; if (!hoveredObject.userData.originalColor) { hoveredObject.userData.originalColor = hoveredObject.material.color.getHex(); } hoveredObject.material.color.set(0xff6600); document.body.style.cursor = "pointer"; } else { hoveredObject = null; }} window.addEventListener("mousemove", onMouseMove);``` ## Keyboard Input ```javascriptconst keys = {}; document.addEventListener("keydown", (event) => { keys[event.code] = true;}); document.addEventListener("keyup", (event) => { keys[event.code] = false;}); function update() { const speed = 0.1; if (keys["KeyW"]) player.position.z -= speed; if (keys["KeyS"]) player.position.z += speed; if (keys["KeyA"]) player.position.x -= speed; if (keys["KeyD"]) player.position.x += speed; if (keys["Space"]) player.position.y += speed; if (keys["ShiftLeft"]) player.position.y -= speed;}``` ## World-Screen Coordinate Conversion ### World to Screen ```javascriptfunction worldToScreen(position, camera) { const vector = position.clone(); vector.project(camera); return { x: ((vector.x + 1) / 2) * window.innerWidth, y: (-(vector.y - 1) / 2) * window.innerHeight, };} // Position HTML element over 3D objectconst screenPos = worldToScreen(mesh.position, camera);element.style.left = screenPos.x + "px";element.style.top = screenPos.y + "px";``` ### Screen to World ```javascriptfunction screenToWorld(screenX, screenY, camera, targetZ = 0) { const vector = new THREE.Vector3( (screenX / window.innerWidth) * 2 - 1, -(screenY / window.innerHeight) * 2 + 1, 0.5, ); vector.unproject(camera); const dir = vector.sub(camera.position).normalize(); const distance = (targetZ - camera.position.z) / dir.z; return camera.position.clone().add(dir.multiplyScalar(distance));}``` ### Ray-Plane Intersection ```javascriptfunction getRayPlaneIntersection(mouse, camera, plane) { const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersection = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, intersection); return intersection;} // Ground planeconst groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);const worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);``` ## Event Handling Best Practices ```javascriptclass InteractionManager { constructor(camera, renderer, scene) { this.camera = camera; this.renderer = renderer; this.scene = scene; this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.clickables = []; this.bindEvents(); } bindEvents() { const canvas = this.renderer.domElement; canvas.addEventListener("click", (e) => this.onClick(e)); canvas.addEventListener("mousemove", (e) => this.onMouseMove(e)); canvas.addEventListener("touchstart", (e) => this.onTouchStart(e)); } updateMouse(event) { const rect = this.renderer.domElement.getBoundingClientRect(); this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; } getIntersects() { this.raycaster.setFromCamera(this.mouse, this.camera); return this.raycaster.intersectObjects(this.clickables, true); } onClick(event) { this.updateMouse(event); const intersects = this.getIntersects(); if (intersects.length > 0) { const object = intersects[0].object; if (object.userData.onClick) { object.userData.onClick(intersects[0]); } } } addClickable(object, callback) { this.clickables.push(object); object.userData.onClick = callback; } dispose() { // Remove event listeners }} // Usageconst interaction = new InteractionManager(camera, renderer, scene);interaction.addClickable(mesh, (intersect) => { console.log("Clicked at:", intersect.point);});``` ## Performance Tips 1. **Limit raycasts**: Throttle mousemove handlers2. **Use layers**: Filter raycast targets3. **Simple collision meshes**: Use invisible simpler geometry for raycasting4. **Disable controls when not needed**: `controls.enabled = false`5. **Batch updates**: Group interaction checks ```javascript// Use simpler geometry for raycastingconst complexMesh = loadedModel;const collisionMesh = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ visible: false }),);collisionMesh.userData.target = complexMesh;clickables.push(collisionMesh);``` ## See Also - `threejs-fundamentals` - Camera and scene setup- `threejs-animation` - Animating interactions- `threejs-shaders` - Visual feedback effectsRelated skills
Threejs Animation
Handles the full Three.js animation pipeline from basic procedural motion to complex skeletal rigs with blend trees. Covers AnimationMixer setup, keyframe track
Threejs Fundamentals
Install Threejs Fundamentals skill for Claude Code from cloudai-x/threejs-skills.
Threejs Geometry
Install Threejs Geometry skill for Claude Code from cloudai-x/threejs-skills.