Threejs Animation

calesthio/OpenMontage/.claude/skills/threejs-animation

by calesthio9327439db69021ab4b0e2776729bf3b58fdb5a87No license65K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 5 days ago

Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.

Instructions onlySoftware Development
AI-generated overview

Reference guide for Three.js animation: keyframe tracks, mixers, actions, skeletal and morph animation, blending.

What it does
This skill is an instructional reference for animating objects in Three.js. It documents the animation system's components (AnimationClip, AnimationMixer, AnimationAction), keyframe track types and interpolation modes, loading and playing GLTF clips, skeletal and bone animation, morph targets, animation blending and additive blending, and procedural motion patterns such as damping, springs and oscillation. It also lists performance tips. It produces guidance and code examples rather than files or scripts.
When to use it
Use it when writing Three.js code that animates objects, plays or blends GLTF animations, drives skeletons or morph targets, or needs procedural motion patterns. It suits developers looking up API usage and blending approaches while implementing animation in a web 3D scene.
Requirements
Requires a Three.js project environment (the three package, and GLTFLoader from three/examples for GLTF animations). It ships no scripts; it is instructions and code examples only.

Three.js Animation

Quick Start

javascript
import * as THREE from "three";
// Simple procedural animationconst clock = new THREE.Clock();
function animate() {  const delta = clock.getDelta();  const elapsed = clock.getElapsedTime();
  mesh.rotation.y += delta;  mesh.position.y = Math.sin(elapsed) * 0.5;
  requestAnimationFrame(animate);  renderer.render(scene, camera);}animate();

Animation System Overview

Three.js animation system has three main components:

  1. AnimationClip - Container for keyframe data
  2. AnimationMixer - Plays animations on a root object
  3. AnimationAction - Controls playback of a clip

AnimationClip

Stores keyframe animation data.

javascript
// Create animation clipconst times = [0, 1, 2]; // Keyframe times (seconds)const values = [0, 1, 0]; // Values at each keyframe
const track = new THREE.NumberKeyframeTrack(  ".position[y]", // Property path  times,  values,);
const clip = new THREE.AnimationClip("bounce", 2, [track]);

KeyframeTrack Types

javascript
// Number track (single value)new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);
// Vector track (position, scale)new THREE.VectorKeyframeTrack(".position", times, [  0,  0,  0, // t=0  1,  2,  0, // t=1  0,  0,  0, // t=2]);
// Quaternion track (rotation)const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));new THREE.QuaternionKeyframeTrack(  ".quaternion",  [0, 1],  [q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w],);
// Color tracknew THREE.ColorKeyframeTrack(".material.color", times, [  1,  0,  0, // red  0,  1,  0, // green  0,  0,  1, // blue]);
// Boolean tracknew THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);
// String track (for morph targets)new THREE.StringKeyframeTrack(  ".morphTargetInfluences[smile]",  [0, 1],  ["0", "1"],);

Interpolation Modes

javascript
const track = new THREE.VectorKeyframeTrack(".position", times, values);
// Interpolationtrack.setInterpolation(THREE.InterpolateLinear); // Defaulttrack.setInterpolation(THREE.InterpolateSmooth); // Cubic splinetrack.setInterpolation(THREE.InterpolateDiscrete); // Step function

AnimationMixer

Plays animations on an object and its descendants.

javascript
const mixer = new THREE.AnimationMixer(model);
// Create action from clipconst action = mixer.clipAction(clip);action.play();
// Update in animation loopfunction animate() {  const delta = clock.getDelta();  mixer.update(delta); // Required!
  requestAnimationFrame(animate);  renderer.render(scene, camera);}

Mixer Events

javascript
mixer.addEventListener("finished", (e) => {  console.log("Animation finished:", e.action.getClip().name);});
mixer.addEventListener("loop", (e) => {  console.log("Animation looped:", e.action.getClip().name);});

AnimationAction

Controls playback of an animation clip.

