Loading...
Loading...
Meta-skill for combining Three.js, GSAP ScrollTrigger, React Three Fiber, Motion, and React Spring for complex 3D web experiences. Use when building applications that integrate multiple 3D and animation libraries, requiring architecture patterns, state management, and performance optimization across the stack. Triggers on tasks involving library integration, multi-library architectures, scroll-driven 3D experiences, physics-based 3D animations, or complex interactive 3D applications.
npx skill4agent add freshtechbro/claudedesignskills web3d-integration-patterns├── 3D Layer (Three.js)
│ ├── Scene management
│ ├── Camera controls
│ └── Render loop
├── Animation Layer (GSAP)
│ ├── ScrollTrigger for 3D properties
│ ├── Timelines for sequences
│ └── UI transitions
└── UI Layer (React + Motion)
├── HTML overlays
├── State management
└── User interactions// App.jsx - React root
import { useEffect, useRef } from 'react'
import { initThreeScene } from './three/scene'
import { initScrollAnimations } from './animations/scroll'
import { motion } from 'framer-motion'
function App() {
const canvasRef = useRef()
const sceneRef = useRef()
useEffect(() => {
// Initialize Three.js scene
sceneRef.current = initThreeScene(canvasRef.current)
// Initialize GSAP ScrollTrigger animations
initScrollAnimations(sceneRef.current)
// Cleanup
return () => {
sceneRef.current.dispose()
}
}, [])
return (
<div className="app">
<canvas ref={canvasRef} />
<motion.div
className="overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<section className="hero">
<h1>3D Experience</h1>
</section>
<section className="content">
{/* Scrollable content */}
</section>
</motion.div>
</div>
)
}// three/scene.js - Three.js setup
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
export function initThreeScene(canvas) {
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
// Setup scene objects
const geometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 10, 7.5)
scene.add(directionalLight)
camera.position.set(0, 2, 5)
// Animation loop
function animate() {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
animate()
// Resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
return { scene, camera, renderer, cube }
}// animations/scroll.js - GSAP ScrollTrigger integration
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
export function initScrollAnimations(sceneRefs) {
const { camera, cube } = sceneRefs
// Animate camera on scroll
gsap.to(camera.position, {
x: 5,
y: 3,
z: 10,
scrollTrigger: {
trigger: '.content',
start: 'top top',
end: 'bottom center',
scrub: 1,
onUpdate: () => camera.lookAt(cube.position)
}
})
// Animate mesh rotation
gsap.to(cube.rotation, {
y: Math.PI * 2,
x: Math.PI,
scrollTrigger: {
trigger: '.content',
start: 'top bottom',
end: 'bottom top',
scrub: true
}
})
// Animate material properties
gsap.to(cube.material, {
opacity: 0.3,
scrollTrigger: {
trigger: '.content',
start: 'top center',
end: 'center center',
scrub: 1
}
})
}React Component Tree
├── <Canvas> (R3F)
│ ├── 3D Scene Components
│ ├── Lights
│ ├── Camera
│ └── Effects
└── <motion.div> (UI overlays)
├── HTML content
└── Animations// App.jsx - Unified React approach
import { Canvas } from '@react-three/fiber'
import { Suspense } from 'react'
import { motion } from 'framer-motion'
import { Scene } from './components/Scene'
import { Loader } from './components/Loader'
function App() {
return (
<div className="app">
<Canvas
camera={{ position: [0, 2, 5], fov: 75 }}
dpr={[1, 2]}
shadows
>
<Suspense fallback={<Loader />}>
<Scene />
</Suspense>
</Canvas>
<motion.div
className="ui-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1 }}
>
<h1>React-First 3D Experience</h1>
</motion.div>
</div>
)
}// components/Scene.jsx - R3F scene
import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'
import { OrbitControls, Environment } from '@react-three/drei'
import { motion } from 'framer-motion-3d'
export function Scene() {
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 10, 7.5]} castShadow />
<AnimatedCube />
<Floor />
<OrbitControls enableDamping dampingFactor={0.05} />
<Environment preset="sunset" />
</>
)
}
function AnimatedCube() {
const [hovered, setHovered] = useState(false)
const [active, setActive] = useState(false)
return (
<motion.mesh
scale={active ? 1.5 : 1}
onClick={() => setActive(!active)}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
animate={{
rotateY: hovered ? Math.PI * 2 : 0
}}
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</motion.mesh>
)
}
function Floor() {
return (
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -1, 0]} receiveShadow>
<planeGeometry args={[100, 100]} />
<meshStandardMaterial color="#222" />
</mesh>
)
}// components/AnimatedScene.jsx
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import gsap from 'gsap'
export function AnimatedScene() {
const groupRef = useRef()
const timelineRef = useRef()
useEffect(() => {
// Create GSAP timeline for complex sequence
const tl = gsap.timeline({ repeat: -1, yoyo: true })
tl.to(groupRef.current.position, {
y: 2,
duration: 1,
ease: 'power2.inOut'
})
.to(groupRef.current.rotation, {
y: Math.PI * 2,
duration: 2,
ease: 'none'
}, 0) // Start at same time
timelineRef.current = tl
return () => tl.kill()
}, [])
return (
<group ref={groupRef}>
<mesh>
<boxGeometry />
<meshStandardMaterial color="cyan" />
</mesh>
</group>
)
}// components/PhysicsCube.jsx
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import { useSpring, animated, config } from '@react-spring/three'
const AnimatedMesh = animated('mesh')
export function PhysicsCube() {
const [springs, api] = useSpring(() => ({
scale: 1,
position: [0, 0, 0],
config: config.wobbly
}), [])
const handleClick = () => {
api.start({
scale: 1.5,
position: [0, 2, 0]
})
// Return to original after delay
setTimeout(() => {
api.start({
scale: 1,
position: [0, 0, 0]
})
}, 1000)
}
return (
<AnimatedMesh
scale={springs.scale}
position={springs.position}
onClick={handleClick}
>
<boxGeometry />
<meshStandardMaterial color="orange" />
</AnimatedMesh>
)
}import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
// Smooth camera path through multiple points
const cameraPath = [
{ x: 0, y: 2, z: 5, lookAt: { x: 0, y: 0, z: 0 } },
{ x: 5, y: 3, z: 10, lookAt: { x: 0, y: 0, z: 0 } },
{ x: -3, y: 1, z: 8, lookAt: { x: 0, y: 0, z: 0 } }
]
const tl = gsap.timeline({
scrollTrigger: {
trigger: '#container',
start: 'top top',
end: 'bottom bottom',
scrub: 1,
pin: true
}
})
cameraPath.forEach((point, i) => {
tl.to(camera.position, {
x: point.x,
y: point.y,
z: point.z,
duration: 1,
onUpdate: () => camera.lookAt(point.lookAt.x, point.lookAt.y, point.lookAt.z)
}, i)
})import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
function CameraRig() {
const scroll = useScroll()
useFrame((state) => {
const offset = scroll.offset
state.camera.position.x = Math.sin(offset * Math.PI * 2) * 5
state.camera.position.z = Math.cos(offset * Math.PI * 2) * 5
state.camera.lookAt(0, 0, 0)
})
return null
}
export function App() {
return (
<Canvas>
<ScrollControls pages={3} damping={0.5}>
<CameraRig />
<Scroll>
<Scene />
</Scroll>
</ScrollControls>
</Canvas>
)
}import { motion } from 'framer-motion-3d'
function DraggableObject() {
return (
<motion.mesh
drag
dragElastic={0.1}
dragConstraints={{ left: -5, right: 5, top: 5, bottom: -5 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
animate={{
rotateY: [0, Math.PI * 2],
transition: { repeat: Infinity, duration: 4, ease: 'linear' }
}}
>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="hotpink" />
</motion.mesh>
)
}// store.js
import create from 'zustand'
export const useStore = create((set) => ({
selectedObject: null,
cameraMode: 'orbit',
setSelectedObject: (obj) => set({ selectedObject: obj }),
setCameraMode: (mode) => set({ cameraMode: mode })
}))// components/InteractiveObject.jsx
import { useRef, useEffect } from 'react'
import { useStore } from '../store'
import gsap from 'gsap'
export function InteractiveObject({ id }) {
const meshRef = useRef()
const selectedObject = useStore((state) => state.selectedObject)
const setSelectedObject = useStore((state) => state.setSelectedObject)
const isSelected = selectedObject === id
useEffect(() => {
if (isSelected) {
gsap.to(meshRef.current.scale, {
x: 1.2,
y: 1.2,
z: 1.2,
duration: 0.3,
ease: 'back.out'
})
gsap.to(meshRef.current.material, {
emissiveIntensity: 0.5,
duration: 0.3
})
} else {
gsap.to(meshRef.current.scale, {
x: 1,
y: 1,
z: 1,
duration: 0.3,
ease: 'power2.inOut'
})
gsap.to(meshRef.current.material, {
emissiveIntensity: 0,
duration: 0.3
})
}
}, [isSelected])
return (
<mesh
ref={meshRef}
onClick={() => setSelectedObject(isSelected ? null : id)}
>
<boxGeometry />
<meshStandardMaterial color="cyan" emissive="cyan" />
</mesh>
)
}// store/scene.js
import create from 'zustand'
export const useSceneStore = create((set, get) => ({
// State
camera: { position: [0, 2, 5], target: [0, 0, 0] },
objects: {},
selectedId: null,
isAnimating: false,
// Actions
updateCamera: (updates) => set((state) => ({
camera: { ...state.camera, ...updates }
})),
addObject: (id, object) => set((state) => ({
objects: { ...state.objects, [id]: object }
})),
selectObject: (id) => set({ selectedId: id }),
setAnimating: (isAnimating) => set({ isAnimating })
}))import { useSceneStore } from '../store/scene'
function Object3D({ id }) {
const selectedId = useSceneStore((state) => state.selectedId)
const selectObject = useSceneStore((state) => state.selectObject)
const isSelected = selectedId === id
return (
<mesh onClick={() => selectObject(id)}>
<boxGeometry />
<meshStandardMaterial color={isSelected ? 'hotpink' : 'orange'} />
</mesh>
)
}// Unified render loop with conditional rendering
import { Clock } from 'three'
const clock = new Clock()
let needsRender = true
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta()
const elapsed = clock.getElapsedTime()
// Only render when needed
if (needsRender || controls.enabled) {
// Update GSAP animations (handled automatically)
// Update Three.js
controls.update()
renderer.render(scene, camera)
// Reset flag
needsRender = false
}
}
// Trigger re-render on interactions
ScrollTrigger.addEventListener('update', () => {
needsRender = true
})import { Canvas } from '@react-three/fiber'
function App() {
return (
<Canvas
frameloop="demand" // Only renders when needed
dpr={[1, 2]} // Adaptive pixel ratio
>
<Scene />
</Canvas>
)
}
function Scene() {
const invalidate = useThree((state) => state.invalidate)
// Trigger render on state change
const handleClick = () => {
// Update state...
invalidate() // Manually trigger render
}
return <mesh onClick={handleClick}>...</mesh>
}// ❌ Wrong: GSAP and React Spring both animating position
gsap.to(meshRef.current.position, { x: 5 })
api.start({ position: [10, 0, 0] }) // Conflict!// ✅ Correct: Separate properties
gsap.to(meshRef.current.position, { x: 5 }) // GSAP handles position
api.start({ scale: 1.5 }) // Spring handles scale// ❌ Wrong: Updating Three.js without updating React state
mesh.position.x = 5 // Three.js updated
// But React state still shows old value!// ✅ Correct: Update both
const updatePosition = (x) => {
mesh.position.x = x
setPosition(x) // Update React state
}// ❌ Wrong: No cleanup
useEffect(() => {
gsap.to(meshRef.current.rotation, { y: Math.PI * 2, repeat: -1 })
}, [])// ✅ Correct: Cleanup on unmount
useEffect(() => {
const tween = gsap.to(meshRef.current.rotation, { y: Math.PI * 2, repeat: -1 })
return () => {
tween.kill()
}
}, [])| Use Case | Recommended Stack | Rationale |
|---|---|---|
| Marketing landing page with scroll-driven 3D | Three.js + GSAP + React UI | GSAP excels at scroll orchestration |
| React app with interactive 3D product viewer | R3F + Motion | Declarative, state-driven, component-based |
| Complex animation sequences (timeline-based) | R3F + GSAP | GSAP timeline control with R3F components |
| Physics-based interactions (drag, momentum) | R3F + React Spring | Spring physics feel natural for gestures |
| High-performance particle systems | Three.js + GSAP | Imperative control, instancing, minimal overhead |
| Rapid prototyping, quick iterations | R3F + Drei + Motion | High-level abstractions, fast development |
| Game-like experiences with physics | R3F + React Spring + Cannon (physics) | Physics engine + spring-based UI feedback |
architecture_patterns.mdperformance_optimization.mdstate_management.mdintegration_helper.pypattern_generator.pystarter_unified/examples/threejs-webglgsap-scrolltriggerreact-three-fibermotion-framerreact-spring-physics