r3f-interaction

安装量: 1.4K
排名: #1018

安装

npx skills add https://github.com/enzed/r3f-skills --skill r3f-interaction

React Three Fiber Interaction Quick Start import { Canvas } from '@react-three/fiber' import { OrbitControls } from '@react-three/drei'

function InteractiveMesh() { return ( console.log('Clicked!', e.point)} onPointerOver={(e) => console.log('Hover')} onPointerOut={(e) => console.log('Unhover')} > ) }

export default function App() { return ( ) }

Pointer Events

R3F provides built-in pointer events on mesh elements.

Available Events {}} // Click (pointerdown + pointerup on same object) onDoubleClick={(e) => {}} // Double click onContextMenu={(e) => {}} // Right click

// Pointer events onPointerDown={(e) => {}} // Pointer pressed onPointerUp={(e) => {}} // Pointer released onPointerMove={(e) => {}} // Pointer moved while over object onPointerOver={(e) => {}} // Pointer enters object onPointerOut={(e) => {}} // Pointer leaves object onPointerEnter={(e) => {}} // Pointer enters object (no bubbling) onPointerLeave={(e) => {}} // Pointer leaves object (no bubbling) onPointerMissed={(e) => {}} // Click that missed all objects

// Wheel onWheel={(e) => {}} // Mouse wheel

// Touch onPointerCancel={(e) => {}} // Touch cancelled

Event Object function InteractiveMesh() { const handleClick = (event) => { // Stop propagation to parent objects event.stopPropagation()

// Event properties
console.log({
  object: event.object,           // The mesh that was clicked
  point: event.point,             // World coordinates of intersection
  distance: event.distance,       // Distance from camera
  face: event.face,               // Intersected face
  faceIndex: event.faceIndex,     // Face index
  uv: event.uv,                   // UV coordinates at intersection
  normal: event.normal,           // Face normal
  camera: event.camera,           // Current camera
  ray: event.ray,                 // Ray used for intersection
  intersections: event.intersections, // All intersections
  nativeEvent: event.nativeEvent, // Original DOM event
  delta: event.delta,             // Click distance (useful for drag detection)
})

}

return ( ) }

Hover Effects import { useState } from 'react'

function HoverableMesh() { const [hovered, setHovered] = useState(false)

return ( { e.stopPropagation() setHovered(true) document.body.style.cursor = 'pointer' }} onPointerOut={(e) => { setHovered(false) document.body.style.cursor = 'default' }} scale={hovered ? 1.2 : 1} > ) }

Selective Raycasting // Disable raycasting for specific objects null}>

// Or use layers console.log('clicked')}

Camera Controls OrbitControls import { OrbitControls } from '@react-three/drei'

function Scene() { return ( <>

  <OrbitControls
    makeDefault                    // Use as default controls
    enableDamping                  // Smooth movement
    dampingFactor={0.05}
    enableZoom={true}
    enablePan={true}
    enableRotate={true}
    autoRotate={false}
    autoRotateSpeed={2}
    minDistance={2}
    maxDistance={50}
    minPolarAngle={0}              // Top limit
    maxPolarAngle={Math.PI / 2}    // Horizon limit
    minAzimuthAngle={-Math.PI / 4} // Left limit
    maxAzimuthAngle={Math.PI / 4}  // Right limit
    target={[0, 1, 0]}             // Look-at point
  />
</>

) }

OrbitControls with Ref import { OrbitControls } from '@react-three/drei' import { useRef, useEffect } from 'react'