javascript
const action = mixer.clipAction(clip);
// Playback controlaction.play();action.stop();action.reset();action.halt(fadeOutDuration);
// Playback stateaction.isRunning();action.isScheduled();
// Time controlaction.time = 0.5; // Current timeaction.timeScale = 1; // Playback speed (negative = reverse)action.paused = false;
// Weight (for blending)action.weight = 1; // 0-1, contribution to final poseaction.setEffectiveWeight(1);
// Loop modesaction.loop = THREE.LoopRepeat; // Default: loop foreveraction.loop = THREE.LoopOnce; // Play once and stopaction.loop = THREE.LoopPingPong; // Alternate forward/backwardaction.repetitions = 3; // Number of loops (Infinity default)
// Clampingaction.clampWhenFinished = true; // Hold last frame when done
// Blendingaction.blendMode = THREE.NormalAnimationBlendMode;action.blendMode = THREE.AdditiveAnimationBlendMode;

Fade In/Out

javascript
// Fade inaction.reset().fadeIn(0.5).play();
// Fade outaction.fadeOut(0.5);
// Crossfade between animationsconst action1 = mixer.clipAction(clip1);const action2 = mixer.clipAction(clip2);
action1.play();
// Later, crossfade to action2action1.crossFadeTo(action2, 0.5, true);action2.play();

Loading GLTF Animations

Most common source of skeletal animations.

javascript
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const loader = new GLTFLoader();loader.load("model.glb", (gltf) => {  const model = gltf.scene;  scene.add(model);
  // Create mixer  const mixer = new THREE.AnimationMixer(model);
  // Get all clips  const clips = gltf.animations;  console.log(    "Available animations:",    clips.map((c) => c.name),  );
  // Play first animation  if (clips.length > 0) {    const action = mixer.clipAction(clips[0]);    action.play();  }
  // Play specific animation by name  const walkClip = THREE.AnimationClip.findByName(clips, "Walk");  if (walkClip) {    mixer.clipAction(walkClip).play();  }
  // Store mixer for update loop  window.mixer = mixer;});
// Animation loopfunction animate() {  const delta = clock.getDelta();  if (window.mixer) window.mixer.update(delta);
  requestAnimationFrame(animate);  renderer.render(scene, camera);}

Skeletal Animation

Skeleton and Bones

javascript
// Access skeleton from skinned meshconst skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");const skeleton = skinnedMesh.skeleton;
// Access bonesskeleton.bones.forEach((bone) => {  console.log(bone.name, bone.position, bone.rotation);});
// Find specific bone by nameconst headBone = skeleton.bones.find((b) => b.name === "Head");if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head
// Skeleton helperconst helper = new THREE.SkeletonHelper(model);scene.add(helper);

Programmatic Bone Animation

javascript
function animate() {  const time = clock.getElapsedTime();
  // Animate bone  const headBone = skeleton.bones.find((b) => b.name === "Head");  if (headBone) {    headBone.rotation.y = Math.sin(time) * 0.3;  }
  // Update mixer if also playing clips  mixer.update(clock.getDelta());}

Bone Attachments

javascript
// Attach object to boneconst weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);const handBone = skeleton.bones.find((b) => b.name === "RightHand");if (handBone) handBone.add(weapon);
// Offset attachmentweapon.position.set(0, 0, 0.5);weapon.rotation.set(0, Math.PI / 2, 0);

Morph Targets

Blend between different mesh shapes.

javascript
// Morph targets are stored in geometryconst geometry = mesh.geometry;console.log("Morph attributes:", Object.keys(geometry.morphAttributes));
// Access morph target influencesmesh.morphTargetInfluences; // Array of weightsmesh.morphTargetDictionary; // Name -> index mapping
// Set morph target by indexmesh.morphTargetInfluences[0] = 0.5;
// Set by nameconst smileIndex = mesh.morphTargetDictionary["smile"];mesh.morphTargetInfluences[smileIndex] = 1;

Animating Morph Targets

javascript
// Proceduralfunction animate() {  const t = clock.getElapsedTime();  mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;}
// With keyframe animationconst track = new THREE.NumberKeyframeTrack(  ".morphTargetInfluences[smile]",  [0, 0.5, 1],  [0, 1, 0],);const clip = new THREE.AnimationClip("smile", 1, [track]);mixer.clipAction(clip).play();

Animation Blending

Mix multiple animations together.

javascript
// Setup actionsconst idleAction = mixer.clipAction(idleClip);const walkAction = mixer.clipAction(walkClip);const runAction = mixer.clipAction(runClip);
// Play all with different weightsidleAction.play();walkAction.play();runAction.play();
// Set initial weightsidleAction.setEffectiveWeight(1);walkAction.setEffectiveWeight(0);runAction.setEffectiveWeight(0);
// Blend based on speedfunction updateAnimations(speed) {  if (speed < 0.1) {    idleAction.setEffectiveWeight(1);    walkAction.setEffectiveWeight(0);    runAction.setEffectiveWeight(0);  } else if (speed < 5) {    const t = speed / 5;    idleAction.setEffectiveWeight(1 - t);    walkAction.setEffectiveWeight(t);    runAction.setEffectiveWeight(0);  } else {    const t = Math.min((speed - 5) / 5, 1);    idleAction.setEffectiveWeight(0);    walkAction.setEffectiveWeight(1 - t);    runAction.setEffectiveWeight(t);  }}

Additive Blending

javascript
// Base poseconst baseAction = mixer.clipAction(baseClip);baseAction.play();
// Additive layer (e.g., breathing)const additiveAction = mixer.clipAction(additiveClip);additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;additiveAction.play();
// Convert clip to additiveTHREE.AnimationUtils.makeClipAdditive(additiveClip);

Animation Utilities

javascript
import * as THREE from "three";
// Find clip by nameconst clip = THREE.AnimationClip.findByName(clips, "Walk");
// Create subclipconst subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);
// Convert to additiveTHREE.AnimationUtils.makeClipAdditive(clip);THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
// Clone clipconst clone = clip.clone();
// Get clip durationclip.duration;
// Optimize clip (remove redundant keyframes)clip.optimize();
// Reset clip to first frameclip.resetDuration();

Procedural Animation Patterns

Smooth Damping

javascript
// Smooth follow/lerpconst target = new THREE.Vector3();const current = new THREE.Vector3();const velocity = new THREE.Vector3();
function smoothDamp(current, target, velocity, smoothTime, deltaTime) {  const omega = 2 / smoothTime;  const x = omega * deltaTime;  const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);  const change = current.clone().sub(target);  const temp = velocity    .clone()    .add(change.clone().multiplyScalar(omega))    .multiplyScalar(deltaTime);  velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp);  return target.clone().add(change.add(temp).multiplyScalar(exp));}
function animate() {  current.copy(smoothDamp(current, target, velocity, 0.3, delta));  mesh.position.copy(current);}

