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>

502 lines (501 loc) 19 kB
import { Point } from '@ue-too/math'; import { Boundaries } from './utils/position'; import { ZoomLevelLimits } from './utils/zoom'; import { RotationLimits } from './utils/rotation'; import { BoardCamera } from './interface'; import { TransformationMatrix } from './utils/matrix'; /** * Base camera implementation providing core functionality for an infinite canvas system. * This is the fundamental building block for camera management in the board package. * * @remarks * BaseCamera is non-observable and does not emit events when state changes. * For event-driven camera updates, use {@link DefaultBoardCamera} instead. * * The camera supports: * - Position, rotation, and zoom transformations * - Configurable boundaries for position, zoom, and rotation * - Coordinate conversion between viewport and world space * - Transformation matrix caching for performance * - High-DPI display support via devicePixelRatio * * @example * ```typescript * // Create a camera for a 1920x1080 viewport * const camera = new BaseCamera(1920, 1080, { x: 0, y: 0 }, 0, 1.0); * * // Set boundaries to constrain camera movement * camera.setHorizontalBoundaries(-5000, 5000); * camera.setVerticalBoundaries(-5000, 5000); * * // Update camera state * camera.setPosition({ x: 100, y: 200 }); * camera.setZoomLevel(2.0); * camera.setRotation(Math.PI / 6); * * // Get transformation matrix for rendering * const transform = camera.getTransform(window.devicePixelRatio, true); * ctx.setTransform(transform.a, transform.b, transform.c, transform.d, transform.e, transform.f); * ``` * * @category Camera * @see {@link DefaultBoardCamera} for observable camera with event support * @see {@link CameraRig} for high-level camera control with input handling */ export default class BaseCamera implements BoardCamera { private _position; private _rotation; private _zoomLevel; private currentCachedTransform; private currentCachedTRS; private _viewPortWidth; private _viewPortHeight; private _boundaries?; private _zoomBoundaries?; private _rotationBoundaries?; /** * Creates a new BaseCamera instance with specified viewport size and optional constraints. * * @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, where 1.0 = 100% (default: 1.0) * @param boundaries - Position constraints in world space (default: ±10000 on both axes) * @param zoomLevelBoundaries - Zoom constraints (default: 0.1 to 10) * @param rotationBoundaries - Optional rotation constraints (default: undefined, unrestricted) * * @example * ```typescript * // Basic camera with defaults * const camera = new BaseCamera(); * * // Camera with custom viewport and position * const camera2 = new BaseCamera( * 1920, 1080, * { x: 500, y: 300 }, * 0, * 1.5 * ); * * // Camera with all constraints * const camera3 = new BaseCamera( * 1920, 1080, * { x: 0, y: 0 }, * 0, * 1.0, * { min: { x: -2000, y: -2000 }, max: { x: 2000, y: 2000 } }, * { min: 0.5, max: 5 }, * { start: 0, end: Math.PI / 2 } * ); * ``` */ constructor(viewPortWidth?: number, viewPortHeight?: number, position?: Point, rotation?: number, zoomLevel?: number, boundaries?: Boundaries, zoomLevelBoundaries?: ZoomLevelLimits, rotationBoundaries?: RotationLimits | undefined); /** * Gets the current position boundaries that constrain camera movement in world coordinates. * * @returns The boundaries object or undefined if no boundaries are set */ get boundaries(): Boundaries | undefined; /** * Sets position boundaries to constrain camera movement in world coordinates. * * @param boundaries - Boundary constraints or undefined to remove all constraints */ set boundaries(boundaries: Boundaries | undefined); /** * Gets the viewport width in CSS pixels. * * @returns Current viewport width */ get viewPortWidth(): number; /** * Sets the viewport width in CSS pixels. * Updates invalidate the cached transformation matrix. * * @param width - New viewport width in CSS pixels */ set viewPortWidth(width: number); /** * Gets the viewport height in CSS pixels. * * @returns Current viewport height */ get viewPortHeight(): number; /** * Sets the viewport height in CSS pixels. * Updates invalidate the cached transformation matrix. * * @param height - New viewport height in CSS pixels */ set viewPortHeight(height: number); /** * Gets the current camera position in world coordinates. * * @returns A copy of the current position (center of viewport in world space) */ get position(): Point; /** * Sets the camera position with boundary validation and floating-point jitter prevention. * * @param destination - Target position in world coordinates * @returns True if position was updated, false if rejected by boundaries or negligible change * * @remarks * Position updates are rejected if: * - The destination is outside the configured boundaries * - The change magnitude is less than 10E-10 * - The change magnitude is less than 1/zoomLevel (prevents sub-pixel jitter) * * @example * ```typescript * camera.setHorizontalBoundaries(-1000, 1000); * camera.setVerticalBoundaries(-1000, 1000); * * camera.setPosition({ x: 500, y: 500 }); // returns true * camera.setPosition({ x: 2000, y: 0 }); // returns false (out of bounds) * ``` */ setPosition(destination: Point): boolean; /** * Gets the current zoom level. * * @returns Current zoom level (1.0 = 100%, 2.0 = 200%, etc.) */ get zoomLevel(): number; /** * Gets the current zoom level constraints. * * @returns Zoom boundaries object or undefined if unconstrained */ get zoomBoundaries(): ZoomLevelLimits | undefined; /** * Sets zoom level constraints with automatic min/max swapping if needed. * * @param zoomBoundaries - Zoom constraints or undefined to remove constraints * * @remarks * If min > max, the values are automatically swapped. */ set zoomBoundaries(zoomBoundaries: ZoomLevelLimits | undefined); /** * Sets the maximum allowed zoom level. * * @param maxZoomLevel - New maximum zoom level * @returns True if successfully set, false if conflicts with existing min or current zoom * * @remarks * Returns false if: * - The new max is less than the current minimum boundary * - The current zoom level exceeds the new maximum */ setMaxZoomLevel(maxZoomLevel: number): boolean; /** * Sets the minimum allowed zoom level. * * @param minZoomLevel - New minimum zoom level * @returns True if successfully set, false if conflicts with existing max * * @remarks * If the current zoom level is below the new minimum, the camera automatically * zooms in to match the minimum. Returns false if new min exceeds existing max boundary. */ setMinZoomLevel(minZoomLevel: number): boolean; /** * Sets the camera zoom level with boundary validation. * * @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 * Returns false if: * - Zoom level is outside configured boundaries * - Already at maximum and trying to zoom beyond it * - Already at minimum and trying to zoom below it * * @example * ```typescript * camera.setZoomLevel(2.0); // 200% zoom * camera.setZoomLevel(0.5); // 50% zoom * ``` */ setZoomLevel(zoomLevel: number): boolean; /** * Gets the current camera rotation in radians. * * @returns Current rotation angle (0 to 2π) */ get rotation(): number; /** * Gets the current rotation constraints. * * @returns Rotation boundaries or undefined if unconstrained */ get rotationBoundaries(): RotationLimits | undefined; /** * Sets rotation constraints with automatic start/end swapping if needed. * * @param rotationBoundaries - Rotation limits or undefined to remove constraints * * @remarks * If start > end, the values are automatically swapped. */ set rotationBoundaries(rotationBoundaries: RotationLimits | undefined); /** * Computes the complete transformation matrix from world space to canvas pixel space. * Includes caching for performance optimization. * * @param devicePixelRatio - Device pixel ratio (typically window.devicePixelRatio) * @param alignCoorindate - If true, uses standard y-up coordinate system. If false, inverts y-axis * @returns Transformation matrix object {a, b, c, d, e, f} with optional cached flag * * @remarks * Transformation order applied: * 1. Scale by devicePixelRatio * 2. Translate to viewport center * 3. Rotate (negated if alignCoorindate is true) * 4. Scale by zoom level * 5. Translate by camera position * * The result is cached based on all parameters. Subsequent calls with identical parameters * return the cached matrix with `cached: true` flag. * * @example * ```typescript * const ctx = canvas.getContext('2d'); * const transform = camera.getTransform(window.devicePixelRatio, true); * ctx.setTransform(transform.a, transform.b, transform.c, transform.d, transform.e, transform.f); * * // Now drawing at world coordinates (100, 200) appears correctly on canvas * ctx.fillRect(100, 200, 50, 50); * ``` * * @see {@link getTRS} for decomposed transformation components */ getTransform(devicePixelRatio?: number, alignCoorindate?: boolean): { cached: boolean; a: number; b: number; c: number; d: number; e: number; f: number; }; /** * Decomposes the transformation matrix into Translation, Rotation, and Scale components. * * @param devicePixelRatio - Device pixel ratio for high-DPI displays * @param alignCoorindate - If true, uses standard y-up coordinate system. If false, inverts y-axis * @returns Object containing separate scale, rotation, and translation values * * @remarks * This is useful when you need individual transformation components rather than * the combined matrix. Internally calls {@link getTransform} and decomposes the result. */ getTRS(devicePixelRatio?: number, alignCoorindate?: boolean): { scale: { x: number; y: number; }; rotation: number; translation: { x: number; y: number; }; cached: boolean; }; /** * Sets camera state by decomposing a transformation matrix. * Inverse operation of {@link getTransform}. * * @param transformationMatrix - 2D transformation matrix to decompose * * @remarks * The matrix is decomposed assuming the same transformation order as {@link getTransform}: * Scale(devicePixelRatio) → Translation(viewport center) → Rotation → Zoom → Translation(position) * * Extracted position, zoom, and rotation values are still validated against boundaries. * * @example * ```typescript * // Apply a transformation matrix from an external source * const matrix = { a: 2, b: 0, c: 0, d: 2, e: 100, f: 100 }; * camera.setUsingTransformationMatrix(matrix); * ``` */ setUsingTransformationMatrix(transformationMatrix: TransformationMatrix, devicePixelRatio?: number): void; /** * Sets the camera rotation with boundary validation and normalization. * * @param rotation - Target rotation in radians * @returns True if rotation was updated, false if outside boundaries or already at limit * * @remarks * Rotation is automatically normalized to 0-2π range. Returns false if: * - Rotation is outside configured boundaries * - Already at maximum boundary and trying to rotate beyond it * - Already at minimum boundary and trying to rotate below it * * @example * ```typescript * camera.setRotation(Math.PI / 4); // 45 degrees * camera.setRotation(Math.PI); // 180 degrees * ``` */ setRotation(rotation: number): boolean; /** * Gets the camera origin in window coordinates. * * @deprecated This method is deprecated and will be removed in a future version. * Currently just returns the input unchanged. * * @param centerInWindow - Center point in window coordinates * @returns The same point (camera origin equals window center) */ getCameraOriginInWindow(centerInWindow: Point): Point; /** * Converts a point from viewport coordinates to world coordinates. * * @param point - Point in viewport space (relative to viewport center, in CSS pixels) * @returns Corresponding point in world coordinates * * @remarks * This accounts for camera position, zoom, and rotation. Useful for converting * mouse/touch input to world space. * * @example * ```typescript * // Convert mouse click to world position * const rect = canvas.getBoundingClientRect(); * const viewportPoint = { * x: event.clientX - rect.left - rect.width / 2, * y: event.clientY - rect.top - rect.height / 2 * }; * const worldPoint = camera.convertFromViewPort2WorldSpace(viewportPoint); * ``` */ convertFromViewPort2WorldSpace(point: Point): Point; /** * Converts a point from world coordinates to viewport coordinates. * * @param point - Point in world coordinates * @returns Corresponding point in viewport space (relative to viewport center, in CSS pixels) * * @remarks * This accounts for camera position, zoom, and rotation. Useful for positioning * UI elements at world object locations. * * @example * ```typescript * // Position a DOM element at a world object's location * const viewportPos = camera.convertFromWorld2ViewPort(objectWorldPos); * element.style.left = `${viewportPos.x + canvas.width / 2}px`; * element.style.top = `${viewportPos.y + canvas.height / 2}px`; * ``` */ convertFromWorld2ViewPort(point: Point): Point; /** * Converts a point from world coordinates to viewport coordinates. * Alternative implementation of {@link convertFromWorld2ViewPort}. * * @param point - Point in world coordinates * @returns Corresponding point in viewport space (relative to viewport center, in CSS pixels) * * @remarks * This method provides an alternative calculation approach. In most cases, * prefer using {@link convertFromWorld2ViewPort} for consistency. */ invertFromWorldSpace2ViewPort(point: Point): Point; /** * Sets horizontal (x-axis) position boundaries for camera movement. * * @param min - Minimum x coordinate in world space * @param max - Maximum x coordinate in world space * * @remarks * If min > max, the values are automatically swapped. The current camera position * is not automatically clamped when boundaries are set. * * @example * ```typescript * camera.setHorizontalBoundaries(-1000, 1000); * // Camera can now only move between x: -1000 and x: 1000 * ``` */ setHorizontalBoundaries(min: number, max: number): void; /** * Sets vertical (y-axis) position boundaries for camera movement. * * @param min - Minimum y coordinate in world space * @param max - Maximum y coordinate in world space * * @remarks * If min > max, the values are automatically swapped. The current camera position * is not automatically clamped when boundaries are set. * * @example * ```typescript * camera.setVerticalBoundaries(-500, 500); * // Camera can now only move between y: -500 and y: 500 * ``` */ setVerticalBoundaries(min: number, max: number): void; /** * Calculates the four corners of the viewport in world space, accounting for rotation. * * @param alignCoordinate - If true, uses standard y-up coordinate system. If false, inverts y-axis (default: true) * @returns Object containing the four corner points organized as top/bottom and left/right * * @remarks * Returns the actual rotated viewport corners. This is more precise than {@link viewPortAABB} * which returns the axis-aligned bounding box. Use this when you need the exact viewport bounds. * * @example * ```typescript * const corners = camera.viewPortInWorldSpace(); * console.log(corners.top.left); // Top-left corner in world coords * console.log(corners.top.right); // Top-right corner * console.log(corners.bottom.left); // Bottom-left corner * console.log(corners.bottom.right);// Bottom-right corner * ``` */ viewPortInWorldSpace(alignCoordinate?: boolean): { top: { left: Point; right: Point; }; bottom: { left: Point; right: Point; }; }; /** * Calculates the axis-aligned bounding box (AABB) of the viewport in world space. * * @param alignCoordinate - If true, uses standard y-up coordinate system. If false, inverts y-axis * @returns Object with min and max points defining the AABB * * @remarks * This returns the smallest axis-aligned rectangle that contains the entire viewport. * When the camera is rotated, this AABB will be larger than the actual viewport. * For exact viewport bounds, use {@link viewPortInWorldSpace}. * * Useful for: * - Frustum culling (checking if objects are visible) * - Broad-phase collision detection * - Determining which tiles/chunks to load * * @example * ```typescript * const aabb = camera.viewPortAABB(); * const isVisible = ( * object.x >= aabb.min.x && object.x <= aabb.max.x && * object.y >= aabb.min.y && object.y <= aabb.max.y * ); * ``` */ viewPortAABB(alignCoordinate?: boolean): { min: Point; max: Point; }; }