ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
295 lines (294 loc) • 12.5 kB
TypeScript
/**
* 2D Renderer Plugin for ECSpresso
*
* An opt-in PixiJS-based 2D rendering plugin that automates scene graph wiring.
* Import from 'ecspresso/plugins/rendering/renderer2D'
*
* This plugin includes transform propagation automatically.
*/
import type { Application, ApplicationOptions, Container, Sprite, Graphics } from 'pixi.js';
import { type Plugin } from 'ecspresso';
import type { ComponentsConfig, EmptyConfig, EventsConfig, ResourcesConfig, WorldConfig } from '../../type-utils';
import { type LocalTransform, type WorldTransform, type TransformComponentTypes, type TransformPluginOptions } from 'ecspresso/plugins/spatial/transform';
import { type BoundsRect } from 'ecspresso/plugins/spatial/bounds';
import type { CameraResourceTypes } from 'ecspresso/plugins/spatial/camera';
export type { LocalTransform, WorldTransform, TransformComponentTypes };
export type { BoundsRect };
export { createTransform, createLocalTransform, createWorldTransform, DEFAULT_LOCAL_TRANSFORM, DEFAULT_WORLD_TRANSFORM } from 'ecspresso/plugins/spatial/transform';
/**
* Visibility and alpha component
*/
export interface Visible {
visible: boolean;
alpha?: number;
}
/**
* Aggregate component types for the 2D renderer plugin.
* Included automatically via `.withPlugin(createRenderer2DPlugin({ ... }))`.
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createRenderer2DPlugin({ ... }))
* .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()
* .build();
* ```
*/
export interface Renderer2DComponentTypes extends TransformComponentTypes {
sprite: Sprite;
graphics: Graphics;
container: Container;
visible: Visible;
/** Assigns the entity to a named render layer for z-ordering */
renderLayer: string;
}
/**
* Events emitted by the 2D renderer plugin
*/
export interface Renderer2DEventTypes {
hierarchyChanged: {
entityId: number;
oldParent: number | null;
newParent: number | null;
};
}
/**
* Resources provided by the 2D renderer plugin
*/
export interface Renderer2DResourceTypes {
pixiApp: Application;
rootContainer: Container;
/** Screen bounds derived from PixiJS screen dimensions, updated on resize */
bounds: BoundsRect;
}
export type ScaleMode = 'fit' | 'cover' | 'stretch';
export interface ScreenScaleOptions {
readonly width: number;
readonly height: number;
readonly mode?: ScaleMode;
}
export interface ViewportScale {
scaleX: number;
scaleY: number;
offsetX: number;
offsetY: number;
physicalWidth: number;
physicalHeight: number;
/** Current scale mode. Mutable — call `reapplyViewportScale(pixiApp)` after changing to re-apply immediately. */
mode: ScaleMode;
readonly designWidth: number;
readonly designHeight: number;
}
export interface ViewportScaleResourceTypes {
viewportScale: ViewportScale;
}
/**
* Common options shared between both initialization modes
*/
interface Renderer2DPluginCommonOptions<G extends string = 'renderer2d'> {
/** Optional custom root container (defaults to app.stage) */
rootContainer?: Container;
/** System group name (default: 'renderer2d') */
systemGroup?: G;
/** Priority for render sync system (default: 500) */
renderSyncPriority?: number;
/** Options for the included transform plugin */
transform?: TransformPluginOptions;
/** When true, wires up pixiApp.ticker to drive ecs.update() automatically (default: true) */
startLoop?: boolean;
/** Ordered render layer names (back-to-front). Entities with a renderLayer component are placed in the corresponding container. */
renderLayers?: string[];
/** Render layers that should not be affected by camera transforms.
* These layers are placed outside rootContainer so camera zoom/pan/rotation does not apply.
* Only relevant when `camera: true`. Layer names listed here must also appear in `renderLayers`. */
screenSpaceLayers?: string[];
/** Automatically apply cameraState resource to rootContainer each frame.
* Requires the camera plugin to be installed. (default: false) */
camera?: boolean;
/** Enforce a logical design resolution with automatic aspect-ratio-aware scaling.
* When set, systems work in design-resolution coordinate space. */
screenScale?: ScreenScaleOptions;
}
/**
* Options when providing a pre-initialized PixiJS Application
*/
export interface Renderer2DPluginAppOptions<G extends string = 'renderer2d'> extends Renderer2DPluginCommonOptions<G> {
/** The PixiJS Application instance (already initialized) */
app: Application;
pixiInit?: never;
container?: never;
background?: never;
width?: never;
height?: never;
}
/**
* Options when letting the plugin create and manage the PixiJS Application
*/
export interface Renderer2DPluginManagedOptions<G extends string = 'renderer2d'> extends Renderer2DPluginCommonOptions<G> {
app?: never;
/** Container element to append the canvas to (or CSS selector string). Defaults to `document.body`.
* The canvas also auto-resizes to this element unless `width`/`height` are set or `pixiInit.resizeTo` is set explicitly. */
container?: HTMLElement | string;
/** Canvas background color. */
background?: ApplicationOptions['background'];
/** Fixed canvas width. When set (with `height`), the canvas is fixed-size and the auto-resize default is suppressed. */
width?: ApplicationOptions['width'];
/** Fixed canvas height. When set (with `width`), the canvas is fixed-size and the auto-resize default is suppressed. */
height?: ApplicationOptions['height'];
/** Escape hatch for raw PixiJS ApplicationOptions not otherwise exposed at the top level.
* Top-level fields (`background`, `width`, `height`) take precedence when both are set. */
pixiInit?: Partial<ApplicationOptions>;
}
/**
* Configuration options for the 2D renderer plugin.
*
* Supports two modes:
* 1. **Pre-initialized**: Pass an already-initialized Application via `app`
* 2. **Managed**: Omit `app` and the plugin creates the Application during `ecs.initialize()`.
* The canvas is appended to `container` (defaults to `document.body`) and auto-resizes to
* match it. Pass `pixiInit: { width, height }` for a fixed-size canvas instead.
*
* This plugin includes transform propagation automatically - no need to add createTransformPlugin() separately.
*
* @example Pre-initialized mode (full control)
* ```typescript
* const app = new Application();
* await app.init({ resizeTo: window });
* const ecs = ECSpresso.create()
* .withPlugin(createRenderer2DPlugin({ app }))
* .withComponentTypes<{ player: true }>()
* .build();
* ```
*
* @example Managed mode (convenience)
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createRenderer2DPlugin({
* background: '#1099bb',
* }))
* .withComponentTypes<{ player: true }>()
* .build();
* await ecs.initialize(); // Application created here
* ```
*/
export type Renderer2DPluginOptions<G extends string = 'renderer2d'> = Renderer2DPluginAppOptions<G> | Renderer2DPluginManagedOptions<G>;
interface PositionOption {
x?: number;
y?: number;
}
interface TransformOptions {
rotation?: number;
scale?: number | {
x: number;
y: number;
};
visible?: boolean;
alpha?: number;
}
/**
* Create components for a sprite entity.
* Returns an object suitable for spreading into spawn().
*
* @example
* ```typescript
* const player = ecs.spawn({
* ...createSpriteComponents(new Sprite(texture), { x: 100, y: 100 }),
* velocity: { x: 0, y: 0 },
* });
* ```
*/
export declare function createSpriteComponents(sprite: Sprite, position?: PositionOption, options?: TransformOptions & {
anchor?: {
x: number;
y: number;
};
}): Pick<Renderer2DComponentTypes, 'sprite' | 'localTransform' | 'worldTransform' | 'visible'>;
/**
* Create components for a graphics entity.
* Returns an object suitable for spreading into spawn().
*
* @example
* ```typescript
* const rect = ecs.spawn({
* ...createGraphicsComponents(graphics, { x: 50, y: 50 }),
* });
* ```
*/
export declare function createGraphicsComponents(graphics: Graphics, position?: PositionOption, options?: TransformOptions): Pick<Renderer2DComponentTypes, 'graphics' | 'localTransform' | 'worldTransform' | 'visible'>;
/**
* Create components for a container entity.
* Returns an object suitable for spreading into spawn().
*
* @example
* ```typescript
* const group = ecs.spawn({
* ...createContainerComponents(new Container(), { x: 0, y: 0 }),
* });
* ```
*/
export declare function createContainerComponents(container: Container, position?: PositionOption, options?: TransformOptions): Pick<Renderer2DComponentTypes, 'container' | 'localTransform' | 'worldTransform' | 'visible'>;
export declare function computeViewportScale(physicalW: number, physicalH: number, designW: number, designH: number, mode: ScaleMode): ViewportScale;
/**
* Convert physical canvas pixel coordinates to design-resolution (logical) coordinates.
* Compose with camera `screenToWorld()` for full physical→world conversion.
*/
export declare function physicalToLogical(physicalX: number, physicalY: number, viewport: ViewportScale): {
x: number;
y: number;
};
/**
* Convert a DOM pointer event's client coordinates to design-resolution (logical) coordinates.
* Handles canvas offset, CSS-pixel to physical-pixel scaling, and viewport letterbox/crop offsets.
* Suitable for wiring into the input plugin's `coordinateTransform` option.
*/
export declare function clientToLogical(clientX: number, clientY: number, canvas: HTMLCanvasElement, viewport: ViewportScale): {
x: number;
y: number;
};
/**
* Re-apply the current viewport scale using the latest `mode` from the `viewportScale` resource.
* Call after mutating `viewportScale.mode` to take effect immediately without waiting for a window resize.
*/
export declare function reapplyViewportScale(pixiApp: Application): void;
/**
* Create a 2D rendering plugin for ECSpresso.
*
* This plugin provides:
* - Transform propagation (localTransform -> worldTransform)
* - Render sync system (updates PixiJS objects from ECS components)
* - Scene graph management (mirrors ECS hierarchy in PixiJS scene graph)
*
* @example Pre-initialized mode
* ```typescript
* const app = new Application();
* await app.init({ resizeTo: window });
*
* const ecs = ECSpresso.create<GameComponents, {}, {}>()
* .withPlugin(createRenderer2DPlugin({ app }))
* .build();
* ```
*
* @example Managed mode
* ```typescript
* const ecs = ECSpresso.create<GameComponents, {}, {}>()
* .withPlugin(createRenderer2DPlugin({
* background: '#1099bb',
* }))
* .build();
* await ecs.initialize();
* ```
*/
type Renderer2DLabels = 'renderer2d-sync' | 'renderer2d-scene-graph' | 'renderer2d-camera-sync' | 'transform-propagation';
type Renderer2DReactiveQueryNames = 'renderer2d-sprites' | 'renderer2d-graphics' | 'renderer2d-containers';
type Renderer2DWorldConfig<R extends WorldConfig['resources'] = Renderer2DResourceTypes> = ComponentsConfig<Renderer2DComponentTypes> & EventsConfig<Renderer2DEventTypes> & ResourcesConfig<R>;
export declare function createRenderer2DPlugin<G extends string = 'renderer2d'>(options: Renderer2DPluginOptions<G> & {
screenScale: ScreenScaleOptions;
camera: true;
}): Plugin<Renderer2DWorldConfig<Renderer2DResourceTypes & ViewportScaleResourceTypes & CameraResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;
export declare function createRenderer2DPlugin<G extends string = 'renderer2d'>(options: Renderer2DPluginOptions<G> & {
screenScale: ScreenScaleOptions;
}): Plugin<Renderer2DWorldConfig<Renderer2DResourceTypes & ViewportScaleResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;
export declare function createRenderer2DPlugin<G extends string = 'renderer2d'>(options: Renderer2DPluginOptions<G> & {
camera: true;
}): Plugin<Renderer2DWorldConfig<Renderer2DResourceTypes & CameraResourceTypes>, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;
export declare function createRenderer2DPlugin<G extends string = 'renderer2d'>(options: Renderer2DPluginOptions<G>): Plugin<Renderer2DWorldConfig, EmptyConfig, Renderer2DLabels, G, never, Renderer2DReactiveQueryNames>;