Back to Creative & Visual

threejs-animation

three.js3d animationwebgljavascriptkeyframeskeletal animationmorph targetsgltf
2.4k🕒 2026-01-19Source ↗

Install this skill

npx skills add cloudai-x/threejs-skills

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

The Three.js animation system provides a structured architecture for creating complex motion within a 3D scene. It centers on three primary objects: AnimationClip, which acts as the data store for property changes over time; AnimationMixer, which serves as the playback controller for objects in the scene; and AnimationAction, which provides fine-grained control over playback settings like looping, crossfading, and time scaling. The system supports various data types including numeric values, quaternions for rotation, vector positions, and even morph target influences. By decoupling the animation data from the object itself, developers can manage multiple simultaneous animations, blend them together, or transition between clips with ease. This framework is particularly effective for handling skeletal animations exported from standard 3D software via GLTF models, ensuring consistent property updates throughout the render loop.

When to Use This Skill

  • Implementing character movement states like walk, run, or idle
  • Creating procedural UI component interactions or property transitions
  • Managing complex multi-part object sequences for 3D product showcases
  • Synchronizing multiple animations to play or fade simultaneously

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • how do I play an animation on my 3D model
  • set up a Three.js animation mixer for my GLTF scene
  • how to crossfade between two different animation clips
  • control playback speed and loop count in Three.js
  • create a keyframe track for object position and rotation

Pro Tips

  • 💡Optimize performance by baking complex animations into GLTF files and using `AnimationMixer` efficiently, especially for large scenes with many animated objects.
  • 💡Master the interplay between `AnimationClip`, `AnimationMixer`, and `AnimationAction` for granular control over animation playback speed, looping, and blending.
  • 💡Leverage procedural animation for subtle, reactive movements that don't require pre-rendered keyframes, often combined with `THREE.Clock` for delta time calculations.

What this skill does

  • Manage multi-track keyframe animation playback with AnimationMixer
  • Interpolate between property states using linear, smooth, or discrete modes
  • Crossfade between distinct animation states to create smooth transitions
  • Control skeletal animations imported directly from GLTF files
  • Manipulate playback speed, looping behavior, and weight blending for complex movement

When not to use it

  • Simple one-off object movements better suited for Tween.js or GSAP
  • Physics-based interactions where rigid body simulation is required

Example workflow

  1. Load the model file and extract the animation clips array
  2. Initialize an AnimationMixer targeting the model root
  3. Obtain an AnimationAction for the desired clip from the mixer
  4. Configure playback settings like loop mode or fade-in duration
  5. Execute action.play() to begin the sequence
  6. Call mixer.update(delta) inside the requestAnimationFrame loop

Prerequisites

  • Basic knowledge of the Three.js scene graph
  • Understanding of the render loop (requestAnimationFrame)
  • 3D model data containing keyframe information (e.g., GLTF)

Pitfalls & limitations

  • !Forgetting to call mixer.update(delta) in the render loop causes animations to freeze
  • !Incorrect property paths in KeyframeTrack strings prevent animations from applying
  • !Attempting to play animations before the model has fully loaded

FAQ

Why is my animation not playing?
Ensure you are calling mixer.update() with the elapsed frame delta inside your animation render loop.
Can I blend two animations together?
Yes, use the crossFadeTo() method on an AnimationAction to transition from one clip to another.
Does the system support non-skeletal objects?
Yes, you can animate any object property, such as material opacity or mesh scale, using custom KeyframeTracks.
How do I make an animation loop only three times?
Set the action.loop property to THREE.LoopRepeat and set the action.repetitions property to 3.

How it compares

Unlike manual property updates in a render loop, this system provides a dedicated event-driven infrastructure for state management, blending, and sequencing that scales with complex 3D project requirements.

Source & trust

2.4k stars🕒 Updated 2026-01-19
📄 Full skill instructions — original source: cloudai-x/threejs-skills
# Three.js Animation

## Quick Start

import * as THREE from "three";

// Simple procedural animation
const 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.

// Create animation clip
const 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

// 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 track
new THREE.ColorKeyframeTrack(".material.color", times, [
1,
0,
0, // red
0,
1,
0, // green
0,
0,
1, // blue
]);

// Boolean track
new 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

const track = new THREE.VectorKeyframeTrack(".position", times, values);

// Interpolation
track.setInterpolation(THREE.InterpolateLinear); // Default
track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline
track.setInterpolation(THREE.InterpolateDiscrete); // Step function


## AnimationMixer

Plays animations on an object and its descendants.

const mixer = new THREE.AnimationMixer(model);

// Create action from clip
const action = mixer.clipAction(clip);
action.play();

// Update in animation loop
function animate() {
const delta = clock.getDelta();
mixer.update(delta); // Required!

requestAnimationFrame(animate);
renderer.render(scene, camera);
}


