<p align="center">
  <img src="https://raw.githubusercontent.com/jango-git/animouse/main/assets/logotype.svg" width="200" alt="Animouse logo"><br/>
  <h1 align="center">Animouse</h1>
  <p align="center">
      A powerful animation state machine and blending system for Three.js that makes complex animation workflows simple and intuitive.
  </p>
</p>

<p align="center">
<a href="https://www.npmjs.com/package/animouse"><img src="https://img.shields.io/npm/v/animouse.svg" alt="npm version"></a>
<a href="https://bundlephobia.com/package/animouse"><img src="https://badgen.net/bundlephobia/min/animouse" alt="bundle size (min)"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-%5E5.8.0-blue" alt="TypeScript"></a>
<a href="https://threejs.org/"><img src="https://img.shields.io/badge/Three.js-%5E0.175.0-green" alt="Three.js"></a>
</p>

## Features

- 🎬 **Animation State Machine** - Event-driven, automatic, and data-driven transitions
- 🎯 **Single Clip States** - Simple animation control with lifecycle events
- 📊 **Linear Blend Trees** - 1D blending for speed/intensity variations
- 🧭 **Polar Blend Trees** - 2D blending in polar coordinates (radius/direction)
- 🎨 **Freeform Blend Trees** - Arbitrary 2D blending using Delaunay triangulation
- 🔄 **Smooth Transitions** - Configurable blend durations between states
- 📦 **Full TypeScript Support** - Complete type safety and IntelliSense

## Installation

```bash
npm install animouse
```

## Requirements

- Three.js ^0.175.0 (peer dependency)
- Modern JavaScript environment with ES2020+ support

## Examples

🎮 **[Live Examples](https://jango-git.github.io/animouse/)** - 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

```typescript
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);
}
```

## Core Concepts

### Animation States

Animation states are the building blocks of the system. Each state manages one or more Three.js AnimationActions and handles their lifecycle:

- **ClipState** - Wraps a single AnimationAction
- **LinearBlendTree** - Blends multiple actions along a 1D axis
- **PolarBlendTree** - Blends actions in 2D polar coordinates
- **FreeformBlendTree** - Blends actions in arbitrary 2D space

### Animation Machine

The AnimationMachine orchestrates state transitions and manages the overall animation flow. It supports three types of transitions:

1. **Event Transitions** - Triggered by specific events
2. **Automatic Transitions** - Triggered when animations complete
3. **Data Transitions** - Triggered by condition evaluation

## Animation States

### ClipState - Single Animation Control

Control individual animation clips with automatic event handling:

```typescript
import { ClipState, AnimationStateEvent } from 'animouse';

const jumpState = new ClipState(jumpAction);

// 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

Perfect for speed variations, intensity levels, or any linear progression:

```typescript
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
```

### PolarBlendTree - 2D Polar Blending

Ideal for directional movement with varying intensities:

```typescript
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);
```

### FreeformBlendTree - Arbitrary 2D Blending

For complex animation spaces with irregular layouts:

```typescript
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 (positive valence/arousal)
  { action: sadAction, x: -1, y: -0.5 },       // Sad (negative valence, low arousal)
  { action: angryAction, x: -0.8, y: 0.9 },    // Angry (negative valence, high arousal)
  { action: surprisedAction, x: 0.2, y: 1.2 }, // Surprised (slight positive, very high arousal)
  { action: disgustAction, x: -1.2, y: 0.1 }   // Disgust (very negative, medium arousal)
]);

// Blend to slightly happy and excited
emotionTree.setBlend(0.6, 0.8);
```

## State Machine Transitions

### Event-Driven Transitions

Respond to specific game events or user input:

```typescript
// 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

Automatically transition when animations complete:

```typescript
// 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

Continuously evaluate conditions for seamless state changes:

```typescript
// Transition based on health
machine.addDataTransition(combatState, {
  to: deathState,
  duration: 0.5,
  condition: (from, to, health) => health <= 0,
  data: [character.health]
});
```

## Animation Events

All animation states emit lifecycle events:

```typescript
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

Animouse supports time-based events that trigger callbacks at specific points during animation playback. This is useful for synchronizing sound effects, particle systems, or other game events with animation frames.

### ClipState Time Events

For single animation clips, register time events directly on the state:

```typescript
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:

```typescript
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), making them perfect for frame-accurate synchronization with animation content.

## Performance Considerations

- Blend trees automatically optimize by only updating active animations
- Use data transitions sparingly for frequently evaluated conditions
- Prefer event transitions for user input and game events

## Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Submit a pull request

## License

MIT © [jango](https://github.com/jango-git)

## Credits

- Built with [Three.js](https://threejs.org/) for 3D animation support
- Event system powered by [eventail](https://www.npmjs.com/package/eventail)
- Mathematical utilities for robust geometric calculations
- Delaunay triangulation for freeform blend spaces
