threejs-animation

Keyframe, skeletal, and morph target animation system for Three.js objects and models. Supports four animation track types: number, vector, quaternion, and color keyframes with linear, smooth, and discrete interpolation modes AnimationMixer plays clips on objects with full playback control: play, stop, pause, speed adjustment, weight-based blending, and fade in/out transitions Skeletal animation via bone manipulation, bone attachments, and GLTF model loading with automatic clip discovery and playback Morph target blending for shape deformation, plus additive animation blending for layering effects like breathing over base poses Procedural animation patterns including smooth damping, spring physics, and oscillation helpers for custom motion without keyframes

INSTALLATION
npx skills add https://github.com/cloudai-x/threejs-skills --skill threejs-animation
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

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

- **Share clips**: Same AnimationClip can be used on multiple mixers

- **Optimize clips**: Call `clip.optimize()` to remove redundant keyframes

- **Disable when off-screen**: Stop mixer updates for invisible objects

- **Use LOD for animations**: Simpler rigs for distant characters

- **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);

}

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card