UNPKG

ecspresso

Version:

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

295 lines (294 loc) 12.5 kB
/** * 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>;