Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.
Install with Tessl CLI
npx tessl i github:cloudai-x/threejs-skills --skill threejs-animation92
Does it follow best practices?
Validation for skill structure
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();Three.js animation system has three main components:
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]);// 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"],
);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 functionPlays 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.addEventListener("finished", (e) => {
console.log("Animation finished:", e.action.getClip().name);
});
mixer.addEventListener("loop", (e) => {
console.log("Animation looped:", e.action.getClip().name);
});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
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();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);
}// 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);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());
}// 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);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;// 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();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);
}
}// 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);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();// 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);
}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);
}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;
}clip.optimize() to remove redundant keyframes// 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);
}threejs-loaders - Loading animated GLTF modelsthreejs-fundamentals - Clock and animation loopthreejs-shaders - Vertex animation in shadersb1c6230
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.