function Scene() { const controlsRef = useRef()

useEffect(() => { // Access controls methods if (controlsRef.current) { controlsRef.current.reset() controlsRef.current.target.set(0, 1, 0) controlsRef.current.update() } }, [])

return }

MapControls

Top-down map-style controls.

import { MapControls } from '@react-three/drei'

FlyControls

Free-flying camera controls.

import { FlyControls } from '@react-three/drei'

FirstPersonControls

FPS-style controls.

import { FirstPersonControls } from '@react-three/drei'

PointerLockControls

Lock pointer for FPS games.

import { PointerLockControls } from '@react-three/drei' import { useRef } from 'react'

function Scene() { const controlsRef = useRef()

return ( <>

  {/* Click to lock pointer */}
  <mesh onClick={() => controlsRef.current?.lock()}>
    <planeGeometry args={[10, 10]} />
    <meshBasicMaterial color="green" />
  </mesh>
</>

) }

CameraControls

Advanced camera controls with smooth transitions.

import { CameraControls } from '@react-three/drei' import { useRef } from 'react'

function Scene() { const controlsRef = useRef()

const focusOnObject = async () => { // Smooth transition to target await controlsRef.current?.setLookAt( 5, 3, 5, // Camera position 0, 0, 0, // Look-at target true // Enable transition ) }

return ( <>

  <mesh onClick={focusOnObject}>
    <boxGeometry />
    <meshStandardMaterial color="red" />
  </mesh>
</>

) }

TrackballControls

Unconstrained rotation controls.

import { TrackballControls } from '@react-three/drei'

ArcballControls

Arc-based rotation controls.

import { ArcballControls } from '@react-three/drei'

Transform Controls

Gizmo for moving/rotating/scaling objects.

import { TransformControls, OrbitControls } from '@react-three/drei' import { useRef, useState } from 'react'

function Scene() { const meshRef = useRef() const [mode, setMode] = useState('translate') const orbitRef = useRef()

return ( <>

  <TransformControls
    object={meshRef}
    mode={mode}  // 'translate' | 'rotate' | 'scale'
    space="local"  // 'local' | 'world'
    onMouseDown={() => {
      // Disable orbit while transforming
      if (orbitRef.current) orbitRef.current.enabled = false
    }}
    onMouseUp={() => {
      if (orbitRef.current) orbitRef.current.enabled = true
    }}
  />

  <mesh ref={meshRef}>
    <boxGeometry />
    <meshStandardMaterial color="orange" />
  </mesh>

  {/* Mode switching buttons in HTML */}
  <div className="controls">
    <button onClick={() => setMode('translate')}>Move</button>
    <button onClick={() => setMode('rotate')}>Rotate</button>
    <button onClick={() => setMode('scale')}>Scale</button>
  </div>
</>

) }

PivotControls

Alternative transform gizmo with pivot point.

import { PivotControls } from '@react-three/drei'

function Scene() { return ( ) }

Drag Controls useDrag from @use-gesture/react npm install @use-gesture/react

import { useDrag } from '@use-gesture/react' import { useSpring, animated } from '@react-spring/three' import { useThree } from '@react-three/fiber'

function DraggableMesh() { const { size, viewport } = useThree() const aspect = size.width / viewport.width

const [spring, api] = useSpring(() => ({ position: [0, 0, 0], config: { mass: 1, tension: 280, friction: 60 } }))

const bind = useDrag(({ movement: [mx, my], down }) => { api.start({ position: down ? [mx / aspect, -my / aspect, 0] : [0, 0, 0] }) })

return ( ) }

DragControls (Drei) import { DragControls, OrbitControls } from '@react-three/drei' import { useRef } from 'react'

function Scene() { const meshRef = useRef() const orbitRef = useRef()

return ( <>

  <DragControls
    onDragStart={() => {
      if (orbitRef.current) orbitRef.current.enabled = false
    }}
    onDragEnd={() => {
      if (orbitRef.current) orbitRef.current.enabled = true
    }}
  >
    <mesh ref={meshRef}>
      <boxGeometry />
      <meshStandardMaterial color="orange" />
    </mesh>
  </DragControls>
</>

) }

Keyboard Controls KeyboardControls (Drei) import { KeyboardControls, useKeyboardControls } from '@react-three/drei' import { useFrame } from '@react-three/fiber' import { useRef } from 'react'

// Define key mappings const keyMap = [ { name: 'forward', keys: ['ArrowUp', 'KeyW'] }, { name: 'backward', keys: ['ArrowDown', 'KeyS'] }, { name: 'left', keys: ['ArrowLeft', 'KeyA'] }, { name: 'right', keys: ['ArrowRight', 'KeyD'] }, { name: 'jump', keys: ['Space'] }, { name: 'sprint', keys: ['ShiftLeft'] }, ]

function Player() { const meshRef = useRef() const [, getKeys] = useKeyboardControls()

useFrame((state, delta) => { const { forward, backward, left, right, jump, sprint } = getKeys()

const speed = sprint ? 10 : 5

if (forward) meshRef.current.position.z -= speed * delta
if (backward) meshRef.current.position.z += speed * delta
if (left) meshRef.current.position.x -= speed * delta
if (right) meshRef.current.position.x += speed * delta
if (jump) meshRef.current.position.y += speed * delta

})

return ( ) }

export default function App() { return ( ) }

Subscribe to Key Changes import { useKeyboardControls } from '@react-three/drei' import { useEffect } from 'react'

function KeyListener() { const jumpPressed = useKeyboardControls((state) => state.jump)

useEffect(() => { if (jumpPressed) { console.log('Jump!') } }, [jumpPressed])

return null }

Selection System Click to Select import { useState } from 'react'

function SelectableScene() { const [selected, setSelected] = useState(null)

return ( <> {[[-2, 0, 0], [0, 0, 0], [2, 0, 0]].map((position, i) => ( { e.stopPropagation() setSelected(i) }} > ))}

  {/* Click on empty space to deselect */}
  <mesh
    position={[0, -1, 0]}
    rotation={[-Math.PI / 2, 0, 0]}
    onClick={() => setSelected(null)}
  >
    <planeGeometry args={[20, 20]} />
    <meshStandardMaterial color="gray" />
  </mesh>
</>

) }

Multi-Select with Outline import { useState } from 'react' import { EffectComposer, Outline, Selection, Select } from '@react-three/postprocessing'

function MultiSelectScene() { const [selected, setSelected] = useState(new Set())

const toggleSelect = (id, event) => { event.stopPropagation() setSelected((prev) => { const next = new Set(prev) if (event.shiftKey) { // Multi-select with shift if (next.has(id)) { next.delete(id) } else { next.add(id) } } else { // Single select next.clear() next.add(id) } return next }) }

return (

  {[0, 1, 2, 3, 4].map((id) => (
    <Select key={id} enabled={selected.has(id)}>
      <mesh
        position={[(id - 2) * 2, 0, 0]}
        onClick={(e) => toggleSelect(id, e)}
      >
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
    </Select>
  ))}
</Selection>

) }

Screen-Space to World-Space Get World Position from Click import { useThree } from '@react-three/fiber' import * as THREE from 'three'

function ClickToPlace() { const { camera, raycaster, pointer } = useThree() const planeRef = useRef()

const handleClick = (event) => { // Create intersection plane const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) const intersection = new THREE.Vector3()

// Cast ray from pointer
raycaster.setFromCamera(pointer, camera)
raycaster.ray.intersectPlane(plane, intersection)

console.log('World position:', intersection)

}

return ( ) }

World Position to Screen Position import { useThree, useFrame } from '@react-three/fiber' import { Html } from '@react-three/drei' import * as THREE from 'three'

function WorldToScreen({ target }) { const { camera, size } = useThree()

const getScreenPosition = (worldPos) => { const vector = worldPos.clone() vector.project(camera)

return {
  x: (vector.x * 0.5 + 0.5) * size.width,
  y: (1 - (vector.y * 0.5 + 0.5)) * size.height
}

}

// Or use Html component which handles this automatically return (

Label
) }

Gesture Recognition usePinch and useWheel import { usePinch, useWheel } from '@use-gesture/react' import { useSpring, animated } from '@react-spring/three'

function ZoomableMesh() { const [spring, api] = useSpring(() => ({ scale: 1, config: { mass: 1, tension: 200, friction: 30 } }))

usePinch( ({ offset: [s] }) => { api.start({ scale: s }) }, { target: window } )

useWheel( ({ delta: [, dy] }) => { api.start({ scale: spring.scale.get() - dy * 0.001 }) }, { target: window } )

return ( ) }

Scroll Controls import { Canvas } from '@react-three/fiber' import { ScrollControls, Scroll, useScroll } from '@react-three/drei' import { useFrame } from '@react-three/fiber' import { useRef } from 'react'

function AnimatedOnScroll() { const meshRef = useRef() const scroll = useScroll()

useFrame(() => { const offset = scroll.offset // 0 to 1 meshRef.current.rotation.y = offset * Math.PI * 2 meshRef.current.position.y = offset * 5 })

return ( ) }

export default function App() { return (

    {/* HTML content that scrolls */}
    <Scroll html>
      <h1 style={{ position: 'absolute', top: '10vh' }}>Page 1</h1>
      <h1 style={{ position: 'absolute', top: '110vh' }}>Page 2</h1>
      <h1 style={{ position: 'absolute', top: '210vh' }}>Page 3</h1>
    </Scroll>
  </ScrollControls>
</Canvas>

) }

Presentation Controls

For product showcases with limited rotation.

import { PresentationControls } from '@react-three/drei'

function ProductShowcase() { return ( ) }

Performance Tips Stop propagation: Prevent unnecessary raycasts Use layers: Filter raycast targets Simpler collision meshes: Use invisible simple geometry Throttle events: Limit onPointerMove frequency Disable controls when not needed: enabled={false} // Use simpler geometry for raycasting function OptimizedInteraction() { return ( {/ Complex visible mesh /} null}>

  {/* Simple invisible collision mesh */}
  <mesh onClick={() => console.log('clicked')}>
    <sphereGeometry args={[1.5]} />
    <meshBasicMaterial visible={false} />
  </mesh>
</group>

) }

// Throttle pointer move events import { useMemo, useCallback } from 'react' import throttle from 'lodash/throttle'

function ThrottledHover() { const handleMove = useMemo( () => throttle((e) => { console.log('Move', e.point) }, 100), [] )

return ( ) }

See Also r3f-fundamentals - Canvas and scene setup r3f-animation - Animating interactions r3f-postprocessing - Visual feedback effects (outline, selection)

返回排行榜