Skip to main content
Technology & EngineeringAnimation Services136 lines

Three.js Animations

3D animation with Three.js — scene setup, AnimationMixer, keyframe tracks, morph targets, skeletal animation, clock-based loops, and integration with requestAnimationFrame.

Quick Summary24 lines
You are an expert in Three.js for creating 3D animations in the browser using WebGL.

## Key Points

- Always use `clock.getDelta()` for `mixer.update()` to keep animations frame-rate-independent.
- Dispose of geometries, materials, and textures when removing objects to prevent GPU memory leaks.
- Use `renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))` to cap resolution on high-DPI screens for performance.
- Forgetting to call `mixer.update(delta)` inside the render loop causes animations to appear frozen.
- Calling `clock.getDelta()` more than once per frame returns near-zero on the second call, breaking animation timing for multiple mixers. Call it once and pass the value to all mixers.

## Quick Example

```bash
npm install three
npm install -D @types/three   # TypeScript support
```

```js
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
```
skilldb get animation-services-skills/Three.js AnimationsFull skill: 136 lines
Paste into your CLAUDE.md or agent config

Three.js Animations — Web Animation

You are an expert in Three.js for creating 3D animations in the browser using WebGL.

Core Philosophy

Overview

Three.js is the dominant JavaScript library for 3D graphics on the web. Its animation system revolves around AnimationMixer, AnimationClip, and KeyframeTrack objects that drive property changes over time. Animations can target position, rotation, scale, morph targets, and skeletal bones. The render loop is driven by requestAnimationFrame with a THREE.Clock providing delta-time for frame-rate-independent playback.

Setup & Configuration

npm install three
npm install -D @types/three   # TypeScript support
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

Minimal scene with an animation loop:

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  // update animations here
  renderer.render(scene, camera);
}
animate();

Core Patterns

Keyframe Animation

const times = [0, 1, 2];
const values = [0, 0, 0, 5, 0, 0, 0, 0, 0]; // x,y,z at each keyframe

const positionTrack = new THREE.VectorKeyframeTrack(".position", times, values);
const clip = new THREE.AnimationClip("move", 2, [positionTrack]);

const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();

// In render loop:
mixer.update(delta);

Loading Animated glTF Models

const loader = new GLTFLoader();
loader.load("/model.glb", (gltf) => {
  scene.add(gltf.scene);

  const mixer = new THREE.AnimationMixer(gltf.scene);
  gltf.animations.forEach((clip) => {
    mixer.clipAction(clip).play();
  });

  // Store mixer for update loop
  mixers.push(mixer);
});

Morph Target Animation

const morphTrack = new THREE.NumberKeyframeTrack(
  ".morphTargetInfluences[0]",
  [0, 0.5, 1],
  [0, 1, 0]
);
const clip = new THREE.AnimationClip("morph", 1, [morphTrack]);
mixer.clipAction(clip).play();

Controlling Playback

const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopRepeat, Infinity);
action.timeScale = 1.5;          // speed up
action.clampWhenFinished = true;  // hold last frame
action.fadeIn(0.3);               // blend in over 0.3s

// Cross-fade between two actions
previousAction.fadeOut(0.5);
nextAction.reset().fadeIn(0.5).play();

Best Practices

  • Always use clock.getDelta() for mixer.update() to keep animations frame-rate-independent.
  • Dispose of geometries, materials, and textures when removing objects to prevent GPU memory leaks.
  • Use renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) to cap resolution on high-DPI screens for performance.

Common Pitfalls

  • Forgetting to call mixer.update(delta) inside the render loop causes animations to appear frozen.
  • Calling clock.getDelta() more than once per frame returns near-zero on the second call, breaking animation timing for multiple mixers. Call it once and pass the value to all mixers.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add animation-services-skills

Get CLI access →