@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
TypeScript
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;
};
}