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

PropertyTypeDefaultDescription
action.loopLoopModeLoopRepeatLoopOnce, LoopRepeat, LoopPingPong
action.repetitionsnumberInfinityTimes to repeat; use with LoopOnce
action.clampWhenFinishedbooleanfalseHold last frame when done
action.weightnumber1Blend weight (0–1)
action.timeScalenumber1Playback speed; -1 = reverse
action.timenumber0Current playback time (seconds)
action.enabledbooleantrueEnable/disable without stopping
action.pausedbooleanfalsePause 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 ClassPropertyValue 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, ...]
StringKeyframeTrackcustom[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);

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. Use action.paused = true to pause while keeping time position.

When crossfading, both actions must be playing — reset and play the incoming action before calling crossFadeFrom.

LoopOnce actions stop and hold the last frame only if clampWhenFinished = true. Otherwise they reset to the first frame.

Morph targets require morphTargets: true on the material (standard and physical materials support this). There's also a morphNormals: true for correct lighting.

Multiple AnimationMixer instances 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.