Spring Physics

javascript
class Spring {  constructor(stiffness = 100, damping = 10) {    this.stiffness = stiffness;    this.damping = damping;    this.position = 0;    this.velocity = 0;    this.target = 0;  }
  update(dt) {    const force = -this.stiffness * (this.position - this.target);    const dampingForce = -this.damping * this.velocity;    this.velocity += (force + dampingForce) * dt;    this.position += this.velocity * dt;    return this.position;  }}
const spring = new Spring(100, 10);spring.target = 1;
function animate() {  mesh.position.y = spring.update(delta);}

Oscillation

javascript
function animate() {  const t = clock.getElapsedTime();
  // Sine wave  mesh.position.y = Math.sin(t * 2) * 0.5;
  // Bouncing  mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;
  // Circular motion  mesh.position.x = Math.cos(t) * 2;  mesh.position.z = Math.sin(t) * 2;
  // Figure 8  mesh.position.x = Math.sin(t) * 2;  mesh.position.z = Math.sin(t * 2) * 1;}

Performance Tips

  1. Share clips: Same AnimationClip can be used on multiple mixers
  2. Optimize clips: Call clip.optimize() to remove redundant keyframes
  3. Disable when off-screen: Stop mixer updates for invisible objects
  4. Use LOD for animations: Simpler rigs for distant characters
  5. Limit active mixers: Each mixer.update() has a cost
javascript
// Pause animation when not visiblemesh.onBeforeRender = () => {  action.paused = false;};
mesh.onAfterRender = () => {  // Check if will be visible next frame  if (!isInFrustum(mesh)) {    action.paused = true;  }};
// Cache clipsconst clipCache = new Map();function getClip(name) {  if (!clipCache.has(name)) {    clipCache.set(name, loadClip(name));  }  return clipCache.get(name);}

See Also

  • threejs-loaders - Loading animated GLTF models
  • threejs-fundamentals - Clock and animation loop
  • threejs-shaders - Vertex animation in shaders

Source and attribution

Source:calesthio/OpenMontagein.claude/skills/threejs-animationat commit9327439

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal