@ue-too/board
Version:
<h1 align="center"> uē-tôo </h1> <p align="center"> pan, zoom, rotate, and more with your html canvas. </p>
365 lines (364 loc) • 14.4 kB
TypeScript
import { Point } from "@ue-too/math";
import { BaseContext } from "@ue-too/being";
import { CanvasPositionDimensionPublisher, Observable, Observer, SubscriptionOptions } from "../../utils";
/**
* Cursor styles used to provide visual feedback for different input states.
*
* @remarks
* These cursor styles indicate the current interaction mode to users:
* - **GRAB**: Indicates the canvas is ready to be panned (spacebar pressed, no drag yet)
* - **GRABBING**: Indicates active panning is in progress
* - **DEFAULT**: Normal cursor state when no special interaction is active
*
* @category Input State Machine
*/
export declare enum CursorStyle {
GRAB = "grab",
DEFAULT = "default",
GRABBING = "grabbing"
}
/**
* Canvas dimension and position information.
*
* @property width - The canvas width in CSS pixels
* @property height - The canvas height in CSS pixels
* @property position - The top-left position of the canvas in window coordinates
*
* @category Input State Machine
*/
export type CanvasDimensions = {
width: number;
height: number;
position: Point;
};
/**
* Abstraction interface for canvas element access and manipulation.
*
* @remarks
* This interface provides a decoupled way to access canvas properties without direct DOM access.
* Multiple implementations exist to support different use cases:
* - **CanvasProxy**: Full implementation for HTML canvas elements with dimension tracking
* - **SvgProxy**: Implementation for SVG elements
* - **DummyCanvas**: No-op implementation for web worker contexts
* - **WorkerRelayCanvas**: Relays canvas dimension updates to web workers
* - **CanvasCacheInWebWorker**: Caches canvas dimensions within a web worker
*
* The abstraction enables:
* - Coordinate system transformations (window → canvas → viewport)
* - Canvas dimension tracking without repeated DOM queries
* - Cursor style management
* - Support for both canvas and SVG rendering contexts
*
* @category Input State Machine
*/
export interface Canvas {
/** The canvas width in CSS pixels */
width: number;
/** The canvas height in CSS pixels */
height: number;
/** The top-left position of the canvas in window coordinates */
position: Point;
/** Sets the CSS cursor style for visual feedback */
setCursor: (style: CursorStyle) => void;
/** Combined dimensions and position information */
dimensions: CanvasDimensions;
/** Whether the canvas is currently detached from the DOM */
detached: boolean;
/** Cleanup method to dispose of resources and event listeners */
tearDown: () => void;
}
/**
* No-op implementation of Canvas interface for web worker relay contexts.
*
* @remarks
* This class is used when an input state machine is configured to relay events to a web worker
* rather than perform actual canvas operations. The state machine requires a Canvas in its context,
* but in the relay scenario, no actual canvas operations are needed - events are simply forwarded
* to the worker thread.
*
* All properties return default/empty values and all methods are no-ops.
*
* @category Input State Machine
*
* @see {@link DummyKmtInputContext}
*/
export declare class DummyCanvas implements Canvas {
width: number;
height: number;
position: Point;
setCursor: (style: CursorStyle) => void;
dimensions: {
width: number;
height: number;
position: Point;
};
detached: boolean;
tearDown: () => void;
}
export declare class CanvasCacheInWebWorker implements Canvas {
private _width;
private _height;
private _position;
private _postMessageFunction;
constructor(postMessageFunction: typeof postMessage);
get dimensions(): {
width: number;
height: number;
position: Point;
};
tearDown(): void;
set width(width: number);
set height(height: number);
set position(position: Point);
get width(): number;
get height(): number;
get position(): Point;
setCursor(style: "grab" | "default" | "grabbing"): void;
get detached(): boolean;
}
/**
* A proxy for the canvas element to prevent constant invoking of the getBoundingClientRect method.
* @remarks This is mainly used as a proxy to the canvas to prevent invoking the getBoundingClientRect method on the canvas every time a pointer event is triggered or a coordinate conversion is needed. Also to autoscale the canvas buffer depending on the device pixel ratio. It's important to note that in normal circumstances, you would not need to set the size of the canvas manually; you should use the css style width and height to set the size of the canvas.
* @category Input State Machine
* @see {@link Canvas} for the interface that this class implements
* @see {@link Observable} for the observable that this class emits
* @see {@link Observer} for the observer that can subscribe to the observable
* @see {@link SubscriptionOptions} for the options that can be passed to the subscribe method
* @see {@link SynchronousObservable} for the synchronous observable that this class emits
* @see {@link CanvasPositionDimensionPublisher} for the publisher that is used to publish the canvas dimensions
*/
export declare class CanvasProxy implements Canvas, Observable<[CanvasDimensions]> {
private _width;
private _height;
private _position;
private _canvasPositionDimensionPublisher;
private _canvas;
private _internalSizeUpdateObservable;
constructor(canvas?: HTMLCanvasElement);
subscribe(observer: Observer<[CanvasDimensions]>, options?: SubscriptionOptions): () => void;
notify(...data: [CanvasDimensions]): void;
get detached(): boolean;
get dimensions(): {
width: number;
height: number;
position: Point;
};
get width(): number;
/**
* set the width of the canvas
* the width is synonymous with the canvas style width not the canvas width
*/
setWidth(width: number): void;
setCanvasWidth(width: number): void;
/**
* set the height of the canvas
* the height is synonymous with the canvas style height not the canvas height
*/
setHeight(height: number): void;
setCanvasHeight(height: number): void;
get height(): number;
get position(): Point;
setCursor(style: "grab" | "default" | "grabbing"): void;
tearDown(): void;
attach(canvas: HTMLCanvasElement): void;
logCanvasTrueSize(): void;
}
export declare class SvgProxy implements Canvas, Observable<[CanvasDimensions]> {
private _width;
private _height;
private _position;
private _svgPositionDimensionPublisher;
private _svg;
private _internalSizeUpdateObservable;
constructor(svg?: SVGSVGElement);
subscribe(observer: Observer<[CanvasDimensions]>, options?: SubscriptionOptions): () => void;
notify(...data: [CanvasDimensions]): void;
get detached(): boolean;
get dimensions(): {
width: number;
height: number;
position: Point;
};
get width(): number;
/**
* set the width of the canvas
* the width is synonymous with the canvas style width not the canvas width
*/
setWidth(width: number): void;
/**
* set the height of the canvas
* the height is synonymous with the canvas style height not the canvas height
*/
setHeight(height: number): void;
get height(): number;
get position(): Point;
setCursor(style: "grab" | "default" | "grabbing"): void;
tearDown(): void;
attach(svg: SVGSVGElement): void;
logCanvasTrueSize(): void;
}
/**
* @description A proxy for the canvas that is used to communicate with the web worker.
* The primary purpose of this class is to cache the canvas dimensions and position in the DOM to reduce the calling of the getBoundingClientRect method.
* This class only serves as a relay of the updated canvas dimensions and position to the web worker.
*
*/
export declare class WorkerRelayCanvas implements Canvas {
private _width;
private _height;
private _position;
private _webWorker;
private _canvas;
private _canvasDiemsionPublisher;
constructor(canvas: HTMLCanvasElement, webWorker: Worker, canvasDiemsionPublisher: CanvasPositionDimensionPublisher);
get width(): number;
get height(): number;
tearDown(): void;
get position(): Point;
get dimensions(): {
width: number;
height: number;
position: Point;
};
get detached(): boolean;
setCursor(style: "grab" | "default" | "grabbing"): void;
}
/**
* Context interface for the Keyboard/Mouse/Trackpad (KMT) input state machine.
*
* @remarks
* This context provides the state and behavior needed by the KMT state machine to:
* 1. Track cursor positions for calculating pan deltas
* 2. Distinguish between mouse and trackpad input modalities
* 3. Access canvas dimensions for coordinate transformations
* 4. Manage coordinate system alignment (inverted Y-axis handling)
*
* **Input Modality Detection**:
* The context uses a scoring system (`kmtTrackpadTrackScore`) to differentiate between
* mouse and trackpad input, which have different zoom behaviors:
* - Mouse: Ctrl+Scroll = zoom, Scroll = pan
* - Trackpad: Scroll = zoom (no Ctrl needed), Two-finger gesture = pan
*
* **Coordinate System**:
* The `alignCoordinateSystem` flag determines Y-axis orientation:
* - `true`: Standard screen coordinates (Y increases downward)
* - `false`: Inverted coordinates (Y increases upward)
*
* This interface extends BaseContext from the \@ue-too/being state machine library,
* inheriting setup() and cleanup() lifecycle methods.
*
* @category Input State Machine
*/
export interface KmtInputContext extends BaseContext {
/** Whether to use standard screen coordinate system (vs inverted Y-axis) */
alignCoordinateSystem: boolean;
/** Canvas accessor for dimensions and cursor control */
canvas: Canvas;
/** Sets the initial cursor position when starting a pan gesture */
setInitialCursorPosition: (position: Point) => void;
/** Cancels the current action and resets cursor position */
cancelCurrentAction: () => void;
/** The cursor position when a pan gesture started */
initialCursorPosition: Point;
/** Score tracking input modality: >0 for mouse, <0 for trackpad, 0 for undetermined */
kmtTrackpadTrackScore: number;
/** Decreases the score toward trackpad */
subtractKmtTrackpadTrackScore: () => void;
/** Increases the score toward mouse */
addKmtTrackpadTrackScore: () => void;
/** Sets the determined input modality */
setMode: (mode: 'kmt' | 'trackpad' | 'TBD') => void;
/** The current input modality: 'kmt' (mouse), 'trackpad', or 'TBD' (to be determined) */
mode: 'kmt' | 'trackpad' | 'TBD';
}
/**
* No-op implementation of KmtInputContext for web worker relay scenarios.
*
* @remarks
* Used when the input state machine is configured to relay events to a web worker
* rather than process them locally. The state machine requires a context, but in
* the relay scenario, no actual state tracking is needed - events are simply forwarded.
*
* All methods are no-ops and all properties return default values.
*
* @category Input State Machine
*
* @see {@link DummyCanvas}
*/
export declare class DummyKmtInputContext implements KmtInputContext {
alignCoordinateSystem: boolean;
canvas: Canvas;
initialCursorPosition: Point;
constructor();
toggleOnEdgeAutoCameraInput: () => void;
toggleOffEdgeAutoCameraInput: () => void;
setCursorPosition: (position: Point) => void;
setInitialCursorPosition(position: Point): void;
cleanup(): void;
setup(): void;
get kmtTrackpadTrackScore(): number;
subtractKmtTrackpadTrackScore(): void;
addKmtTrackpadTrackScore(): void;
setMode(mode: 'kmt' | 'trackpad' | 'TBD'): void;
get mode(): 'kmt' | 'trackpad' | 'TBD';
cancelCurrentAction(): void;
}
/**
* Production implementation of KmtInputContext that tracks input state for the state machine.
*
* @remarks
* This class provides the concrete implementation of the KMT input context, maintaining
* all state required by the state machine to recognize and track gestures:
*
* **State Tracking**:
* - Initial cursor position for calculating pan deltas
* - Input modality score to distinguish mouse vs trackpad
* - Determined input mode (kmt/trackpad/TBD)
* - Coordinate system alignment preference
*
* **Input Modality Detection**:
* The `kmtTrackpadTrackScore` accumulates evidence about the input device:
* - Positive values indicate mouse behavior (middle-click, no horizontal scroll)
* - Negative values indicate trackpad behavior (horizontal scroll, two-finger gestures)
* - Score is used to determine zoom behavior (Ctrl+Scroll for mouse vs Scroll for trackpad)
*
* **Design Pattern**:
* This class follows the Context pattern from the @ue-too/being state machine library,
* providing stateful data and operations that states can access and modify during transitions.
*
* @category Input State Machine
*
* @example
* ```typescript
* const canvasProxy = new CanvasProxy(canvasElement);
* const context = new ObservableInputTracker(canvasProxy);
* const stateMachine = createKmtInputStateMachine(context);
*
* // Context tracks state as the state machine processes events
* stateMachine.happens("leftPointerDown", {x: 100, y: 200});
* console.log(context.initialCursorPosition); // {x: 100, y: 200}
* ```
*/
export declare class ObservableInputTracker implements KmtInputContext {
private _alignCoordinateSystem;
private _canvasOperator;
private _initialCursorPosition;
private _kmtTrackpadTrackScore;
private _mode;
private _deciding;
constructor(canvasOperator: Canvas);
get mode(): 'kmt' | 'trackpad' | 'TBD';
setMode(mode: 'kmt' | 'trackpad' | 'TBD'): void;
enableInputModeDetection(): void;
get kmtTrackpadTrackScore(): number;
subtractKmtTrackpadTrackScore(): void;
addKmtTrackpadTrackScore(): void;
get alignCoordinateSystem(): boolean;
get canvas(): Canvas;
get initialCursorPosition(): Point;
set alignCoordinateSystem(value: boolean);
cancelCurrentAction(): void;
setInitialCursorPosition(position: Point): void;
cleanup(): void;
setup(): void;
}