Three.js Cheatsheet
Animation
Use this Three.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
AnimationMixer
The AnimationMixer is the central controller. One mixer per root object (or skeleton root).
const mixer = new THREE.AnimationMixer(model); // model = THREE.Object3D // Advance in render loop const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); mixer.update(delta); // REQUIRED every frame renderer.render(scene, camera); } animate();
AnimationClip
// From GLTF const clips = gltf.animations; // Array<AnimationClip> // Find by name const runClip = THREE.AnimationClip.findByName(clips, 'Run'); // Create manually const track = new THREE.VectorKeyframeTrack( '.position', // property path [0, 1, 2], // time keys (seconds) [0,0,0, 0,3,0, 0,0,0] // values (3 per key for Vector3) ); const clip = new THREE.AnimationClip('Bounce', 2, [track]);
AnimationAction
const action = mixer.clipAction(clip); action.play(); action.pause(); action.stop(); action.reset(); // reset to time 0 action.reset().play(); // restart from beginning // Check state action.isRunning(); action.isPaused();
AnimationAction Properties
| Property | Type | Default | Description |
|---|---|---|---|
action.loop | LoopMode | LoopRepeat | LoopOnce, LoopRepeat, LoopPingPong |
action.repetitions | number | Infinity | Times to repeat; use with LoopOnce |
action.clampWhenFinished | boolean | false | Hold last frame when done |
action.weight | number | 1 | Blend weight (0–1) |
action.timeScale | number | 1 | Playback speed; -1 = reverse |
action.time | number | 0 | Current playback time (seconds) |
action.enabled | boolean | true | Enable/disable without stopping |
action.paused | boolean | false | Pause flag |
Blending / Crossfading
// Fade between two actions const idleAction = mixer.clipAction(idleClip); const walkAction = mixer.clipAction(walkClip); idleAction.play(); function switchToWalk() { walkAction.reset(); walkAction.crossFadeFrom(idleAction, 0.5, true); // duration 0.5s, warp=true walkAction.play(); } // Manual weight control idleAction.weight = 1 - t; walkAction.weight = t; // 0 ≤ t ≤ 1 // Must call mixer.clipAction(clip).enabled = true for weight to matter
Keyframe Tracks
| Track Class | Property | Value Format |
|---|---|---|
VectorKeyframeTrack | .position, .scale | [x,y,z, x,y,z, ...] |
QuaternionKeyframeTrack | .quaternion | [x,y,z,w, ...] |
NumberKeyframeTrack | .material.opacity, .rotation.y | [v, v, ...] |
ColorKeyframeTrack | .material.color | [r,g,b, ...] |
BooleanKeyframeTrack | .visible | [bool, ...] |
StringKeyframeTrack | custom | [str, ...] |
// Position track: 0s at origin, 1s at (5,0,0), 2s back at origin const posTrack = new THREE.VectorKeyframeTrack( '.position', [0, 1, 2], [0, 0, 0, 5, 0, 0, 0, 0, 0] ); // Quaternion rotation track const euler = new THREE.Euler(0, Math.PI, 0); const quat = new THREE.Quaternion().setFromEuler(euler); const quatTrack = new THREE.QuaternionKeyframeTrack( '.quaternion', [0, 1], [0, 0, 0, 1, quat.x, quat.y, quat.z, quat.w] ); // Opacity fade track const opacityTrack = new THREE.NumberKeyframeTrack( '.material.opacity', [0, 1], [1, 0] ); const clip = new THREE.AnimationClip('MyAnim', 2, [posTrack, quatTrack, opacityTrack]);
Interpolation Modes
// Per-track interpolation track.setInterpolation(THREE.InterpolateLinear); // default track.setInterpolation(THREE.InterpolateSmooth); // cubic (Catmull-Rom) track.setInterpolation(THREE.InterpolateDiscrete); // step/snap
Mixer Events
mixer.addEventListener('finished', (event) => { console.log('Animation finished:', event.action.getClip().name); }); mixer.addEventListener('loop', (event) => { console.log('Loop iteration:', event.action.getClip().name); });
Morph Target Animation
// Geometry must have morphAttributes const geo = new THREE.SphereGeometry(1, 32, 32); // Create morph target (a squashed sphere) const morphGeo = new THREE.SphereGeometry(1, 32, 32); // (In practice, morph target arrays come from your 3D app / GLTF) geo.morphAttributes.position = [morphGeo.attributes.position]; const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ morphTargets: true, // REQUIRED })); // Set influence (0–1) mesh.morphTargetInfluences[0] = 0.5; // GLTF morph targets — same API gltf.scene.traverse((child) => { if (child.isMesh && child.morphTargetInfluences) { child.morphTargetInfluences[0] = 0.7; // 70% of first morph } });
Skinned Mesh / Skeleton
// Usually loaded from GLTF, but can be built manually const bones = []; const root = new THREE.Bone(); root.position.y = 0; bones.push(root); const hip = new THREE.Bone(); hip.position.y = 1; root.add(hip); bones.push(hip); const skeleton = new THREE.Skeleton(bones); const geo = /* your geometry with skinIndex + skinWeight attributes */; const mesh = new THREE.SkinnedMesh(geo, material); mesh.add(root); mesh.bind(skeleton); // After bind, animate by rotating bones root.rotation.z = Math.sin(t) * 0.5; // Skeleton helper const helper = new THREE.SkeletonHelper(mesh); scene.add(helper);
GSAP Integration (Popular Pattern)
import gsap from 'gsap'; // Tween a mesh position gsap.to(mesh.position, { x: 5, y: 2, duration: 1.5, ease: 'power2.inOut' }); // Tween rotation gsap.to(mesh.rotation, { y: Math.PI, duration: 2 }); // Sequence const tl = gsap.timeline(); tl.to(mesh.position, { y: 3, duration: 1 }) .to(mesh.rotation, { y: Math.PI, duration: 0.5 }) .to(mesh.position, { y: 0, duration: 1 }); // Tween material property gsap.to(mat, { opacity: 0, duration: 1, onStart: () => (mat.transparent = true) });
requestAnimationFrame Utilities
// Stop the loop let animId; function animate() { animId = requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); // To stop: cancelAnimationFrame(animId); // Throttle to 30 fps let lastTime = 0; function animate(time) { requestAnimationFrame(animate); if (time - lastTime < 1000 / 30) return; lastTime = time; renderer.render(scene, camera); }
Gotchas
mixer.update(delta)must receive seconds, not milliseconds.clock.getDelta()returns seconds;performance.now()returns ms — divide by 1000.
action.stop()also resets state. Useaction.paused = trueto pause while keeping time position.
When crossfading, both actions must be playing — reset and play the incoming action before calling
crossFadeFrom.
LoopOnceactions stop and hold the last frame only ifclampWhenFinished = true. Otherwise they reset to the first frame.
Morph targets require
morphTargets: trueon the material (standard and physical materials support this). There's also amorphNormals: truefor correct lighting.
Multiple
AnimationMixerinstances for the same model don't share state — each has independent playback. Use a single mixer per skeleton root.
Do not reuse an
AnimationClip's tracks — they are mutated by the mixer internally. Clone the clip if you need independent playback on multiple objects.