ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
162 lines (161 loc) • 6.42 kB
TypeScript
/**
* Tween Plugin for ECSpresso
*
* Declarative property animation within the ECS. Tween any numeric component
* field over time with standard easing functions, sequences, and completion events.
*/
import { type BasePluginOptions } from 'ecspresso';
import type { ComponentsOfWorld, AnyECSpresso } from 'ecspresso';
import { type EasingFn } from '../../utils/easing';
/**
* Data structure published when a tween completes.
* Use this type when defining tween completion events in your EventTypes interface.
*/
export interface TweenEventData {
/** The entity ID the tween belongs to */
entityId: number;
/** Number of steps in the tween */
stepCount: number;
}
export interface TweenTarget {
/** Component name on the entity */
component: string;
/** Pre-split field path (e.g., ['position', 'x']) */
path: readonly string[];
/** Starting value. null = resolve from current value on first tick */
from: number | null;
/** Target value */
to: number;
}
export interface TweenStep {
targets: TweenTarget[];
duration: number;
easing: EasingFn;
}
export interface Tween {
steps: TweenStep[];
currentStep: number;
elapsed: number;
loop: LoopMode;
totalLoops: number;
completedLoops: number;
direction: 1 | -1;
state: 'pending' | 'active' | 'complete';
onComplete?: (data: TweenEventData) => void;
justFinished: boolean;
}
export type LoopMode = 'once' | 'loop' | 'yoyo';
/**
* Component types provided by the tween plugin.
*/
export interface TweenComponentTypes {
tween: Tween;
}
export interface TweenPluginOptions<G extends string = 'tweens'> extends BasePluginOptions<G> {
}
export interface TweenOptions {
/** Explicit starting value (default: captures current value on first tick) */
from?: number;
/** Easing function (default: linear) */
easing?: EasingFn;
/** Loop mode (default: 'once') */
loop?: LoopMode;
/** Number of loops. -1 = infinite (default: 1) */
loops?: number;
/** Callback invoked when tween completes */
onComplete?: (data: TweenEventData) => void;
}
/**
* Create a single-target tween component.
*
* @param component Component name on the entity
* @param field Field path (dot-separated for nested, e.g. 'position.x')
* @param to Target value
* @param duration Duration in seconds
* @param options Optional configuration
* @returns Component object suitable for spreading into spawn()
*/
export declare function createTween(component: string, field: string, to: number, duration: number, options?: TweenOptions): Pick<TweenComponentTypes, 'tween'>;
export interface TweenSequenceStepInput {
targets: ReadonlyArray<{
component: string;
field: string;
to: number;
from?: number;
}>;
duration: number;
easing?: EasingFn;
}
export interface TweenSequenceOptions {
/** Loop mode (default: 'once') */
loop?: LoopMode;
/** Number of loops. -1 = infinite (default: 1) */
loops?: number;
/** Callback invoked when tween completes */
onComplete?: (data: TweenEventData) => void;
}
/**
* Create a multi-step tween sequence. Each step can have parallel targets.
*
* @param steps Array of step definitions
* @param options Optional configuration
* @returns Component object suitable for spreading into spawn()
*/
export declare function createTweenSequence(steps: ReadonlyArray<TweenSequenceStepInput>, options?: TweenSequenceOptions): Pick<TweenComponentTypes, 'tween'>;
/**
* Recursively produce a union of dot-separated paths that resolve to `number`
* within type T. Depth-limited to 4 levels to prevent TS recursion errors.
*
* @example
* NumericPaths<{ x: number; y: number }> // 'x' | 'y'
* NumericPaths<{ position: { x: number }; rotation: number }> // 'position.x' | 'rotation'
*/
export type NumericPaths<T, Depth extends readonly unknown[] = []> = Depth['length'] extends 4 ? never : T extends readonly unknown[] ? never : T extends Record<string, unknown> ? {
[K in keyof T & string]: NonNullable<T[K]> extends number ? K : NonNullable<T[K]> extends readonly unknown[] ? never : NonNullable<T[K]> extends Record<string, unknown> ? `${K}.${NumericPaths<NonNullable<T[K]>, [...Depth, unknown]>}` : never;
}[keyof T & string] : never;
/**
* Discriminated union over component names: each variant constrains `field`
* to the numeric paths of that component. TS narrows inline object literals
* by `component` discriminant — zero runtime overhead.
*/
export type TypedTweenTargetInput<C extends Record<string, any>> = {
[K in keyof C & string]: {
component: K;
field: NumericPaths<C[K]>;
to: number;
from?: number;
};
}[keyof C & string];
export interface TypedTweenSequenceStepInput<C extends Record<string, any>> {
targets: ReadonlyArray<TypedTweenTargetInput<C>>;
duration: number;
easing?: EasingFn;
}
export interface TweenHelpers<W extends AnyECSpresso> {
createTween: <K extends keyof ComponentsOfWorld<W> & string>(component: K, field: NumericPaths<ComponentsOfWorld<W>[K]>, to: number, duration: number, options?: {
from?: number;
easing?: EasingFn;
loop?: LoopMode;
loops?: number;
onComplete?: (data: TweenEventData) => void;
}) => Pick<TweenComponentTypes, 'tween'>;
createTweenSequence: (steps: ReadonlyArray<TypedTweenSequenceStepInput<ComponentsOfWorld<W>>>, options?: {
loop?: LoopMode;
loops?: number;
onComplete?: (data: TweenEventData) => void;
}) => Pick<TweenComponentTypes, 'tween'>;
}
export declare function createTweenHelpers<W extends AnyECSpresso>(_world?: W): TweenHelpers<W>;
/**
* Create a tween plugin for ECSpresso.
*
* This plugin provides:
* - Tween system that processes all tween components each frame
* - Support for single-field, multi-target, and multi-step sequences
* - 31 standard easing functions
* - Loop modes: once, loop, yoyo
* - `justFinished` flag for one-frame completion detection
* - `onComplete` callback on completion
* - Change detection via markChanged
*/
export declare function createTweenPlugin<G extends string = 'tweens'>(options?: TweenPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TweenComponentTypes>, import("ecspresso").EmptyConfig, "tween-update", G, never, never>;