@lark-apaas/coding-steering 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1001 @@
1
+ # React Three Fiber Animation
2
+
3
+ ## Quick Start
4
+
5
+ ```tsx
6
+ import { Canvas, useFrame } from '@react-three/fiber'
7
+ import { useRef } from 'react'
8
+
9
+ function RotatingBox() {
10
+ const meshRef = useRef()
11
+
12
+ useFrame((state, delta) => {
13
+ meshRef.current.rotation.x += delta
14
+ meshRef.current.rotation.y += delta * 0.5
15
+ })
16
+
17
+ return (
18
+ <mesh ref={meshRef}>
19
+ <boxGeometry />
20
+ <meshStandardMaterial color="hotpink" />
21
+ </mesh>
22
+ )
23
+ }
24
+
25
+ export default function App() {
26
+ return (
27
+ <Canvas>
28
+ <ambientLight />
29
+ <RotatingBox />
30
+ </Canvas>
31
+ )
32
+ }
33
+ ```
34
+
35
+ ## useFrame Hook
36
+
37
+ The core animation hook in R3F. Runs every frame.
38
+
39
+ ### Basic Usage
40
+
41
+ ```tsx
42
+ import { useFrame } from '@react-three/fiber'
43
+ import { useRef } from 'react'
44
+
45
+ function AnimatedMesh() {
46
+ const meshRef = useRef()
47
+
48
+ useFrame((state, delta) => {
49
+ // state contains: clock, camera, scene, gl, mouse, etc.
50
+ // delta is time since last frame in seconds
51
+
52
+ meshRef.current.rotation.y += delta
53
+ })
54
+
55
+ return (
56
+ <mesh ref={meshRef}>
57
+ <boxGeometry />
58
+ <meshStandardMaterial color="orange" />
59
+ </mesh>
60
+ )
61
+ }
62
+ ```
63
+
64
+ ### State Object
65
+
66
+ ```tsx
67
+ useFrame((state, delta, xrFrame) => {
68
+ const {
69
+ clock, // THREE.Clock
70
+ camera, // Current camera
71
+ scene, // Scene
72
+ gl, // WebGLRenderer
73
+ mouse, // Normalized mouse position (-1 to 1)
74
+ pointer, // Same as mouse
75
+ viewport, // Viewport dimensions
76
+ size, // Canvas size
77
+ raycaster, // Raycaster
78
+ get, // Get current state
79
+ set, // Set state
80
+ invalidate, // Request re-render (when frameloop="demand")
81
+ } = state
82
+
83
+ // Time-based animation
84
+ const t = clock.getElapsedTime()
85
+ meshRef.current.position.y = Math.sin(t) * 2
86
+ })
87
+ ```
88
+
89
+ ### Render Priority
90
+
91
+ ```tsx
92
+ // Lower numbers run first. Default is 0.
93
+ // Use negative for pre-render, positive for post-render
94
+
95
+ function PreRender() {
96
+ useFrame(() => {
97
+ // Runs before main render
98
+ }, -1)
99
+ }
100
+
101
+ function PostRender() {
102
+ useFrame(() => {
103
+ // Runs after main render
104
+ }, 1)
105
+ }
106
+
107
+ function DefaultRender() {
108
+ useFrame(() => {
109
+ // Runs at default priority (0)
110
+ })
111
+ }
112
+ ```
113
+
114
+ ### Conditional Animation
115
+
116
+ ```tsx
117
+ function ConditionalAnimation({ isAnimating }) {
118
+ const meshRef = useRef()
119
+
120
+ useFrame((state, delta) => {
121
+ if (!isAnimating) return
122
+ meshRef.current.rotation.y += delta
123
+ })
124
+
125
+ return <mesh ref={meshRef}>...</mesh>
126
+ }
127
+ ```
128
+
129
+ ## GLTF Animations with useAnimations
130
+
131
+ The recommended way to play animations from GLTF/GLB files.
132
+
133
+ ### useAnimations Basic Usage
134
+
135
+ ```tsx
136
+ import { useGLTF, useAnimations } from '@react-three/drei'
137
+ import { useEffect, useRef } from 'react'
138
+
139
+ function AnimatedModel() {
140
+ const group = useRef()
141
+ const { scene, animations } = useGLTF('/models/character.glb')
142
+ const { actions, names } = useAnimations(animations, group)
143
+
144
+ useEffect(() => {
145
+ // Play first animation
146
+ actions[names[0]]?.play()
147
+ }, [actions, names])
148
+
149
+ return <primitive ref={group} object={scene} />
150
+ }
151
+ ```
152
+
153
+ ### Animation Control
154
+
155
+ ```tsx
156
+ function Character() {
157
+ const group = useRef()
158
+ const { scene, animations } = useGLTF('/models/character.glb')
159
+ const { actions, mixer } = useAnimations(animations, group)
160
+
161
+ useEffect(() => {
162
+ const action = actions['Walk']
163
+ if (action) {
164
+ // Playback control
165
+ action.play()
166
+ action.stop()
167
+ action.reset()
168
+ action.paused = true
169
+
170
+ // Speed
171
+ action.timeScale = 1.5 // 1.5x speed
172
+ action.timeScale = -1 // Reverse
173
+
174
+ // Loop modes
175
+ action.loop = THREE.LoopOnce
176
+ action.loop = THREE.LoopRepeat
177
+ action.loop = THREE.LoopPingPong
178
+ action.repetitions = 3
179
+ action.clampWhenFinished = true
180
+
181
+ // Weight (for blending)
182
+ action.weight = 1
183
+ }
184
+ }, [actions])
185
+
186
+ return <primitive ref={group} object={scene} />
187
+ }
188
+ ```
189
+
190
+ ### Crossfade Between Animations
191
+
192
+ ```tsx
193
+ import { useGLTF, useAnimations } from '@react-three/drei'
194
+ import { useState, useEffect, useRef } from 'react'
195
+
196
+ function Character() {
197
+ const group = useRef()
198
+ const { scene, animations } = useGLTF('/models/character.glb')
199
+ const { actions } = useAnimations(animations, group)
200
+ const [currentAnim, setCurrentAnim] = useState('Idle')
201
+
202
+ useEffect(() => {
203
+ // Fade out all animations
204
+ Object.values(actions).forEach(action => {
205
+ action?.fadeOut(0.5)
206
+ })
207
+
208
+ // Fade in current animation
209
+ actions[currentAnim]?.reset().fadeIn(0.5).play()
210
+ }, [currentAnim, actions])
211
+
212
+ return (
213
+ <group ref={group}>
214
+ <primitive object={scene} />
215
+ </group>
216
+ )
217
+ }
218
+ ```
219
+
220
+ ### Animation Events
221
+
222
+ ```tsx
223
+ function AnimatedModel() {
224
+ const group = useRef()
225
+ const { scene, animations } = useGLTF('/models/character.glb')
226
+ const { actions, mixer } = useAnimations(animations, group)
227
+
228
+ useEffect(() => {
229
+ // Listen for animation events
230
+ const onFinished = (e) => {
231
+ console.log('Animation finished:', e.action.getClip().name)
232
+ }
233
+
234
+ const onLoop = (e) => {
235
+ console.log('Animation looped:', e.action.getClip().name)
236
+ }
237
+
238
+ mixer.addEventListener('finished', onFinished)
239
+ mixer.addEventListener('loop', onLoop)
240
+
241
+ return () => {
242
+ mixer.removeEventListener('finished', onFinished)
243
+ mixer.removeEventListener('loop', onLoop)
244
+ }
245
+ }, [mixer])
246
+
247
+ return <primitive ref={group} object={scene} />
248
+ }
249
+ ```
250
+
251
+ ### Animation Blending
252
+
253
+ ```tsx
254
+ function CharacterController({ speed = 0 }) {
255
+ const group = useRef()
256
+ const { scene, animations } = useGLTF('/models/character.glb')
257
+ const { actions } = useAnimations(animations, group)
258
+
259
+ useEffect(() => {
260
+ // Start all animations
261
+ actions['Idle']?.play()
262
+ actions['Walk']?.play()
263
+ actions['Run']?.play()
264
+ }, [actions])
265
+
266
+ // Blend based on speed
267
+ useFrame(() => {
268
+ if (speed < 0.1) {
269
+ actions['Idle']?.setEffectiveWeight(1)
270
+ actions['Walk']?.setEffectiveWeight(0)
271
+ actions['Run']?.setEffectiveWeight(0)
272
+ } else if (speed < 5) {
273
+ const t = speed / 5
274
+ actions['Idle']?.setEffectiveWeight(1 - t)
275
+ actions['Walk']?.setEffectiveWeight(t)
276
+ actions['Run']?.setEffectiveWeight(0)
277
+ } else {
278
+ const t = Math.min((speed - 5) / 5, 1)
279
+ actions['Idle']?.setEffectiveWeight(0)
280
+ actions['Walk']?.setEffectiveWeight(1 - t)
281
+ actions['Run']?.setEffectiveWeight(t)
282
+ }
283
+ })
284
+
285
+ return <primitive ref={group} object={scene} />
286
+ }
287
+ ```
288
+
289
+ ## Spring Animation (@react-spring/three)
290
+
291
+ Physics-based spring animations that integrate with R3F.
292
+
293
+ ### Installation
294
+
295
+ ```bash
296
+ npm install @react-spring/three
297
+ ```
298
+
299
+ ### Basic Spring
300
+
301
+ ```tsx
302
+ import { useSpring, animated } from '@react-spring/three'
303
+
304
+ function AnimatedBox() {
305
+ const [active, setActive] = useState(false)
306
+
307
+ const { scale, color } = useSpring({
308
+ scale: active ? 1.5 : 1,
309
+ color: active ? '#ff6b6b' : '#4ecdc4',
310
+ config: { mass: 1, tension: 280, friction: 60 }
311
+ })
312
+
313
+ return (
314
+ <animated.mesh
315
+ scale={scale}
316
+ onClick={() => setActive(!active)}
317
+ >
318
+ <boxGeometry />
319
+ <animated.meshStandardMaterial color={color} />
320
+ </animated.mesh>
321
+ )
322
+ }
323
+ ```
324
+
325
+ ### Spring Config Presets
326
+
327
+ ```tsx
328
+ import { useSpring, animated, config } from '@react-spring/three'
329
+
330
+ function SpringPresets() {
331
+ const { position } = useSpring({
332
+ position: [0, 2, 0],
333
+ config: config.wobbly // Presets: default, gentle, wobbly, stiff, slow, molasses
334
+ })
335
+
336
+ // Or custom config
337
+ const { rotation } = useSpring({
338
+ rotation: [0, Math.PI, 0],
339
+ config: {
340
+ mass: 1,
341
+ tension: 170,
342
+ friction: 26,
343
+ clamp: false,
344
+ precision: 0.01,
345
+ velocity: 0,
346
+ }
347
+ })
348
+
349
+ return (
350
+ <animated.mesh position={position} rotation={rotation}>
351
+ <boxGeometry />
352
+ <meshStandardMaterial />
353
+ </animated.mesh>
354
+ )
355
+ }
356
+ ```
357
+
358
+ ### Multiple Springs
359
+
360
+ ```tsx
361
+ import { useSprings, animated } from '@react-spring/three'
362
+
363
+ function AnimatedBoxes({ count = 5 }) {
364
+ const [springs, api] = useSprings(count, (i) => ({
365
+ position: [i * 2 - count, 0, 0],
366
+ scale: 1,
367
+ config: { mass: 1, tension: 280, friction: 60 }
368
+ }))
369
+
370
+ const handleClick = (index) => {
371
+ api.start((i) => {
372
+ if (i === index) return { scale: 1.5 }
373
+ return { scale: 1 }
374
+ })
375
+ }
376
+
377
+ return springs.map((spring, i) => (
378
+ <animated.mesh
379
+ key={i}
380
+ position={spring.position}
381
+ scale={spring.scale}
382
+ onClick={() => handleClick(i)}
383
+ >
384
+ <boxGeometry />
385
+ <meshStandardMaterial color="orange" />
386
+ </animated.mesh>
387
+ ))
388
+ }
389
+ ```
390
+
391
+ ### Gesture Integration
392
+
393
+ ```tsx
394
+ import { useSpring, animated } from '@react-spring/three'
395
+ import { useDrag } from '@use-gesture/react'
396
+
397
+ function DraggableBox() {
398
+ const [spring, api] = useSpring(() => ({
399
+ position: [0, 0, 0],
400
+ config: { mass: 1, tension: 280, friction: 60 }
401
+ }))
402
+
403
+ const bind = useDrag(({ movement: [mx, my], down }) => {
404
+ api.start({
405
+ position: down ? [mx / 100, -my / 100, 0] : [0, 0, 0]
406
+ })
407
+ })
408
+
409
+ return (
410
+ <animated.mesh {...bind()} position={spring.position}>
411
+ <boxGeometry />
412
+ <meshStandardMaterial color="hotpink" />
413
+ </animated.mesh>
414
+ )
415
+ }
416
+ ```
417
+
418
+ ### Chain Animations
419
+
420
+ ```tsx
421
+ import { useSpring, animated, useChain, useSpringRef } from '@react-spring/three'
422
+
423
+ function ChainedAnimation() {
424
+ const scaleRef = useSpringRef()
425
+ const rotationRef = useSpringRef()
426
+
427
+ const { scale } = useSpring({
428
+ ref: scaleRef,
429
+ from: { scale: 0 },
430
+ to: { scale: 1 },
431
+ config: { tension: 200, friction: 20 }
432
+ })
433
+
434
+ const { rotation } = useSpring({
435
+ ref: rotationRef,
436
+ from: { rotation: [0, 0, 0] },
437
+ to: { rotation: [0, Math.PI * 2, 0] },
438
+ config: { tension: 100, friction: 30 }
439
+ })
440
+
441
+ // Scale first (0-0.5), then rotation (0.5-1)
442
+ useChain([scaleRef, rotationRef], [0, 0.5])
443
+
444
+ return (
445
+ <animated.mesh scale={scale} rotation={rotation}>
446
+ <boxGeometry />
447
+ <meshStandardMaterial color="cyan" />
448
+ </animated.mesh>
449
+ )
450
+ }
451
+ ```
452
+
453
+ ## Morph Targets
454
+
455
+ Blend between different mesh shapes.
456
+
457
+ ```tsx
458
+ import { useGLTF } from '@react-three/drei'
459
+ import { useFrame } from '@react-three/fiber'
460
+ import { useRef } from 'react'
461
+
462
+ function MorphingFace() {
463
+ const { scene, nodes } = useGLTF('/models/face.glb')
464
+ const meshRef = useRef()
465
+
466
+ useFrame(({ clock }) => {
467
+ const t = clock.getElapsedTime()
468
+
469
+ // Access morph target influences
470
+ if (meshRef.current?.morphTargetInfluences) {
471
+ // Animate smile
472
+ const smileIndex = meshRef.current.morphTargetDictionary['smile']
473
+ meshRef.current.morphTargetInfluences[smileIndex] = (Math.sin(t) + 1) / 2
474
+ }
475
+ })
476
+
477
+ return (
478
+ <primitive ref={meshRef} object={nodes.Face} />
479
+ )
480
+ }
481
+ ```
482
+
483
+ ### Controlled Morph Targets
484
+
485
+ ```tsx
486
+ function MorphControls({ morphInfluences }) {
487
+ const { nodes } = useGLTF('/models/face.glb')
488
+ const meshRef = useRef()
489
+
490
+ useFrame(() => {
491
+ if (meshRef.current?.morphTargetInfluences) {
492
+ Object.entries(morphInfluences).forEach(([name, value]) => {
493
+ const index = meshRef.current.morphTargetDictionary[name]
494
+ if (index !== undefined) {
495
+ meshRef.current.morphTargetInfluences[index] = value
496
+ }
497
+ })
498
+ }
499
+ })
500
+
501
+ return <primitive ref={meshRef} object={nodes.Face} />
502
+ }
503
+
504
+ // Usage
505
+ <MorphControls morphInfluences={{ smile: 0.5, blink: 1, angry: 0 }} />
506
+ ```
507
+
508
+ ## Skeletal Animation
509
+
510
+ ### Accessing Bones
511
+
512
+ ```tsx
513
+ import { useGLTF } from '@react-three/drei'
514
+ import { useFrame } from '@react-three/fiber'
515
+ import { useEffect, useRef } from 'react'
516
+
517
+ function SkeletalCharacter() {
518
+ const { scene } = useGLTF('/models/character.glb')
519
+ const headBoneRef = useRef()
520
+
521
+ useEffect(() => {
522
+ // Find skeleton
523
+ scene.traverse((child) => {
524
+ if (child.isSkinnedMesh) {
525
+ const skeleton = child.skeleton
526
+ const headBone = skeleton.bones.find(b => b.name === 'Head')
527
+ headBoneRef.current = headBone
528
+ }
529
+ })
530
+ }, [scene])
531
+
532
+ // Animate bone
533
+ useFrame(({ clock }) => {
534
+ if (headBoneRef.current) {
535
+ headBoneRef.current.rotation.y = Math.sin(clock.elapsedTime) * 0.3
536
+ }
537
+ })
538
+
539
+ return <primitive object={scene} />
540
+ }
541
+ ```
542
+
543
+ ### Bone Attachments
544
+
545
+ ```tsx
546
+ function CharacterWithWeapon() {
547
+ const { scene } = useGLTF('/models/character.glb')
548
+ const weaponRef = useRef()
549
+ const handBoneRef = useRef()
550
+
551
+ useEffect(() => {
552
+ scene.traverse((child) => {
553
+ if (child.isSkinnedMesh) {
554
+ const handBone = child.skeleton.bones.find(b => b.name === 'RightHand')
555
+ if (handBone && weaponRef.current) {
556
+ handBone.add(weaponRef.current)
557
+ handBoneRef.current = handBone
558
+ }
559
+ }
560
+ })
561
+
562
+ return () => {
563
+ // Cleanup
564
+ if (handBoneRef.current && weaponRef.current) {
565
+ handBoneRef.current.remove(weaponRef.current)
566
+ }
567
+ }
568
+ }, [scene])
569
+
570
+ return (
571
+ <>
572
+ <primitive object={scene} />
573
+ <mesh ref={weaponRef} position={[0, 0, 0.5]}>
574
+ <boxGeometry args={[0.1, 0.1, 1]} />
575
+ <meshStandardMaterial color="gray" />
576
+ </mesh>
577
+ </>
578
+ )
579
+ }
580
+ ```
581
+
582
+ ## Procedural Animation Patterns
583
+
584
+ ### Smooth Damping
585
+
586
+ ```tsx
587
+ import { useFrame } from '@react-three/fiber'
588
+ import { useRef } from 'react'
589
+ import * as THREE from 'three'
590
+
591
+ function SmoothFollow({ target }) {
592
+ const meshRef = useRef()
593
+ const currentPos = useRef(new THREE.Vector3())
594
+
595
+ useFrame((state, delta) => {
596
+ // Lerp towards target
597
+ currentPos.current.lerp(target, delta * 5)
598
+ meshRef.current.position.copy(currentPos.current)
599
+ })
600
+
601
+ return (
602
+ <mesh ref={meshRef}>
603
+ <sphereGeometry args={[0.5]} />
604
+ <meshStandardMaterial color="blue" />
605
+ </mesh>
606
+ )
607
+ }
608
+ ```
609
+
610
+ ### Spring Physics (Manual)
611
+
612
+ ```tsx
613
+ function SpringMesh({ target = 0 }) {
614
+ const meshRef = useRef()
615
+ const spring = useRef({
616
+ position: 0,
617
+ velocity: 0,
618
+ stiffness: 100,
619
+ damping: 10
620
+ })
621
+
622
+ useFrame((state, delta) => {
623
+ const s = spring.current
624
+ const force = -s.stiffness * (s.position - target)
625
+ const dampingForce = -s.damping * s.velocity
626
+
627
+ s.velocity += (force + dampingForce) * delta
628
+ s.position += s.velocity * delta
629
+
630
+ meshRef.current.position.y = s.position
631
+ })
632
+
633
+ return (
634
+ <mesh ref={meshRef}>
635
+ <boxGeometry />
636
+ <meshStandardMaterial color="green" />
637
+ </mesh>
638
+ )
639
+ }
640
+ ```
641
+
642
+ ### Oscillation Patterns
643
+
644
+ ```tsx
645
+ function OscillatingMesh() {
646
+ const meshRef = useRef()
647
+
648
+ useFrame(({ clock }) => {
649
+ const t = clock.elapsedTime
650
+
651
+ // Sine wave
652
+ meshRef.current.position.y = Math.sin(t * 2) * 0.5
653
+
654
+ // Circular motion
655
+ meshRef.current.position.x = Math.cos(t) * 2
656
+ meshRef.current.position.z = Math.sin(t) * 2
657
+
658
+ // Bouncing
659
+ meshRef.current.position.y = Math.abs(Math.sin(t * 3)) * 2
660
+
661
+ // Figure 8
662
+ meshRef.current.position.x = Math.sin(t) * 2
663
+ meshRef.current.position.z = Math.sin(t * 2) * 1
664
+ })
665
+
666
+ return (
667
+ <mesh ref={meshRef}>
668
+ <sphereGeometry args={[0.3]} />
669
+ <meshStandardMaterial color="purple" />
670
+ </mesh>
671
+ )
672
+ }
673
+ ```
674
+
675
+ ## Drei Animation Helpers
676
+
677
+ ### Float
678
+
679
+ ```tsx
680
+ import { Float } from '@react-three/drei'
681
+
682
+ function FloatingObject() {
683
+ return (
684
+ <Float
685
+ speed={1} // Animation speed
686
+ rotationIntensity={1} // Rotation intensity
687
+ floatIntensity={1} // Float intensity
688
+ floatingRange={[-0.1, 0.1]} // Range of y-axis float
689
+ >
690
+ <mesh>
691
+ <boxGeometry />
692
+ <meshStandardMaterial color="gold" />
693
+ </mesh>
694
+ </Float>
695
+ )
696
+ }
697
+ ```
698
+
699
+ ### MeshWobbleMaterial / MeshDistortMaterial
700
+
701
+ ```tsx
702
+ import { MeshWobbleMaterial, MeshDistortMaterial } from '@react-three/drei'
703
+
704
+ function WobblyMesh() {
705
+ return (
706
+ <mesh>
707
+ <torusKnotGeometry args={[1, 0.4, 100, 16]} />
708
+ <MeshWobbleMaterial
709
+ factor={1} // Wobble amplitude
710
+ speed={2} // Wobble speed
711
+ color="hotpink"
712
+ />
713
+ </mesh>
714
+ )
715
+ }
716
+
717
+ function DistortedMesh() {
718
+ return (
719
+ <mesh>
720
+ <sphereGeometry args={[1, 64, 64]} />
721
+ <MeshDistortMaterial
722
+ distort={0.5} // Distortion amount
723
+ speed={2} // Animation speed
724
+ color="cyan"
725
+ />
726
+ </mesh>
727
+ )
728
+ }
729
+ ```
730
+
731
+ ### Trail
732
+
733
+ ```tsx
734
+ import { Trail } from '@react-three/drei'
735
+ import { useFrame } from '@react-three/fiber'
736
+ import { useRef } from 'react'
737
+
738
+ function TrailingMesh() {
739
+ const meshRef = useRef()
740
+
741
+ useFrame(({ clock }) => {
742
+ const t = clock.elapsedTime
743
+ meshRef.current.position.x = Math.sin(t) * 3
744
+ meshRef.current.position.y = Math.cos(t * 2) * 2
745
+ })
746
+
747
+ return (
748
+ <Trail
749
+ width={2}
750
+ length={8}
751
+ color="hotpink"
752
+ attenuation={(t) => t * t}
753
+ >
754
+ <mesh ref={meshRef}>
755
+ <sphereGeometry args={[0.2]} />
756
+ <meshStandardMaterial color="white" />
757
+ </mesh>
758
+ </Trail>
759
+ )
760
+ }
761
+ ```
762
+
763
+ ## Animation with Zustand State
764
+
765
+ ```tsx
766
+ import { create } from 'zustand'
767
+ import { useFrame } from '@react-three/fiber'
768
+
769
+ const useStore = create((set) => ({
770
+ isAnimating: false,
771
+ speed: 1,
772
+ toggleAnimation: () => set((state) => ({ isAnimating: !state.isAnimating })),
773
+ setSpeed: (speed) => set({ speed })
774
+ }))
775
+
776
+ function AnimatedMesh() {
777
+ const meshRef = useRef()
778
+ const { isAnimating, speed } = useStore()
779
+
780
+ useFrame((state, delta) => {
781
+ if (isAnimating) {
782
+ meshRef.current.rotation.y += delta * speed
783
+ }
784
+ })
785
+
786
+ return (
787
+ <mesh ref={meshRef}>
788
+ <boxGeometry />
789
+ <meshStandardMaterial color="orange" />
790
+ </mesh>
791
+ )
792
+ }
793
+
794
+ // UI Component
795
+ function Controls() {
796
+ const { toggleAnimation, setSpeed } = useStore()
797
+
798
+ return (
799
+ <div>
800
+ <button onClick={toggleAnimation}>Toggle</button>
801
+ <input
802
+ type="range"
803
+ min="0"
804
+ max="5"
805
+ step="0.1"
806
+ onChange={(e) => setSpeed(parseFloat(e.target.value))}
807
+ />
808
+ </div>
809
+ )
810
+ }
811
+ ```
812
+
813
+ ## State Management Performance
814
+
815
+ Critical patterns for high-performance state management in animations.
816
+
817
+ ### getState() in useFrame
818
+
819
+ Use `getState()` instead of hooks inside useFrame for zero subscription overhead:
820
+
821
+ ```tsx
822
+ import { create } from 'zustand'
823
+
824
+ const useGameStore = create((set) => ({
825
+ playerPosition: [0, 0, 0],
826
+ targetPosition: [0, 0, 0],
827
+ setPlayerPosition: (pos) => set({ playerPosition: pos }),
828
+ }))
829
+
830
+ function Player() {
831
+ const meshRef = useRef()
832
+
833
+ useFrame((state, delta) => {
834
+ // ✅ GOOD: getState() has no subscription overhead
835
+ const { targetPosition } = useGameStore.getState()
836
+
837
+ // Lerp towards target
838
+ meshRef.current.position.lerp(
839
+ new THREE.Vector3(...targetPosition),
840
+ delta * 5
841
+ )
842
+ })
843
+
844
+ return (
845
+ <mesh ref={meshRef}>
846
+ <boxGeometry />
847
+ <meshStandardMaterial color="blue" />
848
+ </mesh>
849
+ )
850
+ }
851
+ ```
852
+
853
+ ### Transient Subscriptions
854
+
855
+ Subscribe to state changes without triggering React re-renders:
856
+
857
+ ```tsx
858
+ import { useEffect, useRef } from 'react'
859
+
860
+ function Enemy() {
861
+ const meshRef = useRef()
862
+
863
+ useEffect(() => {
864
+ // Subscribe directly - updates mesh without re-rendering component
865
+ const unsub = useGameStore.subscribe(
866
+ (state) => state.playerPosition,
867
+ (playerPos) => {
868
+ // Look at player (runs on every state change, no re-render)
869
+ meshRef.current.lookAt(...playerPos)
870
+ }
871
+ )
872
+ return unsub
873
+ }, [])
874
+
875
+ return (
876
+ <mesh ref={meshRef}>
877
+ <coneGeometry args={[0.5, 1, 4]} />
878
+ <meshStandardMaterial color="red" />
879
+ </mesh>
880
+ )
881
+ }
882
+ ```
883
+
884
+ ### Selective Subscriptions with Shallow
885
+
886
+ Subscribe to multiple values efficiently:
887
+
888
+ ```tsx
889
+ import { shallow } from 'zustand/shallow'
890
+
891
+ function HUD() {
892
+ // Only re-renders when health OR score actually changes
893
+ const { health, score } = useGameStore(
894
+ (state) => ({ health: state.health, score: state.score }),
895
+ shallow
896
+ )
897
+
898
+ return (
899
+ <Html>
900
+ <div>Health: {health}</div>
901
+ <div>Score: {score}</div>
902
+ </Html>
903
+ )
904
+ }
905
+
906
+ // For single values, no shallow needed
907
+ const health = useGameStore((state) => state.health)
908
+ ```
909
+
910
+ ### Isolate Animated Components
911
+
912
+ Separate state-dependent UI from animated 3D objects:
913
+
914
+ ```tsx
915
+ // ❌ BAD: Parent re-renders cause animation jank
916
+ function BadPattern() {
917
+ const [score, setScore] = useState(0)
918
+ const meshRef = useRef()
919
+
920
+ useFrame((_, delta) => {
921
+ meshRef.current.rotation.y += delta // Affected by score re-renders
922
+ })
923
+
924
+ return (
925
+ <>
926
+ <mesh ref={meshRef}>...</mesh>
927
+ <ScoreDisplay score={score} />
928
+ </>
929
+ )
930
+ }
931
+
932
+ // ✅ GOOD: Isolated animation component
933
+ function GoodPattern() {
934
+ return (
935
+ <>
936
+ <AnimatedMesh /> {/* Never re-renders from score */}
937
+ <ScoreDisplay /> {/* Has its own state subscription */}
938
+ </>
939
+ )
940
+ }
941
+
942
+ function AnimatedMesh() {
943
+ const meshRef = useRef()
944
+
945
+ useFrame((_, delta) => {
946
+ meshRef.current.rotation.y += delta // Smooth, uninterrupted
947
+ })
948
+
949
+ return <mesh ref={meshRef}>...</mesh>
950
+ }
951
+
952
+ function ScoreDisplay() {
953
+ const score = useGameStore((state) => state.score)
954
+ return <Html><div>Score: {score}</div></Html>
955
+ }
956
+ ```
957
+
958
+ ## Performance Tips
959
+
960
+ 1. **Isolate animated components**: Only the animated mesh re-renders
961
+ 2. **Use refs over state**: Avoid React re-renders for animations
962
+ 3. **Throttle expensive calculations**: Use delta accumulation
963
+ 4. **Pause offscreen animations**: Check visibility
964
+ 5. **Share animation clips**: Same clip for multiple instances
965
+
966
+ ```tsx
967
+ // Isolate animation to prevent parent re-renders
968
+ function Scene() {
969
+ return (
970
+ <>
971
+ <StaticMesh /> {/* Never re-renders */}
972
+ <AnimatedMesh /> {/* Only this updates */}
973
+ </>
974
+ )
975
+ }
976
+
977
+ // Throttle expensive operations
978
+ function ThrottledAnimation() {
979
+ const meshRef = useRef()
980
+ const accumulated = useRef(0)
981
+
982
+ useFrame((state, delta) => {
983
+ accumulated.current += delta
984
+
985
+ // Only update every 100ms
986
+ if (accumulated.current > 0.1) {
987
+ // Expensive calculation here
988
+ accumulated.current = 0
989
+ }
990
+
991
+ // Cheap operations every frame
992
+ meshRef.current.rotation.y += delta
993
+ })
994
+ }
995
+ ```
996
+
997
+ ## See Also
998
+
999
+ - `r3f-loaders` - Loading animated GLTF models
1000
+ - `r3f-fundamentals` - useFrame and animation loop
1001
+ - `r3f-shaders` - Vertex animation in shaders