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>

651 lines (650 loc) 22.9 kB
import { PanHandlerConfig } from "./pan-handler"; import { ZoomHandlerConfig } from "./zoom-handler"; import type { RotationHandlerConfig } from "./rotation-handler"; import { ObservableBoardCamera } from "../interface"; import { Point } from "@ue-too/math"; import type { BaseContext } from "@ue-too/being"; /** * Configuration for camera rig behavior combining pan, zoom, and rotation settings. * Composed from individual handler configs. * * @remarks * This type merges configuration from: * - {@link PanHandlerConfig} - Pan clamping and boundaries * - {@link ZoomHandlerConfig} - Zoom limits and restrictions * - {@link RotationHandlerConfig} - Rotation constraints * * @category Camera Rig * @see {@link PanHandlerConfig} * @see {@link ZoomHandlerConfig} * @see {@link RotationHandlerConfig} */ export type CameraRigConfig = PanHandlerConfig & ZoomHandlerConfig & RotationHandlerConfig; /** * High-level camera control interface providing intuitive methods for pan, zoom, and rotation. * The camera rig acts as a facade over the camera, handling coordinate conversions and constraints. * * @remarks * CameraRig provides: * - **Coordinate-aware methods**: Separate methods for viewport and world coordinates * - **Anchor-point zooming**: Keep points stationary during zoom (zoom-to-cursor) * - **Configuration management**: Unified config for all camera operations * - **Handler composition**: Combines pan, zoom, rotation handlers with proper sequencing * * The rig ensures correct transformation order when combining operations * (e.g., zoom-at-point requires zoom followed by pan compensation). * * @category Camera Rig * @see {@link DefaultCameraRig} for the default implementation * @see {@link createDefaultCameraRig} for a factory function */ export interface CameraRig extends BaseContext { /** The underlying observable camera being controlled */ camera: ObservableBoardCamera; /** Current configuration for all camera operations */ config: CameraRigConfig; /** * Updates the camera rig configuration. * @param config - Partial configuration to merge with current config */ configure(config: Partial<CameraRigConfig>): void; /** * Updates the camera rig state (called per frame if needed). */ update(): void; /** * Pans the camera by a delta in viewport coordinates. * @param delta - Movement delta in viewport space (CSS pixels, origin at center) */ panByViewPort: (delta: Point) => void; /** * Pans the camera to a target position in viewport coordinates. * @param target - Target position in viewport space */ panToViewPort: (target: Point) => void; /** * Pans the camera by a delta in world coordinates. * @param delta - Movement delta in world space */ panByWorld: (delta: Point) => void; /** * Pans the camera to a target position in world coordinates. * @param target - Target position in world space */ panToWorld: (target: Point) => void; /** * Rotates the camera by a delta angle. * @param delta - Rotation delta in radians */ rotateBy: (delta: number) => void; /** * Rotates the camera to a target angle. * @param target - Target rotation in radians */ rotateTo: (target: number) => void; /** * Zooms to a target level, keeping a viewport point stationary. * @param targetZoom - Target zoom level * @param at - Anchor point in viewport coordinates */ zoomToAt: (targetZoom: number, at: Point) => void; /** * Zooms by a delta, keeping a viewport point stationary. * @param delta - Zoom delta * @param at - Anchor point in viewport coordinates */ zoomByAt: (delta: number, at: Point) => void; /** * Zooms to a target level at viewport center. * @param targetZoom - Target zoom level */ zoomTo: (targetZoom: number) => void; /** * Zooms by a delta at viewport center. * @param delta - Zoom delta */ zoomBy: (delta: number) => void; /** * Zooms to a target level, keeping a world point stationary. * @param targetZoom - Target zoom level * @param at - Anchor point in world coordinates */ zoomToAtWorld: (targetZoom: number, at: Point) => void; /** * Zooms by a delta, keeping a world point stationary. * @param delta - Zoom delta * @param at - Anchor point in world coordinates */ zoomByAtWorld: (delta: number, at: Point) => void; } /** * Default implementation of the camera rig providing comprehensive camera control. * Composes pan, zoom, and rotation handlers into a unified, easy-to-use API. * * @remarks * DefaultCameraRig serves as: * - **Context for state machines**: Passed to pan/zoom state machines as execution context * - **Handler composition**: Combines individual pan/zoom/rotation handlers * - **Coordinate conversion**: Manages conversions between viewport and world space * - **Configuration management**: Applies constraints and limits through handlers * * The rig ensures proper transformation sequencing: * 1. For anchor-point zoom: Apply zoom, then compensate camera position to keep anchor stationary * 2. For rotation: Transform coordinates based on current camera rotation * 3. For pan: Apply clamping and boundary constraints * * @example * ```typescript * const camera = new DefaultBoardCamera(); * const rig = new DefaultCameraRig({ * limitEntireViewPort: true, * clampTranslation: true, * clampZoom: true, * restrictZoom: false * }, camera); * * // Pan in viewport coordinates * rig.panByViewPort({ x: 50, y: -30 }); * * // Zoom at cursor position * rig.zoomByAt(0.1, mousePosition); * * // Rotate camera * rig.rotateBy(Math.PI / 4); * ``` * * @category Camera Rig * @see {@link CameraRig} for the interface definition * @see {@link createDefaultCameraRig} for a convenient factory function */ export declare class DefaultCameraRig implements CameraRig { private _panBy; private _panTo; private _zoomTo; private _zoomBy; private _rotateBy; private _rotateTo; private _config; private _camera; /** * Creates a new DefaultCameraRig with specified configuration and camera. * * @param config - Camera rig configuration for pan and zoom constraints * @param camera - Observable camera instance to control (defaults to new DefaultBoardCamera) * * @remarks * The constructor initializes: * - Default pan, zoom, and rotation handler functions * - Rotation config with `restrictRotation: false` and `clampRotation: true` * - Handler functions that will be used to process and constrain all camera operations * * @example * ```typescript * const rig = new DefaultCameraRig({ * limitEntireViewPort: true, * clampTranslation: true, * clampZoom: true, * restrictZoom: false, * restrictXTranslation: false, * restrictYTranslation: false * }); * ``` */ constructor(config: PanHandlerConfig & ZoomHandlerConfig, camera?: ObservableBoardCamera); /** * Zooms to a target level while keeping a viewport point stationary (zoom-to-cursor). * * @param targetZoom - Target zoom level to reach * @param at - Anchor point in viewport coordinates (center-anchored, CSS pixels) * * @remarks * This implements the "zoom to cursor" behavior commonly seen in map applications. * The algorithm: * 1. Converts anchor point from viewport to world space (before zoom) * 2. Applies zoom transformation (may be clamped by config) * 3. Converts anchor point from viewport to world space (after zoom) * 4. Calculates position difference and pans camera to compensate * * The anchor point remains stationary on screen, while the world zooms around it. * * @example * ```typescript * // Zoom to 2x at mouse cursor position * rig.zoomToAt(2.0, { x: mouseX, y: mouseY }); * * // The world point under the cursor stays in place * ``` */ zoomToAt(targetZoom: number, at: Point): void; /** * Zooms by a relative delta while keeping a viewport point stationary. * * @param delta - Relative zoom delta (multiplied by current zoom level) * @param at - Anchor point in viewport coordinates (center-anchored, CSS pixels) * * @remarks * This method is ideal for mouse wheel zoom interactions where the delta * represents a relative change rather than an absolute target. * * The delta is scaled by current zoom level: `actualDelta = delta * currentZoom` * This provides consistent zoom "speed" regardless of current zoom level. * * Like {@link zoomToAt}, this keeps the anchor point stationary during zoom. * * @example * ```typescript * // Zoom in by 10% at cursor position (mouse wheel up) * rig.zoomByAt(0.1, cursorPosition); * * // Zoom out by 10% at cursor position (mouse wheel down) * rig.zoomByAt(-0.1, cursorPosition); * ``` * * @see {@link zoomToAt} for zooming to an absolute level */ zoomByAt(delta: number, at: Point): void; /** * Zooms to a target level with the viewport center as the anchor point. * * @param targetZoom - Target zoom level to reach * * @remarks * This is a simpler version of {@link zoomToAt} that always zooms relative to the * viewport center. The camera position remains unchanged, so the center point of * the viewport stays fixed in world space. * * Use this when you want straightforward zoom without anchor-point tracking, * such as zoom controls in a UI toolbar. * * @example * ```typescript * // Zoom to 2x, centered on current view * rig.zoomTo(2.0); * * // Zoom to fit (100%) * rig.zoomTo(1.0); * ``` * * @see {@link zoomToAt} for zoom with custom anchor point */ zoomTo(targetZoom: number): void; /** * Zooms by a relative delta with the viewport center as the anchor point. * * @param delta - Zoom delta (added to current zoom level) * * @remarks * Unlike {@link zoomByAt}, the delta is NOT scaled by current zoom level. * This provides absolute delta changes, useful for programmatic zoom adjustments. * * The camera position remains unchanged, keeping the viewport center fixed in world space. * * @example * ```typescript * // Increase zoom by 0.5 * rig.zoomBy(0.5); * * // Decrease zoom by 0.2 * rig.zoomBy(-0.2); * ``` * * @see {@link zoomByAt} for zoom with custom anchor point and scaling */ zoomBy(delta: number): void; /** * Zooms to a target level while keeping a world-space point stationary. * * @param targetZoom - Target zoom level to reach * @param at - Anchor point in world coordinates * * @remarks * Similar to {@link zoomToAt}, but accepts world-space coordinates instead of viewport coordinates. * Useful when you want to zoom to keep a specific world object or location centered, * rather than a screen position. * * The algorithm: * 1. Converts world anchor to viewport space (before zoom) * 2. Applies zoom transformation * 3. Converts world anchor to viewport space (after zoom) * 4. Calculates viewport movement and converts to world space * 5. Pans camera to compensate * * @example * ```typescript * // Zoom to 3x while keeping a specific world object in place * const objectWorldPos = { x: 1000, y: 500 }; * rig.zoomToAtWorld(3.0, objectWorldPos); * ``` * * @see {@link zoomToAt} for viewport-space variant */ zoomToAtWorld(targetZoom: number, at: Point): void; /** * Zooms by a delta while keeping a world-space point stationary. * * @param delta - Zoom delta (added to current zoom level, not scaled) * @param at - Anchor point in world coordinates * * @remarks * World-space variant of {@link zoomByAt}. The delta is NOT scaled by current zoom level, * unlike the viewport-space version. * * Use this when programmatically zooming around specific world objects or coordinates. * * @example * ```typescript * // Zoom in by 0.5 while keeping a world landmark stationary * const landmarkPos = { x: 2000, y: 1500 }; * rig.zoomByAtWorld(0.5, landmarkPos); * ``` * * @see {@link zoomByAt} for viewport-space variant with scaled delta */ zoomByAtWorld(delta: number, at: Point): void; /** * Pans the camera by a delta in viewport coordinates. * * @param delta - Movement delta in viewport space (center-anchored, CSS pixels) * * @remarks * This is the most common pan method for user input (mouse drag, touch pan). * The delta is in screen/viewport coordinates and gets converted to world space * accounting for current camera rotation and zoom. * * Conversion formula: * 1. Rotate delta by camera rotation * 2. Scale by inverse zoom (1 / zoomLevel) * 3. Apply as world-space pan * * @example * ```typescript * // Pan camera when user drags mouse * canvas.addEventListener('mousemove', (e) => { * if (isDragging) { * const delta = { x: e.movementX, y: e.movementY }; * rig.panByViewPort(delta); * } * }); * ``` * * @see {@link panByWorld} for world-space panning */ panByViewPort(delta: Point): void; /** * Pans the camera by a delta in world coordinates. * * @param delta - Movement delta in world space * * @remarks * Use this for programmatic camera movement or when you already have world-space * coordinates (e.g., moving camera to follow a world object). * * The delta is passed through the pan handler which may apply: * - Boundary clamping * - Movement restrictions (restrictXTranslation, restrictYTranslation) * - Other constraints from {@link CameraRigConfig} * * @example * ```typescript * // Move camera 100 units right, 50 units up in world space * rig.panByWorld({ x: 100, y: -50 }); * * // Follow a moving object * const objectMovement = { x: obj.dx, y: obj.dy }; * rig.panByWorld(objectMovement); * ``` * * @see {@link panByViewPort} for viewport-space panning */ panByWorld(delta: Point): void; /** * Pans the camera to an absolute position in world coordinates. * * @param target - Target camera position in world space * * @remarks * Sets the camera position directly (subject to constraints). * Unlike pan-by methods, this is an absolute positioning operation. * * The target is passed through the pan handler which may apply: * - Boundary clamping * - Position restrictions * * Use this for: * - "Go to location" features * - Centering camera on specific world coordinates * - Resetting camera to a known position * * @example * ```typescript * // Center camera on world origin * rig.panToWorld({ x: 0, y: 0 }); * * // Go to specific landmark * const landmark = { x: 1000, y: 500 }; * rig.panToWorld(landmark); * ``` * * @see {@link panToViewPort} for viewport-space variant */ panToWorld(target: Point): void; /** * Pans the camera to position a viewport point at a specific location. * * @param target - Target position in viewport coordinates (center-anchored, CSS pixels) * * @remarks * Moves the camera so that the specified viewport point ends up at the viewport center. * This is less commonly used than world-space pan-to operations. * * The method converts the viewport target to world space, then uses {@link panToWorld}. * * @example * ```typescript * // Center the camera on what's currently at the top-left of viewport * rig.panToViewPort({ x: -400, y: -300 }); * ``` * * @see {@link panToWorld} for world-space variant (more commonly used) */ panToViewPort(target: Point): void; /** * Rotates the camera by a delta angle. * * @param delta - Rotation delta in radians (positive = counter-clockwise) * * @remarks * Applies a relative rotation to the camera. The delta is passed through the * rotation handler which may apply clamping or restrictions based on {@link CameraRigConfig}. * * Camera rotation affects: * - How viewport coordinates map to world coordinates * - The orientation of pan operations * - Visual rendering of the world * * @example * ```typescript * // Rotate 45 degrees counter-clockwise * rig.rotateBy(Math.PI / 4); * * // Rotate 90 degrees clockwise * rig.rotateBy(-Math.PI / 2); * ``` * * @see {@link rotateTo} for absolute rotation */ rotateBy(delta: number): void; /** * Rotates the camera to an absolute angle. * * @param target - Target rotation in radians (0 = no rotation, positive = counter-clockwise) * * @remarks * Sets the camera rotation to a specific angle (subject to constraints). * The target is passed through the rotation handler which may apply clamping. * * Use this for: * - Resetting camera to north-up orientation (0 radians) * - Snapping to cardinal directions * - Setting rotation from UI controls * * @example * ```typescript * // Reset to north-up * rig.rotateTo(0); * * // Rotate to 90 degrees * rig.rotateTo(Math.PI / 2); * ``` * * @see {@link rotateBy} for relative rotation */ rotateTo(target: number): void; /** * Sets whether the entire viewport must remain within boundaries. * * @remarks * When true, pan boundaries ensure the entire viewport stays within configured limits. * When false, only the camera center point is constrained. * * This is a convenience setter for {@link CameraRigConfig}.limitEntireViewPort. */ set limitEntireViewPort(limit: boolean); /** * Gets whether the entire viewport must remain within boundaries. * * @returns True if entire viewport is constrained, false if only center is constrained */ get limitEntireViewPort(): boolean; /** * Gets the underlying observable camera instance. * * @returns The camera being controlled by this rig */ get camera(): ObservableBoardCamera; /** * Sets the underlying camera instance. * * @param camera - New camera to control * * @remarks * Use this to swap cameras at runtime, though this is uncommon. * Usually you create a new rig instead. */ set camera(camera: ObservableBoardCamera); /** * Gets the current camera rig configuration. * * @returns Current configuration object * * @remarks * Returns a reference to the internal config. Modifications will affect rig behavior. * For safer updates, use {@link configure} instead. */ get config(): CameraRigConfig; /** * Sets the camera rig configuration. * * @param config - New configuration object * * @remarks * Creates a shallow copy of the provided config. * For partial updates, use {@link configure} instead. */ set config(config: CameraRigConfig); /** * Updates camera rig configuration with partial settings. * * @param config - Partial configuration to merge with current config * * @remarks * This is the recommended way to update configuration at runtime. * Only provided properties are updated; others remain unchanged. * * @example * ```typescript * // Enable zoom restrictions without changing other settings * rig.configure({ * restrictZoom: true, * zoomLevelLimits: { min: 0.5, max: 5.0 } * }); * * // Disable position clamping * rig.configure({ clampTranslation: false }); * ``` */ configure(config: Partial<CameraRigConfig>): void; /** * Cleans up resources used by the camera rig. * * @remarks * Currently a no-op as DefaultCameraRig has no resources to clean up. * Implements {@link BaseContext} interface for consistency with other systems. */ cleanup(): void; /** * Sets up the camera rig. * * @remarks * Currently a no-op as DefaultCameraRig requires no setup. * Implements {@link BaseContext} interface for consistency with other systems. */ setup(): void; /** * Updates the camera rig state. * * @remarks * Currently a no-op as DefaultCameraRig has no per-frame update logic. * Implements {@link BaseContext} interface for consistency with other systems. * * In stateful rig implementations, this might handle: * - Animation interpolation * - Momentum/inertia * - Smooth camera following */ update(): void; } /** * Creates a camera rig with sensible default configuration. * * @param camera - Observable camera instance to control * @returns Configured camera rig ready for use * * @remarks * This factory function creates a {@link DefaultCameraRig} with a balanced default configuration: * * **Enabled by default:** * - `limitEntireViewPort: true` - Entire viewport stays within boundaries * - `clampTranslation: true` - Position is clamped to boundaries * - `clampZoom: true` - Zoom is clamped to limits * * **Disabled by default:** * - All movement restrictions (`restrictXTranslation`, `restrictYTranslation`, etc.) * - Zoom restrictions (`restrictZoom`) * - Relative translation restrictions * * This configuration allows free camera movement with boundary enforcement, * suitable for most infinite canvas applications. * * @example * ```typescript * const camera = new DefaultBoardCamera(1920, 1080); * const rig = createDefaultCameraRig(camera); * * // Ready to use with sensible defaults * rig.configure({ * boundaries: { * min: { x: -1000, y: -1000 }, * max: { x: 1000, y: 1000 } * } * }); * * rig.panByViewPort({ x: 100, y: 50 }); * rig.zoomByAt(0.1, mousePosition); * ``` * * @category Camera Rig * @see {@link DefaultCameraRig} for the implementation * @see {@link CameraRigConfig} for all available configuration options */ export declare function createDefaultCameraRig(camera: ObservableBoardCamera): CameraRig;