From react-native-3d
Implement camera controls for mobile 3D apps - OrbitControls, gesture-based cameras, touch conflicts, camera animation, and bounds/limits.
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-native-3d:camera-controls-mobileThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill covers implementing camera controls in React Native 3D apps - orbit, pan, zoom - while avoiding common touch conflict issues.
This skill covers implementing camera controls in React Native 3D apps - orbit, pan, zoom - while avoiding common touch conflict issues.
OrbitControls and object interaction compete for touch events. Solutions:
The simplest approach - disable controls during object interaction:
import { OrbitControls } from '@react-three/drei/native';
import { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
import { useRef, useState } from 'react';
function SceneWithControls() {
const controlsRef = useRef<OrbitControlsImpl>(null);
const [isInteracting, setIsInteracting] = useState(false);
return (
<>
<OrbitControls
ref={controlsRef}
enabled={!isInteracting}
enablePan={true}
enableZoom={true}
enableRotate={true}
minDistance={2}
maxDistance={20}
minPolarAngle={0}
maxPolarAngle={Math.PI / 2} // Prevent going below ground
/>
<InteractiveObject
onInteractionStart={() => setIsInteracting(true)}
onInteractionEnd={() => setIsInteracting(false)}
/>
</>
);
}
| Prop | Type | Description |
|---|---|---|
enabled | boolean | Master enable/disable |
enableRotate | boolean | Allow orbit rotation |
enablePan | boolean | Allow panning |
enableZoom | boolean | Allow pinch zoom |
minDistance | number | Minimum zoom distance |
maxDistance | number | Maximum zoom distance |
minPolarAngle | number | Minimum vertical angle (0 = top) |
maxPolarAngle | number | Maximum vertical angle (π = bottom) |
minAzimuthAngle | number | Minimum horizontal angle |
maxAzimuthAngle | number | Maximum horizontal angle |
dampingFactor | number | Smoothing (requires enableDamping) |
target | Vector3 | Look-at point |
A package specifically designed for React Native:
npm install r3f-native-orbitcontrols
import { OrbitControls } from 'r3f-native-orbitcontrols';
function Scene() {
return (
<>
<OrbitControls />
{/* Your scene content */}
</>
);
}
Note: This package may have different behavior than drei's OrbitControls. Test both to see which works better for your use case.
For full control over camera behavior and gesture handling:
import { useThree, useFrame } from '@react-three/fiber/native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSharedValue, withSpring } from 'react-native-reanimated';
import { View } from 'react-native';
import * as THREE from 'three';
import { useRef, useCallback } from 'react';
// Shared values for camera state
interface CameraState {
theta: SharedValue<number>; // Horizontal angle
phi: SharedValue<number>; // Vertical angle
radius: SharedValue<number>; // Distance from target
targetX: SharedValue<number>; // Pan X
targetY: SharedValue<number>; // Pan Y
targetZ: SharedValue<number>; // Pan Z
}
function useCameraState(): CameraState {
return {
theta: useSharedValue(0),
phi: useSharedValue(Math.PI / 4),
radius: useSharedValue(10),
targetX: useSharedValue(0),
targetY: useSharedValue(0),
targetZ: useSharedValue(0),
};
}
// Component that updates camera each frame
function CameraController({ state }: { state: CameraState }) {
const { camera } = useThree();
useFrame(() => {
// Spherical to Cartesian conversion
const x = state.radius.value * Math.sin(state.phi.value) * Math.cos(state.theta.value);
const y = state.radius.value * Math.cos(state.phi.value);
const z = state.radius.value * Math.sin(state.phi.value) * Math.sin(state.theta.value);
camera.position.set(
x + state.targetX.value,
y + state.targetY.value,
z + state.targetZ.value
);
camera.lookAt(
state.targetX.value,
state.targetY.value,
state.targetZ.value
);
});
return null;
}
// Gesture wrapper component
function GestureControlledCanvas({ children }: { children: React.ReactNode }) {
const cameraState = useCameraState();
// Rotation gesture (single finger drag)
const rotationGesture = Gesture.Pan()
.onUpdate((e) => {
cameraState.theta.value -= e.changeX * 0.01;
cameraState.phi.value = Math.max(
0.1,
Math.min(Math.PI - 0.1, cameraState.phi.value - e.changeY * 0.01)
);
});
// Zoom gesture (pinch)
const zoomGesture = Gesture.Pinch()
.onUpdate((e) => {
const newRadius = cameraState.radius.value / e.scale;
cameraState.radius.value = Math.max(2, Math.min(50, newRadius));
});
// Pan gesture (two finger drag)
const panGesture = Gesture.Pan()
.minPointers(2)
.onUpdate((e) => {
// Pan in camera-relative space
const panSpeed = 0.01 * cameraState.radius.value;
cameraState.targetX.value -= e.changeX * panSpeed;
cameraState.targetY.value += e.changeY * panSpeed;
});
// Compose gestures
const composed = Gesture.Simultaneous(
rotationGesture,
Gesture.Simultaneous(zoomGesture, panGesture)
);
return (
<GestureDetector gesture={composed}>
<View style={{ flex: 1 }}>
<Canvas>
<CameraController state={cameraState} />
{children}
</Canvas>
</View>
</GestureDetector>
);
}
// Usage
export function App() {
return (
<GestureControlledCanvas>
<ambientLight />
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</GestureControlledCanvas>
);
}
Use different gesture types for different actions:
function SmartGestureCanvas({ children }) {
const [mode, setMode] = useState<'camera' | 'object'>('camera');
const cameraState = useCameraState();
// Long press to enter object mode
const longPressGesture = Gesture.LongPress()
.minDuration(300)
.onStart(() => setMode('object'))
.onEnd(() => setMode('camera'));
// Camera rotation only when in camera mode
const rotationGesture = Gesture.Pan()
.enabled(mode === 'camera')
.onUpdate((e) => {
// ... rotate camera
});
return (
<GestureDetector gesture={Gesture.Race(longPressGesture, rotationGesture)}>
<View style={{ flex: 1 }}>
<Canvas>
<CameraController state={cameraState} />
<ObjectLayer enabled={mode === 'object'} />
{children}
</Canvas>
{/* Mode indicator */}
<View style={styles.modeIndicator}>
<Text>{mode === 'camera' ? '🎥 Camera' : '✋ Edit'}</Text>
</View>
</View>
</GestureDetector>
);
}
Smoothly animate camera to focus on selected objects:
import { useSpring } from '@react-spring/three';
function AnimatedCamera({ targetPosition, targetLookAt }) {
const { camera } = useThree();
const [springs, api] = useSpring(() => ({
position: [10, 10, 10],
lookAt: [0, 0, 0],
config: { mass: 1, tension: 170, friction: 26 }
}));
// Animate to new target
useEffect(() => {
api.start({
position: targetPosition,
lookAt: targetLookAt
});
}, [targetPosition, targetLookAt]);
useFrame(() => {
camera.position.set(...springs.position.get());
camera.lookAt(...springs.lookAt.get());
});
return null;
}
// Usage: Focus on selected node
function Scene({ selectedNode }) {
const targetPos = selectedNode
? [
selectedNode.position.x + 5,
selectedNode.position.y + 5,
selectedNode.position.z + 5
]
: [10, 10, 10];
const targetLookAt = selectedNode
? [selectedNode.position.x, selectedNode.position.y, selectedNode.position.z]
: [0, 0, 0];
return (
<>
<AnimatedCamera targetPosition={targetPos} targetLookAt={targetLookAt} />
{/* ... */}
</>
);
}
Prevent camera from going where it shouldn't:
function BoundedCameraController({ bounds }) {
const { camera } = useThree();
useFrame(() => {
// Clamp position to bounds
camera.position.x = Math.max(bounds.minX, Math.min(bounds.maxX, camera.position.x));
camera.position.y = Math.max(bounds.minY, Math.min(bounds.maxY, camera.position.y));
camera.position.z = Math.max(bounds.minZ, Math.min(bounds.maxZ, camera.position.z));
// Ensure camera doesn't go below ground
if (camera.position.y < 0.5) {
camera.position.y = 0.5;
}
});
return null;
}
Provide a way to reset to default view:
function CameraResetButton({ controlsRef }) {
const resetCamera = () => {
if (controlsRef.current) {
controlsRef.current.reset();
}
};
return (
<TouchableOpacity style={styles.resetButton} onPress={resetCamera}>
<Text>Reset View</Text>
</TouchableOpacity>
);
}
// Or with animated transition
function useCameraReset() {
const { camera } = useThree();
const controlsRef = useRef();
const reset = useCallback(() => {
// Animate to default position
const defaultPos = new THREE.Vector3(10, 10, 10);
const defaultTarget = new THREE.Vector3(0, 0, 0);
// Use GSAP, react-spring, or manual animation
// ...
}, [camera]);
return reset;
}
Solution: Use makeDefault={false} or manage enabled state
<OrbitControls makeDefault={false} enabled={!interacting} />
Solution: Store initial state and use relative changes
const panGesture = Gesture.Pan()
.onStart((e) => {
// Store starting values
startTheta.value = theta.value;
startPhi.value = phi.value;
})
.onUpdate((e) => {
// Use total translation, not change
theta.value = startTheta.value - e.translationX * 0.01;
phi.value = startPhi.value - e.translationY * 0.01;
});
Solution: Use logarithmic zoom with saved base value
const savedRadius = useSharedValue(10);
const zoomGesture = Gesture.Pinch()
.onStart(() => {
savedRadius.value = radius.value;
})
.onUpdate((e) => {
// Logarithmic zoom feels more natural
radius.value = savedRadius.value * Math.pow(e.scale, -1);
});
Solution: Implement collision detection or use minimum distance
useFrame(() => {
const minDistance = 2;
const distToTarget = camera.position.distanceTo(target);
if (distToTarget < minDistance) {
camera.position.normalize().multiplyScalar(minDistance);
}
});
For structural visualization with node selection:
function TensegrityViewer({ structure }) {
const controlsRef = useRef();
const [selectedNode, setSelectedNode] = useState(null);
const [isDragging, setIsDragging] = useState(false);
return (
<Canvas>
{/* Camera controls - disabled during node drag */}
<OrbitControls
ref={controlsRef}
enabled={!isDragging}
enablePan={true}
enableZoom={true}
minDistance={3}
maxDistance={30}
maxPolarAngle={Math.PI * 0.9}
/>
{/* Structure with selectable/draggable nodes */}
<TensegrityStructure
structure={structure}
selectedNode={selectedNode}
onNodeSelect={setSelectedNode}
onDragStart={() => setIsDragging(true)}
onDragEnd={() => setIsDragging(false)}
/>
{/* Lights */}
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 10]} intensity={1} />
</Canvas>
);
}
npx claudepluginhub smartwatermelon/smartwatermelon-marketplace --plugin react-native-3dImplements Three.js interactions: raycasting for object selection and picking, OrbitControls for camera, mouse/touch input handling. Use for clickable 3D scenes and user controls.
Build declarative 3D scenes with React Three Fiber in React apps using JSX components for Three.js objects. For interactive configurators, games, portfolios, and data viz.
Provides architecture patterns for integrating Three.js, GSAP ScrollTrigger, React Three Fiber, Framer Motion, and React Spring in complex 3D web experiences. Use for multi-library setups, scroll-driven animations, physics interactions, state management, and performance optimization.