### Mixer Events

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.

const action = mixer.clipAction(clip);

// Playback control
action.play();
action.stop();
action.reset();
action.halt(fadeOutDuration);

// Playback state
action.isRunning();
action.isScheduled();

// Time control
action.time = 0.5; // Current time
action.timeScale = 1; // Playback speed (negative = reverse)
action.paused = false;

// Weight (for blending)
action.weight = 1; // 0-1, contribution to final pose
action.setEffectiveWeight(1);

// Loop modes
action.loop = THREE.LoopRepeat; // Default: loop forever
action.loop = THREE.LoopOnce; // Play once and stop
action.loop = THREE.LoopPingPong; // Alternate forward/backward
action.repetitions = 3; // Number of loops (Infinity default)

// Clamping
action.clampWhenFinished = true; // Hold last frame when done

// Blending
action.blendMode = THREE.NormalAnimationBlendMode;
action.blendMode = THREE.AdditiveAnimationBlendMode;


### Fade In/Out

// Fade in
action.reset().fadeIn(0.5).play();

// Fade out
action.fadeOut(0.5);

// Crossfade between animations
const action1 = mixer.clipAction(clip1);
const action2 = mixer.clipAction(clip2);

action1.play();

// Later, crossfade to action2
action1.crossFadeTo(action2, 0.5, true);
action2.play();


## Loading GLTF Animations

Most common source of skeletal animations.

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 loop
function animate() {
const delta = clock.getDelta();
if (window.mixer) window.mixer.update(delta);

requestAnimationFrame(animate);
renderer.render(scene, camera);
}


## Skeletal Animation

### Skeleton and Bones

// Access skeleton from skinned mesh
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
const skeleton = skinnedMesh.skeleton;

// Access bones
skeleton.bones.forEach((bone) => {
console.log(bone.name, bone.position, bone.rotation);
});

// Find specific bone by name
const headBone = skeleton.bones.find((b) => b.name === "Head");
if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head

// Skeleton helper
const helper = new THREE.SkeletonHelper(model);
scene.add(helper);


### Programmatic Bone Animation

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

// Attach object to bone
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
if (handBone) handBone.add(weapon);

// Offset attachment
weapon.position.set(0, 0, 0.5);
weapon.rotation.set(0, Math.PI / 2, 0);


## Morph Targets

Blend between different mesh shapes.

// Morph targets are stored in geometry
const geometry = mesh.geometry;
console.log("Morph attributes:", Object.keys(geometry.morphAttributes));

// Access morph target influences
mesh.morphTargetInfluences; // Array of weights
mesh.morphTargetDictionary; // Name -> index mapping

// Set morph target by index
mesh.morphTargetInfluences[0] = 0.5;

// Set by name
const smileIndex = mesh.morphTargetDictionary["smile"];
mesh.morphTargetInfluences[smileIndex] = 1;


### Animating Morph Targets

// Procedural
function animate() {
const t = clock.getElapsedTime();
mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
}

// With keyframe animation
const 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.

// Setup actions
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);

// Play all with different weights
idleAction.play();
walkAction.play();
runAction.play();

// Set initial weights
idleAction.setEffectiveWeight(1);
walkAction.setEffectiveWeight(0);
runAction.setEffectiveWeight(0);

// Blend based on speed
function 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

// Base pose
const 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 additive
THREE.AnimationUtils.makeClipAdditive(additiveClip);


## Animation Utilities

import * as THREE from "three";

// Find clip by name
const clip = THREE.AnimationClip.findByName(clips, "Walk");

// Create subclip
const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);

// Convert to additive
THREE.AnimationUtils.makeClipAdditive(clip);
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);

// Clone clip
const clone = clip.clone();

// Get clip duration
clip.duration;

// Optimize clip (remove redundant keyframes)
clip.optimize();

// Reset clip to first frame
clip.resetDuration();


## Procedural Animation Patterns

### Smooth Damping

// Smooth follow/lerp
const 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

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

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

// Pause animation when not visible
mesh.onBeforeRender = () => {
action.paused = false;
};

mesh.onAfterRender = () => {
// Check if will be visible next frame
if (!isInFrustum(mesh)) {
action.paused = true;
}
};

// Cache clips
const 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

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/threejs-animation/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/cloudai-x/threejs-skills/threejs-animation/SKILL.md
  • Cursor: ~/.cursor/skills/cloudai-x/threejs-skills/threejs-animation/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/cloudai-x/threejs-skills/threejs-animation/SKILL.md

🚀 Install with CLI:
npx skills add cloudai-x/threejs-skills

Read the Master Guide: Mastering Agent Skills

Related Skill Units

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid creative & visual issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Creative & Visual and is published by CloudAI-X, maintained in cloudai-x/threejs-skills.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.