Three.js Cheatsheet

Animation

Use this Three.js reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

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.