UNPKG

ecspresso

Version:

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

192 lines (191 loc) 7.8 kB
/** * UI / HUD Plugin for ECSpresso. * * Screen-space primitives: * - `uiElement` — anchor/pivot/offset positioning resolved against the `bounds` resource * - `uiLabel` — PixiJS Text * - `uiPanel` — PixiJS Graphics rectangle with optional border * - `uiProgressBar` — PixiJS Graphics value indicator with four fill directions * * Pointer interaction (buttons): * - `uiInteractive` (marker) opts an entity into hit-testing * - `uiInteraction.state` — `'none' | 'hover' | 'pressed'` (Bevy-style single enum) * - `uiButton` marker composes `uiInteractive` + `uiInteraction` * - `uiDisabled` skips hit-testing entirely * - Emits `uiButtonPressed` (confirmed down→up on same widget) and `uiButtonHovered` * * Depends on `renderer2D` (for the `bounds` resource + scene graph + screen-space layer), * the transform plugin (bundled by renderer2D), and the input plugin. * * Future phases will add the message log (Phase 3). */ import { type BasePluginOptions } from 'ecspresso'; import type { ComponentsConfig, ResourcesConfig } from '../../type-utils'; import type { Vector2D } from '../../utils/math'; import { type TransformComponentTypes } from '../spatial/transform'; import type { BoundsResourceTypes } from '../spatial/bounds'; import type { InputResourceTypes } from '../input/input'; export type AnchorPreset = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; export declare const ANCHOR_PRESETS: Readonly<Record<AnchorPreset, Readonly<Vector2D>>>; export type AnchorInput = AnchorPreset | Vector2D; /** Resolve a preset string or vec2 into a mutable Vector2D copy. */ export declare function resolveAnchorPreset(input: AnchorInput): Vector2D; /** * Write the top-left screen position of a widget into `out`. * * Formula: position = anchor * bounds + offset - pivot * size. * `anchor` specifies where on the canvas the widget attaches (0..1 normalized). * `pivot` specifies where on the widget that attachment point lands (0..1 normalized). * Writes in place to avoid per-frame allocation. */ export declare function resolveAnchorPosition(anchor: Readonly<Vector2D>, pivot: Readonly<Vector2D>, offset: Readonly<Vector2D>, bounds: Readonly<{ width: number; height: number; }>, size: Readonly<{ width: number; height: number; }>, out: Vector2D): void; export type ProgressDirection = 'ltr' | 'rtl' | 'ttb' | 'btt'; export interface FillRect { x: number; y: number; width: number; height: number; } export declare function clampProgressValue(value: number, max: number): number; export declare function computeProgressFillRect(width: number, height: number, ratio: number, direction: ProgressDirection, out: FillRect): void; export interface UIElement { anchor: Vector2D; pivot: Vector2D; offset: Vector2D; width: number; height: number; } export interface UITextStyle { fontFamily: string; fontSize: number; fill: number; align: 'left' | 'center' | 'right'; } export interface UILabel { text: string; style: UITextStyle; } export interface UIPanel { fillColor: number; borderColor?: number; borderWidth: number; } export interface UIProgressBar { value: number; max: number; fillColor: number; bgColor: number; direction: ProgressDirection; } export type UIInteractionState = 'none' | 'hover' | 'pressed'; export interface UIInteraction { state: UIInteractionState; } export interface LogFragment { text: string; color: number; } export interface UIMessageLog { lines: LogFragment[][]; maxLines: number; visibleLines: number; lineHeight: number; style: UITextStyle; } export interface UIComponentTypes { uiElement: UIElement; uiLabel: UILabel; uiPanel: UIPanel; uiProgressBar: UIProgressBar; uiButton: {}; uiInteractive: {}; uiInteraction: UIInteraction; uiDisabled: {}; uiMessageLog: UIMessageLog; } export interface UIButtonPressedEvent { entityId: number; } export interface UIButtonHoveredEvent { entityId: number; entered: boolean; } export interface UIMessageLogAppendedEvent { entityId: number; line: LogFragment[]; } export interface UIEventTypes { uiButtonPressed: UIButtonPressedEvent; uiButtonHovered: UIButtonHoveredEvent; uiLogAppended: UIMessageLogAppendedEvent; } export interface CreateUIElementInput { anchor: AnchorInput; pivot?: AnchorInput; offset?: Vector2D; width: number; height: number; } export declare function createUIElement(input: CreateUIElementInput): Pick<UIComponentTypes, 'uiElement'>; export declare function createUILabel(text: string, style?: Partial<UITextStyle>): Pick<UIComponentTypes, 'uiLabel'>; export interface CreateUIPanelInput { fillColor: number; borderColor?: number; borderWidth?: number; } export declare function createUIPanel(input: CreateUIPanelInput): Pick<UIComponentTypes, 'uiPanel'>; export interface CreateUIProgressBarInput { value: number; max: number; fillColor: number; bgColor: number; direction?: ProgressDirection; } export declare function createUIProgressBar(input: CreateUIProgressBarInput): Pick<UIComponentTypes, 'uiProgressBar'>; export interface CreateUIMessageLogInput { maxLines: number; visibleLines: number; lineHeight: number; style?: Partial<UITextStyle>; initialLines?: LogFragment[][]; } export declare function createUIMessageLog(input: CreateUIMessageLogInput): Pick<UIComponentTypes, 'uiMessageLog'>; export declare function createUIInteractive(): Pick<UIComponentTypes, 'uiInteractive'>; export declare function createUIButton(): Pick<UIComponentTypes, 'uiButton'>; export declare function createUIDisabled(): Pick<UIComponentTypes, 'uiDisabled'>; /** Structural ECS surface for `appendLogLine`; mirrors the `CoroutineWorld` pattern. */ export interface MessageLogWorld { commands: { mutateComponent(entityId: number, componentName: 'uiMessageLog', mutator: (value: UIMessageLog) => void): void; }; eventBus: { publish(event: 'uiLogAppended', payload: UIMessageLogAppendedEvent): void; }; } /** * Append a line (vector of fragments) to a `uiMessageLog` entity. * * Queues a buffered mutation that swaps `lines` for a fresh array (FIFO-truncated to * `maxLines`) — the array-identity change is the sync system's redraw signal — and * synchronously publishes `uiLogAppended` carrying the line for entry-animation * listeners. Safe to call from inside a system process callback. */ export declare function appendLogLine(ecs: MessageLogWorld, entityId: number, line: LogFragment[]): void; type UIRequires = ComponentsConfig<TransformComponentTypes> & ResourcesConfig<BoundsResourceTypes & InputResourceTypes>; type UILabels = 'ui-anchor-resolve' | 'ui-interaction' | 'ui-label-sync' | 'ui-panel-sync' | 'ui-progress-sync' | 'ui-message-log-sync'; export interface UIPluginOptions<G extends string = 'ui'> extends BasePluginOptions<G> { /** Priority for the anchor-resolve system in preUpdate (default: 0). */ anchorPriority?: number; /** Priority for the pointer hit-test system in preUpdate (default: 200, after input's 100). */ interactionPriority?: number; /** Priority for render-sync systems (default: 480, just before renderer2D's 500). */ renderSyncPriority?: number; } export declare function createUIPlugin<G extends string = 'ui'>(options?: UIPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, UIComponentTypes>, UIEventTypes>, UIRequires, UILabels, G, never, "ui-labels" | "ui-panels" | "ui-progress-bars" | "ui-message-logs">; export {};