UNPKG

@ue-too/board

Version:

<h1 align="center"> uē-tôo </h1> <p align="center"> pan, zoom, rotate, and more with your html canvas. </p>

334 lines (333 loc) 13.2 kB
import { Point } from '@ue-too/math'; import { Boundaries } from './utils/position'; import { TransformationMatrix } from './utils/matrix'; import { UnSubscribe } from './update-publisher'; import { ZoomLevelLimits } from './utils/zoom'; import { RotationLimits } from './utils/rotation'; import { CameraEventMap, CameraState } from './update-publisher'; import { ObservableBoardCamera } from './interface'; import { SubscriptionOptions } from '../utils/observable'; /** Default viewport width in CSS pixels */ export declare const DEFAULT_BOARD_CAMERA_VIEWPORT_WIDTH = 1000; /** Default viewport height in CSS pixels */ export declare const DEFAULT_BOARD_CAMERA_VIEWPORT_HEIGHT = 1000; /** Default zoom level constraints (0.1x to 10x) */ export declare const DEFAULT_BOARD_CAMERA_ZOOM_BOUNDARIES: ZoomLevelLimits; /** Default position boundaries (±10000 on both axes) */ export declare const DEFAULT_BOARD_CAMERA_BOUNDARIES: Boundaries; /** Default rotation boundaries (unrestricted) */ export declare const DEFAULT_BOARD_CAMERA_ROTATION_BOUNDARIES: RotationLimits | undefined; /** * Observable camera implementation that extends {@link BaseCamera} with event notification. * This is the recommended camera class for most applications. * * @remarks * DefaultBoardCamera wraps {@link BaseCamera} and adds an event system via {@link CameraUpdatePublisher}. * All camera state changes (pan, zoom, rotate) trigger corresponding events that observers can subscribe to. * * Use this class when you need to: * - React to camera changes in your UI or game logic * - Synchronize multiple systems with camera state * - Implement camera-dependent features (minimap, LOD, culling) * * For a non-observable camera without event overhead, use {@link BaseCamera} directly. * * @example * ```typescript * const camera = new DefaultBoardCamera(1920, 1080); * * // Subscribe to camera events * camera.on('zoom', (event, state) => { * console.log(`Zoomed by ${event.deltaZoomAmount}`); * console.log(`New zoom level: ${state.zoomLevel}`); * }); * * camera.on('pan', (event, state) => { * console.log(`Panned by (${event.diff.x}, ${event.diff.y})`); * }); * * // Camera updates trigger events * camera.setZoomLevel(2.0); * camera.setPosition({ x: 100, y: 200 }); * ``` * * @category Camera * @see {@link BaseCamera} for non-observable camera * @see {@link ObservableBoardCamera} for the interface definition */ export default class DefaultBoardCamera implements ObservableBoardCamera { private _baseCamera; private _observer; /** * Creates a new observable camera with event notification capabilities. * * @param viewPortWidth - Width of the viewport in CSS pixels (default: 1000) * @param viewPortHeight - Height of the viewport in CSS pixels (default: 1000) * @param position - Initial camera position in world coordinates (default: {x: 0, y: 0}) * @param rotation - Initial rotation in radians (default: 0) * @param zoomLevel - Initial zoom level (default: 1.0) * @param boundaries - Position constraints (default: ±10000 on both axes) * @param zoomLevelBoundaries - Zoom constraints (default: 0.1 to 10) * @param rotationBoundaries - Optional rotation constraints (default: unrestricted) * * @example * ```typescript * // Camera with default settings * const camera1 = new DefaultBoardCamera(); * * // Camera with custom viewport * const camera2 = new DefaultBoardCamera(1920, 1080); * * // Camera with all options * const camera3 = new DefaultBoardCamera( * 1920, 1080, * { x: 0, y: 0 }, * 0, * 1.0, * { min: { x: -5000, y: -5000 }, max: { x: 5000, y: 5000 } }, * { min: 0.5, max: 4 }, * { start: 0, end: Math.PI * 2 } * ); * ``` */ constructor(viewPortWidth?: number, viewPortHeight?: number, position?: Point, rotation?: number, zoomLevel?: number, boundaries?: Boundaries, zoomLevelBoundaries?: ZoomLevelLimits, rotationBoundaries?: RotationLimits | undefined); /** * @description The boundaries of the camera in the world coordinate system. * * @category Camera */ get boundaries(): Boundaries | undefined; set boundaries(boundaries: Boundaries | undefined); /** * @description The width of the viewport. (The width of the canvas in css pixels) * * @category Camera */ get viewPortWidth(): number; set viewPortWidth(width: number); /** * @description The height of the viewport. (The height of the canvas in css pixels) * * @category Camera */ get viewPortHeight(): number; set viewPortHeight(height: number); /** * @description The position of the camera in the world coordinate system. * * @category Camera */ get position(): Point; /** * Sets the camera position and notifies observers if successful. * * @param destination - Target position in world coordinates * @returns True if position was updated, false if rejected by boundaries or negligible change * * @remarks * If the position changes, a 'pan' event is triggered with the position delta and new camera state. * All 'pan' and 'all' event subscribers will be notified. * * @example * ```typescript * camera.on('pan', (event, state) => { * console.log(`Camera moved by (${event.diff.x}, ${event.diff.y})`); * }); * * camera.setPosition({ x: 100, y: 200 }); // Triggers pan event * ``` */ setPosition(destination: Point): boolean; /** * @description The zoom level of the camera. * * @category Camera */ get zoomLevel(): number; /** * @description The boundaries of the zoom level of the camera. * * @category Camera */ get zoomBoundaries(): ZoomLevelLimits | undefined; set zoomBoundaries(zoomBoundaries: ZoomLevelLimits | undefined); setMaxZoomLevel(maxZoomLevel: number): boolean; setMinZoomLevel(minZoomLevel: number): boolean; /** * Sets the camera zoom level and notifies observers if successful. * * @param zoomLevel - Target zoom level (1.0 = 100%, 2.0 = 200%, etc.) * @returns True if zoom was updated, false if outside boundaries or already at limit * * @remarks * If the zoom changes, a 'zoom' event is triggered with the zoom delta and new camera state. * All 'zoom' and 'all' event subscribers will be notified. * * @example * ```typescript * camera.on('zoom', (event, state) => { * console.log(`Zoom changed by ${event.deltaZoomAmount}`); * console.log(`New zoom: ${state.zoomLevel}`); * }); * * camera.setZoomLevel(2.0); // Triggers zoom event * ``` */ setZoomLevel(zoomLevel: number): boolean; /** * Gets the current camera rotation in radians. * * @returns Current rotation angle (0 to 2π) */ get rotation(): number; /** * @description The boundaries of the rotation of the camera. * * @category Camera */ get rotationBoundaries(): RotationLimits | undefined; set rotationBoundaries(rotationBoundaries: RotationLimits | undefined); /** * @description The order of the transformation is as follows: * 1. Scale (scale the context using the device pixel ratio) * 2. Translation (move the origin of the context to the center of the canvas) * 3. Rotation (rotate the context negatively the rotation of the camera) * 4. Zoom (scale the context using the zoom level of the camera) * 5. Translation (move the origin of the context to the position of the camera in the context coordinate system) * * @param devicePixelRatio The device pixel ratio of the canvas * @param alignCoorindate Whether to align the coordinate system to the camera's position * @returns The transformation matrix */ getTransform(devicePixelRatio?: number, alignCoorindate?: boolean): TransformationMatrix; /** * Sets the camera rotation and notifies observers if successful. * * @param rotation - Target rotation in radians * @returns True if rotation was updated, false if outside boundaries or already at limit * * @remarks * If the rotation changes, a 'rotate' event is triggered with the rotation delta and new camera state. * All 'rotate' and 'all' event subscribers will be notified. * Rotation is automatically normalized to 0-2π range. * * @example * ```typescript * camera.on('rotate', (event, state) => { * console.log(`Camera rotated by ${event.deltaRotation} radians`); * }); * * camera.setRotation(Math.PI / 4); // Triggers rotate event * ``` */ setRotation(rotation: number): boolean; /** * @description The origin of the camera in the window coordinate system. * @deprecated * * @param centerInWindow The center of the camera in the window coordinate system. * @returns The origin of the camera in the window coordinate system. */ getCameraOriginInWindow(centerInWindow: Point): Point; /** * @description Converts a point from the viewport coordinate system to the world coordinate system. * * @param point The point in the viewport coordinate system. * @returns The point in the world coordinate system. */ convertFromViewPort2WorldSpace(point: Point): Point; /** * @description Converts a point from the world coordinate system to the viewport coordinate system. * * @param point The point in the world coordinate system. * @returns The point in the viewport coordinate system. */ convertFromWorld2ViewPort(point: Point): Point; /** * @description Inverts a point from the world coordinate system to the viewport coordinate system. * * @param point The point in the world coordinate system. * @returns The point in the viewport coordinate system. */ invertFromWorldSpace2ViewPort(point: Point): Point; setHorizontalBoundaries(min: number, max: number): void; setVerticalBoundaries(min: number, max: number): void; /** * Subscribes to camera events with optional AbortController for cancellation. * * @typeParam K - The event type key from CameraEventMap * @param eventName - Event type to listen for: 'pan', 'zoom', 'rotate', or 'all' * @param callback - Function called when event occurs, receives event data and camera state * @param options - Optional subscription configuration including AbortController signal * @returns Function to unsubscribe from this event * * @remarks * Available events: * - 'pan': Triggered when camera position changes * - 'zoom': Triggered when zoom level changes * - 'rotate': Triggered when rotation changes * - 'all': Triggered for any camera change (pan, zoom, or rotate) * * Use the AbortController pattern to manage multiple subscriptions: * * @example * ```typescript * // Basic subscription * const unsubscribe = camera.on('pan', (event, state) => { * console.log(`Panned by (${event.diff.x}, ${event.diff.y})`); * console.log(`New position: (${state.position.x}, ${state.position.y})`); * }); * * // Later: unsubscribe * unsubscribe(); * * // Subscribe to all events * camera.on('all', (event, state) => { * if (event.type === 'pan') { * console.log('Pan event:', event.diff); * } else if (event.type === 'zoom') { * console.log('Zoom event:', event.deltaZoomAmount); * } else if (event.type === 'rotate') { * console.log('Rotate event:', event.deltaRotation); * } * }); * * // Using AbortController for batch unsubscribe * const controller = new AbortController(); * camera.on('pan', handlePan, { signal: controller.signal }); * camera.on('zoom', handleZoom, { signal: controller.signal }); * camera.on('rotate', handleRotate, { signal: controller.signal }); * * // Unsubscribe all at once * controller.abort(); * ``` */ on<K extends keyof CameraEventMap>(eventName: K, callback: (event: CameraEventMap[K], cameraState: CameraState) => void, options?: SubscriptionOptions): UnSubscribe; getTRS(devicePixelRatio?: number, alignCoordinateSystem?: boolean): { scale: { x: number; y: number; }; rotation: number; translation: { x: number; y: number; }; cached: boolean; }; setUsingTransformationMatrix(transformationMatrix: TransformationMatrix, devicePixelRatio?: number): void; viewPortInWorldSpace(alignCoordinate?: boolean): { top: { left: Point; right: Point; }; bottom: { left: Point; right: Point; }; }; viewPortAABB(alignCoordinate?: boolean): { min: Point; max: Point; }; }