ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
192 lines (191 loc) • 7.8 kB
TypeScript
/**
* 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 {};