ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
260 lines (259 loc) • 10.5 kB
TypeScript
/**
* Sprite Animation Plugin for ECSpresso
*
* ECS-native frame-based sprite animation. Advances through spritesheet frames,
* handles loop modes (once, loop, pingPong), publishes completion events, and
* syncs the current frame's texture to the PixiJS Sprite via structural access.
*
* Renderer2D is a required dependency — the `sprite` component comes from that plugin.
* This plugin declares only `spriteAnimation` as its component type.
*/
import { type BasePluginOptions } from 'ecspresso';
import type { BaseWorld } from 'ecspresso';
import type { Spritesheet, SpritesheetData, TextureSource } from 'pixi.js';
/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */
type SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;
export type AnimationLoopMode = 'once' | 'loop' | 'pingPong';
/**
* A single animation clip: an ordered sequence of texture frames with timing.
* Immutable and shared across entities.
*/
export interface SpriteAnimationClip {
readonly frames: readonly unknown[];
readonly frameDuration: number;
readonly frameDurations: readonly number[] | null;
readonly loop: AnimationLoopMode;
}
/**
* Input format for defining a clip. Accepts either uniform or per-frame timing.
*/
export interface SpriteAnimationClipInput {
/** Array of PixiJS Texture objects */
frames: readonly unknown[];
/** Uniform seconds-per-frame (used when frameDurations is not provided) */
frameDuration?: number;
/** Per-frame durations in seconds (overrides frameDuration) */
frameDurations?: readonly number[];
/** Loop mode (default: 'loop') */
loop?: AnimationLoopMode;
}
/**
* A named collection of animation clips. Immutable and shared across entities.
* Parameterized by A (animation name union) for compile-time validation.
*/
export interface SpriteAnimationSet<A extends string = string> {
readonly id: string;
readonly clips: {
readonly [K in A]: SpriteAnimationClip;
};
readonly defaultClip: A;
}
/**
* Per-entity runtime animation state.
*/
export interface SpriteAnimation<A extends string = string> {
readonly set: SpriteAnimationSet<A>;
current: A;
currentFrame: number;
elapsed: number;
playing: boolean;
speed: number;
direction: 1 | -1;
totalLoops: number;
completedLoops: number;
justFinished: boolean;
onComplete?: (data: SpriteAnimationEventData) => void;
}
/**
* Component types provided by the sprite animation plugin.
*/
export interface SpriteAnimationComponentTypes<A extends string = string> {
spriteAnimation: SpriteAnimation<A>;
}
/**
* Data published when an animation completes.
*/
export interface SpriteAnimationEventData {
entityId: number;
animation: string;
}
export interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {
}
/**
* Define a single-clip animation set named 'default'.
* For simple use cases like spinning coins, pulsing effects, etc.
*
* @param id Unique identifier for this animation set
* @param clip Clip definition
* @returns A frozen SpriteAnimationSet with one clip named 'default'
*/
export declare function defineSpriteAnimation(id: string, clip: SpriteAnimationClipInput): SpriteAnimationSet<'default'>;
/**
* Define a multi-clip animation set with named animations.
* Animation names are inferred from the keys of the clips object.
*
* @param id Unique identifier for this animation set
* @param clips Object mapping animation names to clip definitions
* @param options Optional configuration (defaultClip)
* @returns A frozen SpriteAnimationSet with inferred animation name union
*/
export declare function defineSpriteAnimations<A extends string>(id: string, clips: Record<A, SpriteAnimationClipInput>, options?: {
defaultClip?: NoInfer<A>;
}): SpriteAnimationSet<A>;
/**
* Create a spriteAnimation component from an animation set.
*
* @param set The animation set to use
* @param options Optional configuration (initial clip, speed, onComplete event)
* @returns Component object suitable for spreading into spawn()
*/
export declare function createSpriteAnimation<A extends string>(set: SpriteAnimationSet<A>, options?: {
initial?: A;
speed?: number;
totalLoops?: number;
onComplete?: (data: SpriteAnimationEventData) => void;
}): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'>;
/**
* Switch an entity's current animation at runtime.
* Resets state if switching to a different animation (or restart=true).
*
* @returns false if entity has no spriteAnimation or animation name doesn't exist
*/
export declare function playAnimation(ecs: SpriteAnimationWorld, entityId: number, animation: string, options?: {
restart?: boolean;
speed?: number;
}): boolean;
/**
* Pause an entity's animation.
*
* @returns false if entity has no spriteAnimation
*/
export declare function stopAnimation(ecs: SpriteAnimationWorld, entityId: number): boolean;
/**
* Resume a paused animation.
*
* @returns false if entity has no spriteAnimation
*/
export declare function resumeAnimation(ecs: SpriteAnimationWorld, entityId: number): boolean;
/**
* Create a sprite animation plugin for ECSpresso.
*
* Provides:
* - Frame-based animation system processing spriteAnimation components
* - Loop modes: once, loop, pingPong
* - justFinished one-frame flag for completion detection
* - onComplete event publishing
* - Sprite texture sync via structural cross-plugin access
* - Change detection via markChanged
*/
export declare function createSpriteAnimationPlugin<G extends string = 'spriteAnimation'>(options?: SpriteAnimationPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SpriteAnimationComponentTypes<string>>, import("ecspresso").EmptyConfig, "sprite-animation-update", G, never, never>;
/**
* Per-clip timing/loop overrides keyed by animation name. Each entry tweaks
* a single clip; omitted entries fall back to the top-level defaults.
*/
export type SheetClipOverrides<A extends string> = {
readonly [K in A]?: Omit<SpriteAnimationClipInput, 'frames'>;
};
/**
* Extract the animation-name union from a typed SpritesheetData. Falls back
* to `string` for untyped sheets.
*/
export type SheetAnimationKeys<S extends SpritesheetData> = S extends {
animations: infer A;
} ? A extends Record<infer K, unknown> ? K extends string ? K : never : string : string;
/**
* Build a clip from a named animation in a loaded PixiJS Spritesheet.
*
* @example
* const sheet = await Assets.load<Spritesheet>('/hero.json');
* const idle = clipFromSheet(sheet, 'idle', { frameDuration: 1 / 12 });
*/
export declare function clipFromSheet(sheet: Spritesheet, animationName: string, options?: Omit<SpriteAnimationClipInput, 'frames'>): SpriteAnimationClip;
/**
* Build an animation set from every named animation in a PixiJS Spritesheet.
* When the sheet is typed as `Spritesheet<MyData>`, animation names and
* `defaultClip` / `perClip` keys are inferred at compile time.
*
* @example
* const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>
* const set = animationSetFromSheet('hero', sheet, {
* defaultClip: 'idle',
* frameDuration: 1 / 12,
* perClip: { attack: { loop: 'once' } },
* });
*/
export declare function animationSetFromSheet<S extends SpritesheetData = SpritesheetData>(id: string, sheet: Spritesheet<S>, options?: {
defaultClip?: SheetAnimationKeys<S>;
frameDuration?: number;
loop?: AnimationLoopMode;
perClip?: SheetClipOverrides<SheetAnimationKeys<S>>;
}): SpriteAnimationSet<SheetAnimationKeys<S>>;
/**
* Slice a grid-arranged sprite sheet into a clip. Use when you don't have a
* TexturePacker JSON — just an image and uniform cell dimensions. Cells are
* walked row-major.
*
* Specify exactly one of `rows`, `count`, or `indices` to define the cell set
* (along with `columns`). Combining `count` and `indices` is rejected.
*
* Returns a `Promise` because pixi.js is imported lazily — keeps the static
* module graph free of a runtime pixi dependency for consumers who only use
* the sheet-based helpers.
*
* @example
* const tex = await Assets.load<Texture>('/coin.png');
* const clip = await clipFromGrid({
* source: tex.source,
* frameWidth: 16, frameHeight: 16,
* columns: 8, count: 8,
* frameDuration: 1 / 10,
* });
*/
export declare function clipFromGrid(input: {
source: TextureSource;
frameWidth: number;
frameHeight: number;
columns: number;
rows?: number;
/** Explicit row-major, 0-based cell indices. Mutually exclusive with `count`. */
indices?: readonly number[];
/** Number of cells to use, walked row-major. Mutually exclusive with `indices`. */
count?: number;
/** Pixels between cells. */
spacing?: number;
/** Pixels around the sheet edge. */
margin?: number;
frameDuration?: number;
frameDurations?: readonly number[];
loop?: AnimationLoopMode;
}): Promise<SpriteAnimationClip>;
/**
* Build an asset-manager-compatible loader for a PixiJS spritesheet atlas
* (TexturePacker JSON, etc.). Returns the fully-parsed `Spritesheet` object
* with `.animations` and `.textures` populated.
*
* The loader performs a runtime shape check on the resolved value — `Assets.load<T>`
* is purely nominal in PixiJS, so pointing this at a non-atlas URL would
* otherwise surface as a misleading 'animation not found' error deep in
* `clipFromSheet`/`animationSetFromSheet`. The shape check turns that into a
* load-time error with a clear message.
*
* To get literal animation-name inference, declare `S` as an
* `interface ... extends SpritesheetData` (a `type` alias re-widens via
* `SpritesheetData.animations`'s `Dict<string[]>` string index signature).
*
* @example
* interface HeroData extends SpritesheetData {
* animations: { idle: string[]; walk: string[]; attack: string[] };
* }
*
* ecs.builder.withAssets(a => a
* .add('hero', spritesheetLoader<HeroData>('/hero.json'))
* );
*
* // Later:
* const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>
* const set = animationSetFromSheet('hero', sheet); // names inferred
*/
export declare function spritesheetLoader<S extends SpritesheetData = SpritesheetData>(url: string): () => Promise<Spritesheet<S>>;
export {};