스킬 불러오는 중
스킬 불러오는 중
impertio-studio/three.js-claude-skill-package · GitHub
Use when playing animations, crossfading between animation states, or loading skeletal animations from GLTF in Three.js. Prevents the common mistake of not calling mixer.update(delta) every frame, wrong crossfade setup, or missing Clock. Covers AnimationMixer, AnimationClip, AnimationAction, KeyframeTrack, crossfade, blending. Keywords: animation, AnimationMixer, AnimationClip, AnimationAction, crossfade, skeletal, GLTF animation, keyframe, blend, Clock, camera animation, smooth camera, fly to, tween, GSAP.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add impertio-studio/three.js-claude-skill-package --skill threejs-impl-animation설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
.gitkeepreferences/anti-patterns.md# threejs-impl-animation — Anti-Patterns
> What NOT to do with the Three.js Animation System. Every entry includes the mistake, why it fails, and the correct approach.
---
## Anti-Pattern 1: Forgetting mixer.update(delta) in the Render Loop
**WRONG:**
```js
const mixer = new THREE.AnimationMixer(model);
mixer.clipAction(clip).play();
function animate() {
// Missing mixer.update(delta) -- animations will NEVER play
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
**WHY:** The mixer does NOT advance time automatically. Without `mixer.update(delta)`, all actions remain frozen at time 0.
**CORRECT:**
```js
const clock = new THREE.Clock();
const mixer = new THREE.AnimationMixer(model);
mixer.clipAction(clip).play();
function animate() {
const delta = clock.getDelta();
mixer.update(delta); // ALWAYS call every frame
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Anti-Pattern 2: Using a Fixed Delta Instead of Clock
**WRONG:**
```js
function animate() {
mixer.update(1 / 60); // Assumes constant 60fps -- breaks on slow devices
renderer.render(scene, camera);
}
```
**WHY:** Frame rates vary across devices and over time. A fixed delta causes animations to speed up or slow down unpredictably. On a 30fps device, animations play at half speed.
**CORRECT:**
```js
const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta(); // Actual time between frames
mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Anti-Pattern 3: Crossfading Without reset() on the Incoming Action
**WRONG:**
```js
function switchToWalk() {
walkAction.crossFadeFrom(idleAction, 0.5, true);
walkAction.play();
// walkAction may still have stale time/weight from a previous play
}
```
**WHY:** If the walk action was previously played and stopped, its internal state (time, weight, timeScale) retains old values. The crossfade starts from the wrong position or weight, producing jerky transitions.
**CORRECT:**
```js
function switchToWalk() {
walkAction.reset(); // ALWAYS reset before crossfading
walkAction.setEffectiveTimeScale(1);
walkAction.setEffectiveWeight(1);
walkAction.crossFadeFrom(idleAction, 0.5, true);
walkAction.play();
}
```
---
## Anti-Pattern 4: Using LoopOnce Without clampWhenFinished
**WRONG:**
```js
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopOnce, 1);
action.play();
// Animation snaps back to frame 0 after finishing
```
**WHY:** Without `clampWhenFinished = true`, the action resets to its initial state when it completes. The model visibly jumps back to the start pose, which looks broken for one-shot animations like door opens, attacks, or death animations.
**CORRECT:**
```js
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopOnce, 1);
action.clampWhenFinished = true; // ALWAYS set for LoopOnce
action.play();
```
---
## Anti-Pattern 5: Creating a New Mixer Every Frame
**WRONG:**
```js
function animate() {
const mixer = new THREE.AnimationMixer(model); // New mixer every frame!
mixer.clipAction(clip).play();
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
```
**WHY:** Creating a new mixer every frame discards all previous action state. Animations restart from frame 0 every frame, producing a frozen first-frame appearance. It also generates massive garbage collection pressure.
**CORRECT:**
```js
const mixer = new THREE.AnimationMixer(model); // Create ONCE
const action = mixer.clipAction(clip);
action.play();
function animate() {
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
```
---
## Anti-Pattern 6: Calling play() Every Frame
**WRONG:**
```js
const action = mixer.clipAction(clip);
function animate() {
action.play(); // Called every frame -- unnecessary
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
```
**WHY:** `.play()` is an activation method, not an update method. While calling it on an already-playing action is harmless (it returns immediately), doing so every frame signals a fundamental misunderstanding of the API. It can also interfere with fading and crossfade transitions by re-activating an action that is being faded out.
**CORRECT:**
```js
const action = mixer.clipAction(clip);
action.play(); // Call ONCE
function animate() {
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
```
---
## Anti-Pattern 7: Instantiating AnimationAction Directly
**WRONG:**
```js
const action = new THREE.AnimationAction(mixer, clip); // NEVER do this
```
**WHY:** `AnimationAction` is designed to be created and cached by the mixer. Direct instantiation bypasses the mixer's internal action cache, leading to duplicate actions, broken crossfades, and memory leaks.
**CORRECT:**
```js
const action = mixer.clipAction(clip); // ALWAYS use mixer.clipAction()
```
---
## Anti-Pattern 8: Forgetting to Dispose the Mixer on Model Removal
**WRONG:**
```js
scene.remove(model);
// Mixer still holds references, continues to call update, leaks memory
```
**WHY:** The mixer retains internal references to the model and all its actions. If the model is removed from the scene but the mixer is not cleaned up, it continues to consume memory and CPU during `mixer.update()`.
**CORRECT:**
```js
mixer.stopAllAction();
mixer.uncacheRoot(model);
scene.remove(model);
mixer = null; // Allow garbage collection
```
---
## Anti-Pattern 9: Using Multiple Mixers for the Same Model
**WRONG:**
```js
const mixer1 = new THREE.AnimationMixer(model);
const mixer2 = new THREE.AnimationMixer(model);
mixer1.clipAction(walkClip).play();
mixer2.clipAction(idleClip).play();
```
**WHY:** Multiple mixers on the same root object cause conflicting property writes. Both mixers attempt to set the same bone transforms every frame, producing jittering, twitching, or completely broken animation. Crossfade and blending between clips on different mixers is impossible.
**CORRECT:**
```js
const mixer = new THREE.AnimationMixer(model); // ONE mixer per model
mixer.clipAction(walkClip).play();
mixer.clipAction(idleClip).play();
// Use weight and crossfade to blend between them
```
---
## Anti-Pattern 10: Animating material.opacity Without Setting transparent=true
**WRONG:**
```js
const track = new THREE.NumberKeyframeTrack(
'Mesh.material.opacity',
[0, 1],
[1, 0]
);
// Opacity changes but object remains fully visible
```
**WHY:** Three.js materials require `material.transparent = true` for opacity values below 1 to have any visual effect. Without this flag, the material ignores the opacity property during rendering.
**CORRECT:**
```js
material.transparent = true; // MUST set before animating opacity
const track = new THREE.NumberKeyframeTrack(
'Mesh.material.opacity',
[0, 1],
[1, 0]
);
```
---
## Anti-Pattern 11: Using getDelta() Multiple Times per Frame
**WRONG:**
```js
function animate() {
mixer1.update(clock.getDelta()); // Returns actual delta
mixer2.update(clock.getDelta()); // Returns ~0 (called immediately after)
renderer.render(scene, camera);
}
```
**WHY:** `clock.getDelta()` returns the time since the LAST call to `getDelta()`. The second call within the same frame returns approximately zero, effectively freezing the second mixer.
**CORRECT:**
```js
function animate() {
const delta = clock.getDelta(); // Call ONCE per frame
mixer1.update(delta);
mixer2.update(delta); // Same delta for both
renderer.render(scene, camera);
}
```
references/examples.md# threejs-impl-animation — Examples
> Working code examples for the Three.js Animation System (r160+).
---
## Example 1: Load and Play All GLTF Animations
The most common animation workflow: load a GLTF model and play all embedded clips.
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1.5, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// Play every animation clip from the GLTF file
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Example 2: Play a Single Named Clip
Select and play a specific animation by name from the GLTF file.
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// Find a specific clip by name
const idleClip = THREE.AnimationClip.findByName(gltf.animations, 'Idle');
if (idleClip) {
const action = mixer.clipAction(idleClip);
action.play();
}
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Example 3: Crossfade Character State Machine
Smooth transitions between idle, walk, and run states using crossfade.
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const actions = {};
let currentAction;
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// Cache all actions by clip name
gltf.animations.forEach((clip) => {
actions[clip.name] = mixer.clipAction(clip);
});
// Start with idle
currentAction = actions['Idle'];
currentAction.play();
});
function switchAction(toName, duration = 0.5) {
if (!actions[toName] || actions[toName] === currentAction) return;
const toAction = actions[toName];
// ALWAYS reset the incoming action before crossfading
toAction.reset();
toAction.setEffectiveTimeScale(1);
toAction.setEffectiveWeight(1);
toAction.crossFadeFrom(currentAction, duration, true);
toAction.play();
currentAction = toAction;
}
// Usage: respond to input
document.addEventListener('keydown', (event) => {
switch (event.key) {
case 'w': switchAction('Walk'); break;
case 'r': switchAction('Run'); break;
case 'i': switchAction('Idle'); break;
}
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Example 4: Additive Animation Blending
Layer an additive animation (e.g., breathing or damage reaction) on top of a base animation.
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// Base animation: normal blending
const idleClip = THREE.AnimationClip.findByName(gltf.animations, 'Idle');
const baseAction = mixer.clipAction(idleClip);
baseAction.play();
// Additive animation: layered on top
const breatheClip = THREE.AnimationClip.findByName(gltf.animations, 'Breathe');
const additiveAction = mixer.clipAction(
breatheClip,
undefined,
THREE.AdditiveAnimationBlendMode
);
additiveAction.play();
additiveAction.setEffectiveWeight(0.5); // control blend strength
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Example 5: Play-Once Animation with Finished Event
Play an animation exactly once, clamp at the last frame, and detect completion.
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('door.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
const openClip = THREE.AnimationClip.findByName(gltf.animations, 'DoorOpen');
const action = mixer.clipAction(openClip);
// Configure play-once behavior
action.setLoop(THREE.LoopOnce, 1);
action.clampWhenFinished = true; // ALWAYS set for LoopOnce
// Listen for completion
mixer.addEventListener('finished', (event) => {
console.log('Animation finished:', event.action.getClip().name);
});
action.play();
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Example 6: Programmatic KeyframeTrack Animation
Create an animation entirely in code without loading a GLTF file.
```js
import * as THREE from 'three';
const clock = new THREE.Clock();
// Create a simple cube
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.name = 'MyCube';
scene.add(cube);
// Define keyframe tracks
const positionTrack = new THREE.VectorKeyframeTrack(
'MyCube.position', // PropertyBinding path
[0, 1, 2], // times in seconds
[0, 0, 0, 0, 2, 0, 0, 0, 0] // values: [x,y,z] at each time
);
const rotationTrack = new THREE.QuaternionKeyframeTrack(
'MyCube.quaternion',
[0, 1, 2],
[
0, 0, 0, 1, // identity at t=0
0, 0.707, 0, 0.707, // 90deg Y at t=1
0, 0, 0, 1 // identity at t=2
]
);
const opacityTrack = new THREE.NumberKeyframeTrack(
'MyCube.material.opacity',
[0, 1, 2],
[1, 0.3, 1]
);
// Create clip from tracks
const clip = new THREE.AnimationClip('BounceAndSpin', 2, [
positionTrack,
rotationTrack,
opacityTrack
]);
// Set up mixer and play
const mixer = new THREE.AnimationMixer(cube);
const action = mixer.clipAction(clip);
action.play();
// IMPORTANT: enable transparency for opacity animation
material.transparent = true;
function animate() {
const delta = clock.getDelta();
mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
---
## Example 7: Morph Target Animation
Animate morph targets (blend shapes) loaded from GLTF.
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('face.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// GLTF morph target animations are stored as regular clips
// They target mesh.morphTargetInfluences[index]
const smileClip = THREE.AnimationClip.findByName(gltf.animations, 'Smile');
if (smileClip) {
const action = mixer.clipAction(smileClip);
action.play();
}
});
// Manual morph target control (alternative to clip-based)
function manualMorphControl(mesh, elapsed) {
if (mesh.morphTargetInfluences) {
mesh.morphTargetInfluences[0] = Math.sin(elapsed) * 0.5 + 0.5;
}
}
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
references/methods.md# threejs-impl-animation — Method Reference
> Complete API signatures for the Three.js Animation System (r160+).
---
## AnimationMixer
### Constructor
```ts
new AnimationMixer(rootObject: Object3D): AnimationMixer
```
### Properties
```ts
mixer.time: number // Global mixer time in seconds (default: 0)
mixer.timeScale: number // Global speed multiplier (default: 1; 0 pauses all)
```
### Methods
```ts
mixer.clipAction(
clip: AnimationClip,
optionalRoot?: Object3D,
blendMode?: number
): AnimationAction
// Returns (or creates and caches) an AnimationAction for the given clip.
// blendMode: THREE.NormalAnimationBlendMode | THREE.AdditiveAnimationBlendMode
mixer.existingAction(
clip: AnimationClip,
optionalRoot?: Object3D
): AnimationAction | null
// Returns a previously created action or null.
mixer.update(deltaTimeInSeconds: number): AnimationMixer
// Advances the mixer. MUST call every frame with clock.getDelta().
mixer.setTime(timeInSeconds: number): AnimationMixer
// Sets the global mixer time and forces an update of all active actions.
mixer.stopAllAction(): AnimationMixer
// Deactivates all currently scheduled actions.
mixer.getRoot(): Object3D
// Returns the root object passed to the constructor.
mixer.uncacheAction(clip: AnimationClip, optionalRoot?: Object3D): void
// Deallocates the cached action for the given clip.
mixer.uncacheClip(clip: AnimationClip): void
// Deallocates all cached data for the given clip.
mixer.uncacheRoot(root: Object3D): void
// Deallocates all cached data for the given root object.
```
### Events
```ts
mixer.addEventListener('finished', (event: {
action: AnimationAction,
direction: number,
type: 'finished'
}) => void)
// Fires when an action with LoopOnce and clampWhenFinished=true completes.
mixer.addEventListener('loop', (event: {
action: AnimationAction,
loopDelta: number,
type: 'loop'
}) => void)
// Fires when an action completes a loop iteration.
```
---
## AnimationAction
### Constructor (Internal -- NEVER call directly)
```ts
new AnimationAction(
mixer: AnimationMixer,
clip: AnimationClip,
localRoot?: Object3D,
blendMode?: number
): AnimationAction
```
ALWAYS create via `mixer.clipAction(clip)`.
### Properties
```ts
action.blendMode: number // NormalAnimationBlendMode | AdditiveAnimationBlendMode
action.clampWhenFinished: boolean // default: false
action.enabled: boolean // default: true
action.loop: number // LoopOnce | LoopRepeat | LoopPingPong (default: LoopRepeat)
action.paused: boolean // default: false
action.repetitions: number // default: Infinity
action.time: number // local playback time in seconds (default: 0)
action.timeScale: number // speed multiplier (default: 1; 0=pause, negative=reverse)
action.weight: number // blend influence [0, 1] (default: 1)
action.zeroSlopeAtEnd: boolean // default: true
action.zeroSlopeAtStart: boolean // default: true
```
### Playback Methods
```ts
action.play(): AnimationAction
// Activates the action. Call ONCE to start playback.
action.stop(): AnimationAction
// Stops playback, resets time to 0, and deactivates the action.
action.reset(): AnimationAction
// Resets time=0, enabled=true, paused=false, timeScale=1, weight=1.
// Cancels any scheduled fading and warping. Does NOT deactivate.
action.startAt(time: number): AnimationAction
// Delays the start to the specified mixer time.
```
### Fading Methods
```ts
action.fadeIn(durationInSeconds: number): AnimationAction
// Fades weight from 0 to 1 over the specified duration.
action.fadeOut(durationInSeconds: number): AnimationAction
// Fades weight from current value to 0 over the specified duration.
action.crossFadeFrom(
fadeOutAction: AnimationAction,
durationInSeconds: number,
warpBoolean: boolean
): AnimationAction
// Crossfades from fadeOutAction into this action.
// If warp=true, also warps timeScale for smooth speed transition.
action.crossFadeTo(
fadeInAction: AnimationAction,
durationInSeconds: number,
warpBoolean: boolean
): AnimationAction
// Crossfades from this action to fadeInAction.
action.stopFading(): AnimationAction
// Cancels any currently active fade.
```
### Speed and Timing Methods
```ts
action.halt(durationInSeconds: number): AnimationAction
// Decelerates timeScale to 0 over the specified duration.
action.warp(
startTimeScale: number,
endTimeScale: number,
durationInSeconds: number
): AnimationAction
// Smoothly transitions playback speed from start to end over duration.
action.stopWarping(): AnimationAction
// Cancels any currently active warp.
action.setDuration(durationInSeconds: number): AnimationAction
// Sets timeScale so one loop takes exactly durationInSeconds.
action.setEffectiveTimeScale(timeScale: number): AnimationAction
// Sets effective time scale (considers paused state).
action.setEffectiveWeight(weight: number): AnimationAction
// Sets effective weight (considers enabled state).
action.setLoop(mode: number, repetitions: number): AnimationAction
// Sets loop mode and repetition count.
action.syncWith(otherAction: AnimationAction): AnimationAction
// Synchronizes this action's time with another action.
```
### Query Methods
```ts
action.isRunning(): boolean
// true only when action is actively playing (not paused, weight > 0, speed != 0).
action.isScheduled(): boolean
// true if .play() was called and action has not been stopped.
action.getClip(): AnimationClip
action.getMixer(): AnimationMixer
action.getRoot(): Object3D
action.getEffectiveTimeScale(): number
action.getEffectiveWeight(): number
```
---
## AnimationClip
### Constructor
```ts
new AnimationClip(
name?: string, // default: ''
duration?: number, // default: -1 (auto-calculate)
tracks?: KeyframeTrack[],
blendMode?: number
): AnimationClip
```
### Properties
```ts
clip.blendMode: number
clip.duration: number // length in seconds
clip.name: string // identifier
clip.tracks: KeyframeTrack[] // keyframe data
clip.userData: Object // custom metadata
clip.uuid: string // readonly unique ID
```
### Instance Methods
```ts
clip.clone(): AnimationClip
clip.optimize(): AnimationClip // removes redundant sequential keys
clip.resetDuration(): AnimationClip // updates duration to longest track
clip.toJSON(): Object
clip.trim(): AnimationClip // crops all tracks to clip duration
clip.validate(): boolean // returns true if all tracks are valid
```
### Static Methods
```ts
AnimationClip.findByName(
objectOrClipArray: Object3D | AnimationClip[],
name: string
): AnimationClip
// Finds a clip by name in an array or on an object's .animations property.
AnimationClip.CreateFromMorphTargetSequence(
name: string,
morphTargetSequence: MorphTarget[],
fps: number,
noLoop: boolean
): AnimationClip
AnimationClip.CreateClipsFromMorphTargetSequences(
morphTargets: MorphTarget[],
fps: number,
noLoop: boolean
): AnimationClip[]
AnimationClip.parse(json: Object): AnimationClip
AnimationClip.toJSON(clip: AnimationClip): Object
```
---
## KeyframeTrack
### Base Constructor
```ts
new KeyframeTrack(
name: string, // PropertyBinding path (e.g., "mesh.position")
times: Float32Array | number[], // keyframe times in seconds
values: Float32Array | number[], // keyframe values
interpolation?: number // InterpolateDiscrete | InterpolateLinear | InterpolateSmooth
): KeyframeTrack
```
### Specialized Track Constructors
```ts
new VectorKeyframeTrack(name, times, values, interpolation?)
new QuaternionKeyframeTrack(name, times, values, interpolation?)
new NumberKeyframeTrack(name, times, values, interpolation?)
new BooleanKeyframeTrack(name, times, values) // discrete only
new ColorKeyframeTrack(name, times, values, interpolation?)
new StringKeyframeTrack(name, times, values) // discrete only
```
### Track Methods
```ts
track.optimize(): KeyframeTrack // removes redundant sequential keys
track.scale(timeScale: number): KeyframeTrack
track.shift(timeOffset: number): KeyframeTrack
track.trim(startTime: number, endTime: number): KeyframeTrack
track.clone(): KeyframeTrack
track.validate(): boolean
```
---
## Clock
### Constructor
```ts
new Clock(autoStart?: boolean): Clock
// autoStart defaults to true.
```
### Properties
```ts
clock.autoStart: boolean
clock.elapsedTime: number // total accumulated time in seconds
clock.oldTime: number // last method call timestamp
clock.running: boolean // default: true
clock.startTime: number // when start() was last called
```
### Methods
```ts
clock.getDelta(): number // seconds since last getDelta() call
clock.getElapsedTime(): number // total elapsed time in seconds
clock.start(): void // starts the clock
clock.stop(): void // halts without resetting
```
---
## Constants
```ts
// Loop modes
THREE.LoopOnce // plays once and stops
THREE.LoopRepeat // loops, restarting from beginning (default)
THREE.LoopPingPong // alternates forward/backward
// Blend modes
THREE.NormalAnimationBlendMode // standard blending (default)
THREE.AdditiveAnimationBlendMode // additive layering
// Interpolation modes
THREE.InterpolateDiscrete // step function
THREE.InterpolateLinear // linear (default)
THREE.InterpolateSmooth // cubic spline
```
SKILL.md---
name: threejs-impl-animation
description: >
Use when playing animations, crossfading between animation states,
or loading skeletal animations from GLTF in Three.js. Prevents the
common mistake of not calling mixer.update(delta) every frame,
wrong crossfade setup, or missing Clock. Covers AnimationMixer,
AnimationClip, AnimationAction, KeyframeTrack, crossfade, blending.
Keywords: animation, AnimationMixer, AnimationClip, AnimationAction, crossfade, skeletal, GLTF animation, keyframe, blend, Clock, camera animation, smooth camera, fly to, tween, GSAP.
license: MIT
compatibility: "Designed for Claude Code. Requires Three.js r160+."
metadata:
author: OpenAEC-Foundation
version: "1.0"
---
# threejs-impl-animation
## Quick Reference
### Architecture
```
AnimationClip (data: array of KeyframeTrack objects)
└── AnimationAction (playback controller: play, pause, fade, crossfade)
└── AnimationMixer (master scheduler: one per animated root object)
└── Clock (provides delta time for mixer.update)
```
ALWAYS create exactly ONE `AnimationMixer` per animated root object.
ALWAYS call `mixer.update(delta)` every frame inside the render loop.
NEVER instantiate `AnimationAction` directly -- ALWAYS use `mixer.clipAction(clip)`.
### Essential Imports
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
```
### Minimal Animation Setup
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const clock = new THREE.Clock();
let mixer;
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
// Play all animations from the GLTF file
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
});
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```
### Critical Warnings
**NEVER** forget to call `mixer.update(delta)` in the render loop -- animations will NOT play without it.
**NEVER** use `new Date()` or `performance.now()` to compute delta manually -- ALWAYS use `THREE.Clock` or `renderer.setAnimationLoop` which provides stable frame timing.
**NEVER** call `mixer.clipAction(clip)` repeatedly in the render loop -- it caches internally, but the lookup is unnecessary overhead. ALWAYS store the returned action in a variable.
**NEVER** call `.play()` every frame -- call it ONCE to start playback. Calling `.play()` again on an already-playing action has no effect, but it signals misunderstanding.
**ALWAYS** call `.reset()` before `.play()` when restarting a stopped or finished action, or the action may resume from its last position.
**ALWAYS** set `action.clampWhenFinished = true` when using `LoopOnce` -- otherwise the action resets to the first frame when finished.
---
## AnimationMixer
The master scheduler that drives all animation actions for a single object hierarchy.
### Constructor
```js
const mixer = new THREE.AnimationMixer(rootObject);
```
`rootObject` is the root `Object3D` of the animated model (typically `gltf.scene`).
### Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `.time` | number | `0` | Global mixer time in seconds |
| `.timeScale` | number | `1` | Global speed multiplier; `0` pauses ALL actions |
### Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `.clipAction(clip, root?, blendMode?)` | `AnimationAction` | Returns or creates an action for the clip |
| `.existingAction(clip, root?)` | `AnimationAction \| null` | Returns previously created action or `null` |
| `.update(delta)` | `this` | Advances mixer by delta seconds -- MUST call every frame |
| `.setTime(seconds)` | `this` | Sets global time, updates all actions |
| `.stopAllAction()` | `this` | Deactivates all scheduled actions |
| `.getRoot()` | `Object3D` | Returns the mixer's root object |
| `.uncacheAction(clip, root?)` | `void` | Deallocates cached action |
| `.uncacheClip(clip)` | `void` | Deallocates clip data |
| `.uncacheRoot(root)` | `void` | Deallocates root object data |
### Events
Listen via `mixer.addEventListener(type, callback)`:
| Event | Fires When |
|-------|-----------|
| `'finished'` | Action completes (ONLY with `LoopOnce` + `clampWhenFinished = true`) |
| `'loop'` | Action completes a loop iteration |
Event object properties: `{ action, loopDelta, type }`.
### Blend Modes
Pass as the third argument to `mixer.clipAction(clip, root, blendMode)`:
| Constant | Behavior |
|----------|----------|
| `THREE.NormalAnimationBlendMode` | Standard blending (default) |
| `THREE.AdditiveAnimationBlendMode` | Layered on top of base animation |
---
## AnimationAction
Controls playback of a single animation clip. NEVER instantiate directly.
### Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `.blendMode` | number | `NormalAnimationBlendMode` | Blending strategy |
| `.clampWhenFinished` | boolean | `false` | Pause at last frame when done |
| `.enabled` | boolean | `true` | Disable without resetting |
| `.loop` | number | `LoopRepeat` | Loop mode |
| `.paused` | boolean | `false` | Freeze playback |
| `.repetitions` | number | `Infinity` | Loop count |
| `.time` | number | `0` | Local time in seconds |
| `.timeScale` | number | `1` | Speed: `0` pauses, negative reverses |
| `.weight` | number | `1` | Blend influence `[0, 1]` |
| `.zeroSlopeAtEnd` | boolean | `true` | Smooth interpolation at loop end |
| `.zeroSlopeAtStart` | boolean | `true` | Smooth interpolation at loop start |
### Loop Modes
| Constant | Behavior |
|----------|----------|
| `THREE.LoopOnce` | Plays once, stops |
| `THREE.LoopRepeat` | Restarts from beginning each loop |
| `THREE.LoopPingPong` | Alternates forward/backward |
### Playback Methods
| Method | Description |
|--------|-------------|
| `.play()` | Start playback |
| `.stop()` | Stop and reset to start |
| `.reset()` | Reset time, weight, speed to initial state |
| `.startAt(mixerTime)` | Delay start until specified mixer time |
### Fading and Crossfade Methods
| Method | Description |
|--------|-------------|
| `.fadeIn(duration)` | Fade weight from `0` to `1` |
| `.fadeOut(duration)` | Fade weight from `1` to `0` |
| `.crossFadeFrom(fadeOutAction, duration, warp)` | Crossfade from another action into this one |
| `.crossFadeTo(fadeInAction, duration, warp)` | Crossfade from this action to another |
| `.stopFading()` | Cancel any active fade |
### Speed and Timing Methods
| Method | Description |
|--------|-------------|
| `.halt(duration)` | Decelerate timeScale to `0` over duration |
| `.warp(startScale, endScale, duration)` | Smoothly transition playback speed |
| `.stopWarping()` | Cancel any active warp |
| `.setDuration(seconds)` | Adjust timeScale so one loop takes exactly `seconds` |
| `.setEffectiveTimeScale(scale)` | Set effective time scale |
| `.setEffectiveWeight(weight)` | Set effective weight |
| `.setLoop(mode, repetitions)` | Set loop mode and count |
| `.syncWith(otherAction)` | Synchronize time with another action |
### Query Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `.isRunning()` | boolean | `true` only if actively playing |
| `.isScheduled()` | boolean | `true` if `.play()` was called |
| `.getClip()` | `AnimationClip` | The associated clip |
| `.getMixer()` | `AnimationMixer` | The owning mixer |
| `.getRoot()` | `Object3D` | The root object |
| `.getEffectiveTimeScale()` | number | Computed time scale |
| `.getEffectiveWeight()` | number | Computed weight |
---
## AnimationClip
A reusable set of keyframe tracks. Typically loaded from GLTF files.
### Constructor
```js
const clip = new THREE.AnimationClip(name, duration, tracks, blendMode);
```
- `name` -- string identifier (GLTF clips use names from the file)
- `duration` -- seconds; `-1` to auto-calculate from tracks
- `tracks` -- array of `KeyframeTrack` objects
- `blendMode` -- optional blend mode constant
### Key Static Methods
| Method | Description |
|--------|-------------|
| `AnimationClip.findByName(arrayOrObject, name)` | Look up clip by name |
| `AnimationClip.CreateFromMorphTargetSequence(name, targets, fps, noLoop)` | Create clip from morph targets |
| `AnimationClip.parse(json)` | Deserialize from JSON |
---
## KeyframeTrack Types
| Track Type | Value Type | Use Case |
|------------|-----------|----------|
| `VectorKeyframeTrack` | Vector3 | Position, scale |
| `QuaternionKeyframeTrack` | Quaternion | Rotation (uses slerp) |
| `NumberKeyframeTrack` | number | Opacity, intensity |
| `BooleanKeyframeTrack` | boolean | Visibility toggles |
| `ColorKeyframeTrack` | Color | Color animation |
| `StringKeyframeTrack` | string | Discrete string values |
### Interpolation Modes
| Constant | Behavior |
|----------|----------|
| `THREE.InterpolateDiscrete` | Step function, no smoothing |
| `THREE.InterpolateLinear` | Linear interpolation (default) |
| `THREE.InterpolateSmooth` | Cubic spline interpolation |
### PropertyBinding Path Format
```
"meshName.position" // animate position
"meshName.material.opacity" // animate material property
"meshName.morphTargetInfluences[0]" // animate morph target
"boneName.quaternion" // animate bone rotation
```
---
## Clock
### Constructor
```js
const clock = new THREE.Clock(autoStart); // autoStart defaults to true
```
### Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `.getDelta()` | number | Seconds since last `getDelta()` call |
| `.getElapsedTime()` | number | Total elapsed time in seconds |
| `.start()` | void | Start the clock |
| `.stop()` | void | Stop without resetting |
---
## Crossfade Pattern (Character State Machine)
```js
const actions = {};
gltf.animations.forEach((clip) => {
actions[clip.name] = mixer.clipAction(clip);
});
let currentAction = actions['Idle'];
currentAction.play();
function switchAction(toName, duration = 0.5) {
const toAction = actions[toName];
toAction.reset();
toAction.setEffectiveTimeScale(1);
toAction.setEffectiveWeight(1);
toAction.crossFadeFrom(currentAction, duration, true);
toAction.play();
currentAction = toAction;
}
```
ALWAYS call `.reset()` on the incoming action before crossfading.
ALWAYS store the current action reference for the next transition.
---
## Additive Animation Blending
```js
const baseAction = mixer.clipAction(baseClip);
const additiveAction = mixer.clipAction(
additiveClip, undefined, THREE.AdditiveAnimationBlendMode
);
baseAction.play();
additiveAction.play();
additiveAction.setEffectiveWeight(0.5);
```
Use additive blending for layered effects: breathing, damage reactions, aim offsets.
---
## Morph Target Animation
```js
// Manual control
mesh.morphTargetInfluences[0] = Math.sin(elapsed) * 0.5 + 0.5;
// Via GLTF animation clip (preferred)
const morphAction = mixer.clipAction(morphClip);
morphAction.play();
```
---
## Reference Links
- [references/methods.md](references/methods.md) -- Full API signatures
- [references/examples.md](references/examples.md) -- Working code examples
- [references/anti-patterns.md](references/anti-patterns.md) -- What NOT to do
### Official Sources
- https://threejs.org/docs/#api/en/animation/AnimationMixer
- https://threejs.org/docs/#api/en/animation/AnimationAction
- https://threejs.org/docs/#api/en/animation/AnimationClip
- https://threejs.org/docs/#api/en/animation/KeyframeTrack
- https://threejs.org/docs/#api/en/core/Clock
- https://threejs.org/examples/#webgl_animation_skinning_blending