UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

260 lines (259 loc) 10.5 kB
/** * 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 {};