UNPKG

animouse

Version:

lightweight animation state machine for three js

227 lines (226 loc) 10.7 kB
import type { AnimationAction } from "three"; import { AnimationTree } from "./AnimationTree"; /** * Configuration for a polar animation action in the blend tree. * Associates an animation action with polar coordinates (radius, azimuth). * * @public */ export interface PolarAction { /** The Three.js animation action to be played */ action: AnimationAction; /** Radial distance from the origin. Must be finite and positive. */ radius: number; /** Angular position in radians. Must be finite. Will be normalized to [0, 2π) range. */ azimuth: number; } /** * Configuration options for polar blend tree behavior. * * @public */ export interface PolarBlendTreeOptions { /** Animation action to play at the center (origin) of the polar space */ centerAction: AnimationAction; /** Whether the blend tree should loop azimuthal blending at 2π */ isLooped: boolean; } /** * Polar blend tree implementation for 2D animation blending in polar coordinates. * * This class manages a collection of animation actions positioned in polar space (radius, azimuth), * automatically blending between adjacent animations based on polar coordinates. The blend tree * organizes actions into rays (constant azimuth) and rings (constant radius) to enable efficient * bilinear interpolation between the four closest animations, creating smooth transitions across * the polar space. * * ## Architecture * - **Rays**: Groups of anchors with the same azimuth but different radii * - **Rings**: Groups of anchors with the same radius but different azimuths * - **Bilinear Interpolation**: Weight calculation between 4 corner anchors in polar grid * - **Center Action**: Optional action at origin (0,0) for special handling * * ## Coordinate System * - **Radius**: Distance from origin, must be non-negative * - **Azimuth**: Angle in radians, automatically normalized to [0, 2π) range * - **Origin**: Special point (0,0) handled separately if center action provided * * ## Input Validation * - Minimum 2 actions required for basic interpolation * - All radius values must be finite, positive, within JavaScript safe range * - All azimuth values must be finite, within JavaScript safe range * - No duplicate polar coordinates allowed * - At least 2 rays required (different azimuth values) * - All rays must have the same number of anchors for grid consistency * * ## Blending Algorithm * 1. Find the two adjacent rays that contain the target azimuth * 2. Calculate angular interpolation weights between these rays * 3. For each ray, find the two adjacent rings containing the target radius * 4. Calculate radial interpolation weights between these rings * 5. Apply bilinear interpolation to determine final animation weights * 6. Handle special cases for center action and edge boundaries * * @example Walk/Run Directional Movement * ```typescript * // Create actions for 4-directional movement at two speeds * const idle = mixer.clipAction(idleAnimationClip); * * // Walk actions (radius = 1) * const walkForward = mixer.clipAction(walkForwardAnimationClip); * const walkRight = mixer.clipAction(walkRightAnimationClip); * const walkBackward = mixer.clipAction(walkBackwardAnimationClip); * const walkLeft = mixer.clipAction(walkLeftAnimationClip); * * // Run actions (radius = 2) * const runForward = mixer.clipAction(runForwardAnimationClip); * const runRight = mixer.clipAction(runRightAnimationClip); * const runBackward = mixer.clipAction(runBackwardAnimationClip); * const runLeft = mixer.clipAction(runLeftAnimationClip); * * // Set up polar blend tree with 4-directional movement * const blendTree = new PolarBlendTree([ * // Walk speed (radius = 1) * { action: walkForward, radius: 1, azimuth: MathUtils.degToRad(0) }, // Forward * { action: walkLeft, radius: 1, azimuth: MathUtils.degToRad(90) }, // Left * { action: walkBackward, radius: 1, azimuth: MathUtils.degToRad(180) }, // Backward * { action: walkRight, radius: 1, azimuth: MathUtils.degToRad(270) }, // Right * * // Run speed (radius = 2) * { action: runForward, radius: 2, azimuth: MathUtils.degToRad(0) }, // Fast Forward * { action: runLeft, radius: 2, azimuth: MathUtils.degToRad(90) }, // Fast Left * { action: runBackward, radius: 2, azimuth: MathUtils.degToRad(180) }, // Fast Backward * { action: runRight, radius: 2, azimuth: MathUtils.degToRad(270) }, // Fast Right * ], idle); // Center action for stationary state * * // Blend to medium speed northeast (45° at 1.5x speed) * blendTree.setBlend(1.5, MathUtils.degToRad(45)); * * // Blend to slow walk forward * blendTree.setBlend(0.5, MathUtils.degToRad(0)); * ``` * * @public */ export declare class PolarBlendTree extends AnimationTree { private readonly tempAnchorMap; private readonly trackableAnchors; /** Optional center anchor at origin (0,0) for special blending behavior */ private readonly centerAnchor?; /** Array of rays (constant azimuth lines) containing anchors, sorted by azimuth */ private readonly rays; /** Array of rings (constant radius circles) containing anchors, sorted by radius */ private readonly rings; /** Current radial position for blending calculations */ private currentRadius; /** Current angular position for blending calculations, normalized to [0, 2π) */ private currentAzimuth; /** * Creates a new polar blend tree with the specified animation actions. * Actions are organized into rays (by azimuth) and rings (by radius) for * efficient bilinear interpolation. Initializes all actions to stopped state * and validates clip durations. * * @param polarActions - Array of polar actions defining the blend space. * Must contain at least 2 actions with unique coordinates. * @param centerAction - Optional center action at origin (0,0) * @throws {Error} When fewer than 2 actions are provided * @throws {Error} When any action has non-finite radius or azimuth values * @throws {Error} When any action has non-positive radius * @throws {Error} When any action has values outside JavaScript's safe range * @throws {Error} When multiple actions have the same polar coordinates * @throws {Error} When fewer than 2 rays are created (insufficient azimuth variety) * @throws {Error} When rays don't have consistent anchor counts for valid grid * @throws {Error} When any animation clip duration is not a positive finite number * @see {@link assertValidNumber} for coordinate validation details * @see {@link assertValidPositiveNumber} for radius and duration validation details */ constructor(polarActions: PolarAction[], centerAction?: AnimationAction); get blendValue(): { azimuth: number; radius: number; }; /** * Sets the blend position in polar coordinates to determine animation weights. * * The azimuth is normalized to [0, 2π) range and radius must be non-negative. * When the position changes, animation weights are recalculated using bilinear * interpolation between the closest anchors. * * @param azimuth - Target angular position in radians (finite number). Will be normalized to [0, 2π). * @param radius - Target radial distance from origin (finite non-negative number). * @throws {Error} When azimuth is not a finite number * @throws {Error} When radius is not a finite non-negative number * @see {@link assertValidNumber} for azimuth validation details * @see {@link assertValidNonNegativeNumber} for radius validation details * * @example * ```typescript * // Blend to half speed, 45 degrees * blendTree.setBlend(0.5, MathUtils.degToRad(45)); * * // Blend to full speed, straight back * blendTree.setBlend(1.0, MathUtils.degToRad(180)); * * // Azimuth values are normalized to [0, 2π) range * blendTree.setBlend(1.0, MathUtils.degToRad(450)); // Becomes (1.0, ~1.57) * ``` * * @public */ setBlend(azimuth: number, radius: number): void; protected ["onEnterInternal"](): void; /** * Updates the global influence for all active anchors in the polar blend tree. * * Called when the tree's overall influence changes but relative weights between * anchors should remain the same. Applies the current tree influence to all * active anchors while maintaining their existing weight distribution from * the polar blending calculations. * * @override * @protected */ protected updateAnchorsInfluence(): void; /** * Recalculates and updates animation weights based on current blend position. * * This is the core blending algorithm that: * 1. Finds the two adjacent rays containing the current azimuth * 2. Calculates angular interpolation weights between these rays * 3. Determines if blending occurs in the center region or outer grid * 4. Applies appropriate interpolation (linear for center, bilinear for grid) * 5. Updates active anchors set and applies calculated weights * * The method handles three distinct cases: * - **Center Region**: When radius < first ring radius, blends with center action * - **Grid Region**: When radius >= first ring radius, uses bilinear interpolation * - **Edge Cases**: Boundary conditions and wraparound azimuth handling * * @private */ private updateAnchors; /** * Calculates bilinear interpolation weights for the four corner anchors in the polar grid. * * This method performs standard bilinear interpolation between four points arranged * in a rectangular grid pattern in polar space. The four corners are defined by: * - Inner ring vs outer ring (radial dimension) * - Left ray vs right ray (angular dimension) * * The bilinear interpolation formula combines the radial and angular interpolation * weights to determine how much each of the four corner anchors contributes to * the final blend result. * * @param weights - Map to store calculated weights for each anchor * @param lRayT - Weight for the left ray (0 = all left, 1 = all right) * @param rRayT - Weight for the right ray (0 = all left, 1 = all right) * @param lRayIndex - Index of the left ray in the rays array * @param rRayIndex - Index of the right ray in the rays array * * @throws {Error} When no ring pair contains the current radius (should never happen) * * @private */ private calculateBilinearWeights; }