@ue-too/board
Version:
<h1 align="center"> uē-tôo </h1> <p align="center"> pan, zoom, rotate, and more with your html canvas. </p>
183 lines (182 loc) • 6.68 kB
TypeScript
import { EventReactions, EventGuards, Guard, TemplateState, TemplateStateMachine } from "@ue-too/being";
import { TouchContext, TouchPoints } from "./touch-input-context";
import type { Point } from "@ue-too/math";
/**
* Possible states of the touch input state machine.
*
* @remarks
* State transitions:
* - **IDLE**: No touches active, or single touch (reserved for UI)
* - **PENDING**: Exactly two touches active, waiting for movement to determine gesture type
* - **IN_PROGRESS**: Two-finger gesture in progress (pan or zoom)
*
* The state machine only handles two-finger gestures. Single-finger touches are ignored
* to avoid interfering with UI interactions (button clicks, text selection, etc.).
*
* @category Input State Machine - Touch
*/
export type TouchStates = "IDLE" | "PENDING" | "IN_PROGRESS";
/**
* Payload for touch events containing active touch points.
*
* @property points - Array of touch points involved in this event
*
* @category Input State Machine - Touch
*/
export type TouchEventPayload = {
points: TouchPoints[];
};
/**
* Output events produced by the touch state machine for the orchestrator.
*
* @remarks
* Touch gestures are recognized from two-finger interactions:
*
* **Pan Gesture**:
* - Two fingers move in the same direction
* - Delta is calculated from the midpoint movement
* - Triggers when midpoint delta > distance delta
*
* **Zoom Gesture**:
* - Two fingers move toward/away from each other (pinch)
* - Delta is calculated from distance change between fingers
* - Anchor point is the midpoint between fingers
* - Triggers when distance delta > midpoint delta
*
* **Coordinate Spaces**:
* - Pan delta is in window pixels
* - Zoom anchor point is in viewport coordinates
*
* @category Input State Machine - Touch
*/
export type TouchOutputEvent = {
type: "pan";
delta: Point;
} | {
type: "zoom";
delta: number;
anchorPointInViewPort: Point;
} | {
type: "none";
};
/**
* Event mapping for the touch input state machine.
*
* @remarks
* Maps touch event names to their payload types. The state machine handles
* the three core touch events: touchstart, touchmove, and touchend.
*
* @category Input State Machine - Touch
*/
export type TouchEventMapping = {
touchstart: TouchEventPayload;
touchmove: TouchEventPayload;
touchend: TouchEventPayload;
};
/**
* Mapping of events to their output types.
*
* @remarks
* Only touchmove produces outputs (pan or zoom gestures).
* touchstart and touchend only manage state transitions.
*
* @category Input State Machine - Touch
*/
export type TouchInputEventOutputMapping = {
touchmove: TouchOutputEvent;
};
/**
* IDLE state - waiting for two-finger touch.
*
* @remarks
* This state handles touch lifecycle but only transitions to PENDING when exactly
* two touches are active. Single touches and three+ touches are ignored.
*
* **Guard Condition**:
* Transitions to PENDING only when `getCurrentTouchPointsCount() === 2`.
* This ensures the state machine only handles two-finger gestures.
*
* @category Input State Machine - Touch
*/
export declare class IdleState extends TemplateState<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping> {
protected _eventReactions: EventReactions<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping>;
protected _guards: Guard<TouchContext, "touchPointsCount">;
protected _eventGuards: Partial<EventGuards<TouchEventMapping, TouchStates, TouchContext, typeof this._guards>>;
touchstart(context: TouchContext, payload: TouchEventPayload): void;
touchend(context: TouchContext, payload: TouchEventPayload): void;
}
/**
* @description The pending state of the touch input state machine.
*
* @category Input State Machine
*/
export declare class PendingState extends TemplateState<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping> {
protected _eventReactions: EventReactions<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping>;
touchstart(context: TouchContext, payload: TouchEventPayload): void;
touchend(context: TouchContext, payload: TouchEventPayload): void;
touchmove(context: TouchContext, payload: TouchEventPayload): TouchOutputEvent;
}
/**
* @description The in progress state of the touch input state machine.
*
* @category Input State Machine
*/
export declare class InProgressState extends TemplateState<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping> {
protected _eventReactions: EventReactions<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping>;
touchmove(context: TouchContext, payload: TouchEventPayload): TouchOutputEvent;
touchend(context: TouchContext, payload: TouchEventPayload): void;
}
/**
* Type alias for the touch input state machine.
*
* @category Input State Machine - Touch
*/
export type TouchInputStateMachine = TemplateStateMachine<TouchEventMapping, TouchContext, TouchStates, TouchInputEventOutputMapping>;
/**
* Creates a new touch input state machine for multi-touch gesture recognition.
*
* @param context - The context providing touch point tracking and canvas access
* @returns A configured state machine ready to process touch events
*
* @remarks
* This factory creates a state machine that recognizes two-finger pan and pinch-to-zoom gestures.
*
* **State Flow**:
* ```
* IDLE → (2 touches start) → PENDING → (touch move) → IN_PROGRESS
* IN_PROGRESS → (touch end) → IDLE
* ```
*
* **Gesture Recognition Algorithm**:
* 1. Wait for exactly 2 touches (IDLE → PENDING)
* 2. On first move, determine gesture type:
* - If distance change > midpoint change: ZOOM
* - If midpoint change > distance change: PAN
* 3. Continue producing pan/zoom outputs until touches end
*
* **Pan Gesture**:
* Delta = current midpoint - initial midpoint
*
* **Zoom Gesture**:
* Delta = (current distance - initial distance) * 0.005
* Anchor = midpoint in viewport coordinates
*
* @category Input State Machine - Touch
*
* @example
* ```typescript
* const canvasProxy = new CanvasProxy(canvasElement);
* const context = new TouchInputTracker(canvasProxy);
* const stateMachine = createTouchInputStateMachine(context);
*
* // Process a touch start event with 2 fingers
* const result = stateMachine.happens("touchstart", {
* points: [
* {ident: 0, x: 100, y: 200},
* {ident: 1, x: 300, y: 200}
* ]
* });
* console.log(result.nextState); // "PENDING"
* ```
*/
export declare function createTouchInputStateMachine(context: TouchContext): TouchInputStateMachine;