name: threejs-animation
description: Animates Three.js objects using AnimationMixer, AnimationClip, keyframe tracks, morph targets, skeletal animation, and GSAP integration. Use when the user asks about animations, AnimationMixer, play/pause animations, keyframes, GLTF animations, morph targets, bones, or GSAP with Three.js. Trigger keywords: animation, AnimationMixer, AnimationClip, keyframe, morph target, skeleton, bones, GSAP, tween, play animation.
Three.js Animation
AnimationMixer (GLTF animations)
const { scene: model, animations } = await loader.loadAsync("/model.glb");
scene.add(model);
const mixer = new THREE.AnimationMixer(model);
const clock = new THREE.Clock();
const clip = THREE.AnimationClip.findByName(animations, "walk");
const action = mixer.clipAction(clip);
action.play();
action.paused = false;
action.loop = THREE.LoopRepeat;
action.clampWhenFinished = true;
action.timeScale = 1.5;
action.weight = 1.0;
const runAction = mixer.clipAction(
THREE.AnimationClip.findByName(animations, "run"),
);
action.fadeOut(0.3);
runAction.reset().fadeIn(0.3).play();
mixer.addEventListener("finished", (e) => {
console.log("Animation finished:", e.action.getClip().name);
});
function animate() {
requestAnimationFrame(animate);
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
Keyframe Animation (programmatic)
const posKF = new THREE.VectorKeyframeTrack(
".position",
[0, 1, 2],
[0, 0, 0, 0, 2, 0, 0, 0, 0],
);
const rotKF = new THREE.QuaternionKeyframeTrack(
".quaternion",
[0, 1, 2],
[0, 0, 0, 1, 0, 0.707, 0, 0.707, 0, 0, 0, 1],
);
const scaleKF = new THREE.VectorKeyframeTrack(
".scale",
[0, 0.5, 1],
[1, 1, 1, 2, 2, 2, 1, 1, 1],
);
const colorKF = new THREE.ColorKeyframeTrack(
".material.color",
[0, 1, 2],
[1, 0, 0, 0, 1, 0, 0, 0, 1],
);
const clip = new THREE.AnimationClip("my-animation", 2, [
posKF,
rotKF,
scaleKF,
]);
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();
Morph Targets
const { scene: model } = await loader.loadAsync("/face.glb");
scene.add(model);
model.traverse((child) => {
if (child.isMesh && child.morphTargetInfluences) {
child.morphTargetInfluences[0] = 0.5;
const morphKF = new THREE.NumberKeyframeTrack(
".morphTargetInfluences[0]",
[0, 1, 2],
[0, 1, 0],
);
const clip = new THREE.AnimationClip("blink", 2, [morphKF]);
const mixer = new THREE.AnimationMixer(child);
mixer.clipAction(clip).play();
}
});
Skeletal Animation
const { scene: model, animations } = await loader.loadAsync("/character.glb");
const helper = new THREE.SkeletonHelper(model);
scene.add(helper);
model.traverse((child) => {
if (child.isSkinnedMesh) {
const skeleton = child.skeleton;
const spineBone = skeleton.getBoneByName("spine");
spineBone.rotation.z = Math.sin(elapsedTime) * 0.3;
}
});
GSAP Integration
import gsap from "gsap";
gsap.to(mesh.position, {
x: 3,
y: 1,
z: 0,
duration: 2,
ease: "power2.inOut",
onUpdate: () => {
},
});
gsap.to(mesh.rotation, {
y: Math.PI * 2,
duration: 3,
ease: "none",
repeat: -1,
});
gsap.to(mat.color, { r: 1, g: 0, b: 0, duration: 1 });
gsap.to(mat.uniforms.uProgress, {
value: 1,
duration: 2,
ease: "power1.inOut",
});
const tl = gsap.timeline();
tl.to(mesh.position, { y: 2, duration: 1 })
.to(mesh.rotation, { y: Math.PI, duration: 0.5 }, "-=0.3")
.to(mesh.scale, { x: 0.5, y: 0.5, z: 0.5, duration: 0.5 });
Common Gotchas
mixer.update(delta) is required every frame — animation doesn't advance otherwise
THREE.AnimationClip.findByName(animations, 'name') — name must exactly match the GLTF clip name
LoopOnce + clampWhenFinished = true to freeze on last frame
action.reset() before play() if replaying from start
- GSAP and AnimationMixer can conflict on the same property — pick one per property
morphTargetDictionary maps morph target names to indices — use child.morphTargetDictionary['smile'] to find index
- SkeletonHelper must be added to the scene root, not as a child of the model