An animation state machine and blending system for Three.js that I use in my daily work with 3D development.
- 🎬 Animation State Machine - Event-driven and data-driven transitions
- 🎯 Single Clip States - Animation control with lifecycle events
- 📊 Linear Blend Trees - 1D blending for speed variations
- 🧭 Polar Blend Trees - 2D blending in polar coordinates
- 🎨 Freeform Blend Trees - 2D blending using Delaunay triangulation
- 🔄 Transitions - Configurable blend durations between states
- 📦 TypeScript Support - Type safety and IntelliSense
npm install animouse
- Three.js ^0.175.0 (peer dependency)
- Modern JavaScript environment with ES2020+ support
🎮 Live Examples - Interactive demos showing Animouse in action
Browse working examples that demonstrate:
- Basic GLB character animation loading and playback
- Integration with Three.js scene setup
- Real-time animation control
Visit the examples page to see the library in action!
import { LinearBlendTree, AnimationMachine } from 'animouse';
import { AnimationMixer, Vector2 } from 'three';
// Setup Three.js animation mixer with your loaded character
const mixer = new AnimationMixer(character);
// Create linear blend tree for movement speed
const movementTree = new LinearBlendTree([
{ action: mixer.clipAction(idleClip), value: 0 }, // Idle
{ action: mixer.clipAction(walkClip), value: 0.5 }, // Walk
{ action: mixer.clipAction(runClip), value: 1 } // Run
]);
// Create state machine
const machine = new AnimationMachine(movementTree, mixer);
// Input handling
const movementInput = new Vector2(0, 0);
function handleInput() {
// Get movement input (WASD, gamepad, etc.)
const inputMagnitude = movementInput.length();
// Blend animations based on movement speed
// 0 = idle, 0.5 = walk, 1 = run
movementTree.setBlend(inputMagnitude);
}
// Main update loop
function animate() {
const deltaTime = clock.getDelta();
handleInput(); // Update input and blend values
machine.update(deltaTime); // Update state machine and animations
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
Animation states manage one or more Three.js AnimationActions:
- ClipState - Wraps a single AnimationAction
- LinearBlendTree - Blends multiple actions along a 1D axis
- PolarBlendTree - Blends actions in 2D polar coordinates
- FreeformBlendTree - Blends actions in 2D space
The AnimationMachine handles state transitions. It supports three types:
- Event Transitions - Triggered by specific events
- Automatic Transitions - Triggered when animations complete
- Data Transitions - Triggered by condition evaluation
Control individual animation clips with event handling:
import { ClipState, AnimationStateEvent } from 'animouse';
const jumpState = new ClipState(jumpAction);
jumpState.name = 'jump'; // Optional: name states for debugging
// Listen for animation events
jumpState.on(AnimationStateEvent.PLAY, (action, state) => {
console.log('Jump animation started');
});
jumpState.on(AnimationStateEvent.FINISH, (action, state) => {
console.log('Jump animation completed');
});
For speed variations or linear progressions:
import { LinearBlendTree } from 'animouse';
// Create speed-based movement blend tree
const movementTree = new LinearBlendTree([
{ action: idleAction, value: 0 }, // Stationary
{ action: walkAction, value: 1 }, // Slow movement
{ action: jogAction, value: 2 }, // Medium movement
{ action: runAction, value: 3 }, // Fast movement
{ action: sprintAction, value: 4 } // Maximum speed
]);
// Blend based on movement speed
movementTree.setBlend(2.5); // Blend between jog and run
// Get current blend value
console.log('Current speed:', movementTree.blendValue); // 2.5
For directional movement with varying intensities:
import { PolarBlendTree } from 'animouse';
import { MathUtils } from 'three';
// Create directional movement system
const directionTree = new PolarBlendTree([
// Walk speed (radius = 1)
{ action: walkForwardAction, radius: 1, azimuth: MathUtils.degToRad(0) },
{ action: walkLeftAction, radius: 1, azimuth: MathUtils.degToRad(-90) },
{ action: walkRightAction, radius: 1, azimuth: MathUtils.degToRad(90) },
{ action: walkBackAction, radius: 1, azimuth: MathUtils.degToRad(180) },
// Run speed (radius = 2)
{ action: runForwardAction, radius: 2, azimuth: MathUtils.degToRad(0) },
{ action: runLeftAction, radius: 2, azimuth: MathUtils.degToRad(-90) }
{ action: runRightAction, radius: 2, azimuth: MathUtils.degToRad(90) },
{ action: runBackAction, radius: 2, azimuth: MathUtils.degToRad(180) },
], idleAction); // Optional center action
// Blend to northeast at medium speed
directionTree.setBlend(MathUtils.degToRad(45), 1.5);
// Get current blend values
console.log('Current direction:', directionTree.blendValue);
// { azimuth: 0.785, radius: 1.5 }
For irregular animation spaces:
import { FreeformBlendTree } from 'animouse';
// Create emotion-based facial animation system
const emotionTree = new FreeformBlendTree([
{ action: neutralAction, x: 0, y: 0 }, // Center: neutral
{ action: happyAction, x: 1, y: 1 }, // Happy
{ action: sadAction, x: -1, y: -0.5 }, // Sad
{ action: angryAction, x: -0.8, y: 0.9 }, // Angry
{ action: surprisedAction, x: 0.2, y: 1.2 }, // Surprised
{ action: disgustAction, x: -1.2, y: 0.1 } // Disgust
]);
// Blend to slightly happy and excited
emotionTree.setBlend(0.6, 0.8);
// Get current blend position
console.log('Current emotion:', emotionTree.blendValue); // { x: 0.6, y: 0.8 }
Respond to game events or user input:
// Basic transition
machine.addEventTransition('jump', {
from: idleState,
to: jumpState,
duration: 0.2
});
// Conditional transition
machine.addEventTransition('attack', {
to: attackState,
duration: 0.1,
condition: (from, to, event, weaponType) => weaponType === 'sword'
});
// Trigger transitions
machine.handleEvent('jump');
machine.handleEvent('attack', 'sword');
Transition when animations complete:
// Transition to falling after jump completes
machine.addAutomaticTransition(jumpState, {
to: fallState,
duration: 0.1
});
// Chain multiple animations
machine.addAutomaticTransition(landState, {
to: idleState,
duration: 0.3
});
Evaluate conditions for state changes:
// Transition based on health
machine.addDataTransition(combatState, {
to: deathState,
duration: 0.5,
condition: (from, to, health) => health <=
0,
data: [character.health]
});
Animation states emit lifecycle events:
import { AnimationStateEvent } from 'animouse';
// State lifecycle events
state.on(AnimationStateEvent.ENTER, (state) => {
console.log('State activated');
});
state.on(AnimationStateEvent.EXIT, (state) => {
console.log('State deactivated');
});
// Animation playback events
state.on(AnimationStateEvent.PLAY, (action, state) => {
console.log('Animation started playing');
});
state.on(AnimationStateEvent.STOP, (action, state) => {
console.log('Animation stopped');
});
// Animation completion events
state.on(AnimationStateEvent.ITERATE, (action, state) => {
console.log('Looped animation completed a cycle');
});
state.on(AnimationStateEvent.FINISH, (action, state) => {
console.log('Non-looped animation finished');
});
Time-based events that trigger callbacks at specific points during animation playback. Useful for synchronizing sound effects or other events with animation frames.
For single animation clips, register time events directly on the state:
import { ClipState } from 'animouse';
const walkState = new ClipState(walkAction);
// Trigger footstep sound at 25% and 75% of the walk cycle
walkState.onTimeEvent(0.25, (action, state) => {
playSound('footstep-left');
});
walkState.onTimeEvent(0.75, (action, state) => {
playSound('footstep-right');
});
// One-time event for attack impact
const attackState = new ClipState(attackAction);
attackState.onceTimeEvent(0.6, (action, state) => {
dealDamage();
showImpactEffect();
});
For blend trees, specify which action to monitor:
import { LinearBlendTree } from 'animouse';
const movementTree = new LinearBlendTree([
{ action: walkAction, value: 1 },
{ action: runAction, value: 2 }
]);
// Add footstep events to specific actions
movementTree.onTimeEvent(walkAction, 0.5, (action, state) => {
playSound('walk-footstep');
});
movementTree.onTimeEvent(runAction, 0.3, (action, state) => {
playSound('run-footstep');
});
// Remove events when no longer needed
movementTree.offTimeEvent(walkAction, 0.5, footstepCallback);
Time events fire when the animation crosses the specified time threshold (0.0 to 1.0).
- Blend trees update only active animations (weight > 0)
- Animation actions are automatically started/stopped based on weights
- Use data transitions carefully for frequently evaluated conditions
- Event transitions work well for user input and game events
- States can be named for easier debugging and identification
Feel free to submit issues and pull requests if you find this library helpful.
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
MIT © jango