animouse
v0.8.0
Published
lightweight animation state machine for three js
Downloads
419
Maintainers
Readme
What's included
- 🎬 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
Installation
npm install animouseRequirements
- Three.js ^0.175.0 (peer dependency)
- Modern JavaScript environment with ES2020+ support
Examples
🎮 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!
Quick Start
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);
}How it works
Animation States
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
Animation Machine
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
Animation States
ClipState - Single Animation Control
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');
});LinearBlendTree - 1D Animation Blending
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.5PolarBlendTree - 2D Polar Blending
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 }FreeformBlendTree - 2D Blending
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 }State Machine Transitions
Event-Driven Transitions
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');Automatic Transitions
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
});Data-Driven Transitions
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 Events
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 Events
Time-based events that trigger callbacks at specific points during animation playback. Useful for synchronizing sound effects or other events with animation frames.
ClipState Time Events
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();
});Blend Tree Time Events
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).
Performance Notes
- 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
Contributing
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
License
MIT © jango
