# zoom-pan **Repository Path**: mirrors_LancerComet/zoom-pan ## Basic Information - **Project Name**: zoom-pan - **Description**: I made this to replace Fabric.js in my projects. Work in progress. - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-10-01 - **Last Updated**: 2026-01-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ZoomPan A lightweight **camera & input control core** for 2D rendering systems. It provides smooth **panning**, **zooming**, and **inertia** behaviors — completely independent of any rendering engine. You can think of it as a **camera control module** that separates *input dynamics* from *rendering logic*, giving you full control over how the view moves. Although it currently uses **Canvas2D** to render images, it can technically work with any rendering engine such as **Pixi.js** or others. I built this to replace **Fabric.js** in my own project. ## Design Goals | Goal | Description | |------|--------------| | **Independent coordinate space** | Keeps world coordinates separate from screen space | | **Smooth and physical motion** | Uses exponential decay and EMA (Exponential Moving Average) for velocity smoothing | | **Engine-agnostic** | Works without depending on any specific graphics library | | **Continuous motion** | Supports inertia and smooth decay after user input ends | | **Predictable zooming** | Logarithmic interpolation ensures consistent zoom behavior | | **Touch-friendly** | Supports both mouse and multi-touch pinch gestures | | **Layer-based system** | Designed like Photoshop-style layers for world and UI separation | ## Installation ```bash npm install @lancercomet/zoom-pan ``` ## Quick Start ### Basic Viewer (Read-only) ```typescript import { ViewManager, ContentLayerManager } from '@lancercomet/zoom-pan' // Create layer manager const layerManager = new ContentLayerManager() // Initialize view const view = new ViewManager( canvas, (view) => { layerManager.renderAllLayersIn(view) }, { minZoom: 0.2, maxZoom: 10, background: '#ffffff' } ) // Register layer manager view.registerLayerManager(layerManager) // Add an image layer await layerManager.createImageLayer({ src: 'path/to/image.png', x: 0, y: 0 }) ``` ### Painter with Undo/Redo ```typescript import { ViewManager, ContentLayerManager, CanvasLayer, HistoryManager, createSnapshotCommand, createInteractionPlugin, createDocumentPlugin } from '@lancercomet/zoom-pan' const layerManager = new ContentLayerManager() const historyManager = new HistoryManager({ maxHistorySize: 50 }) const view = new ViewManager(canvas, (view) => { layerManager.renderAllLayersIn(view) }) view.registerLayerManager(layerManager) // Use plugins for interaction const interactionPlugin = createInteractionPlugin() view.use(interactionPlugin) // Create a drawable canvas layer const drawLayer = new CanvasLayer({ width: 1200, height: 800, x: 0, y: 0 }) layerManager.addLayer(drawLayer) // Drawing with snapshot-based undo/redo let snapshotBefore: ImageData | null = null function onPointerDown(wx: number, wy: number) { snapshotBefore = drawLayer.captureSnapshot() drawLayer.beginStroke(wx, wy) } function onPointerMove(wx: number, wy: number, pressure: number) { drawLayer.stroke(wx, wy, '#ff0000', 10, pressure, 'brush') } function onPointerUp() { drawLayer.endStroke() const snapshotAfter = drawLayer.captureSnapshot() const command = createSnapshotCommand(drawLayer, snapshotBefore, snapshotAfter) if (command) { historyManager.addCommand(command) } } // Undo / Redo historyManager.undo() historyManager.redo() ``` ## API Reference ### ViewManager The core class that manages the canvas viewport. ```typescript const view = new ViewManager(canvas, renderFn, options?) ``` #### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `minZoom` | `number` | `0.5` | Minimum zoom level | | `maxZoom` | `number` | `10` | Maximum zoom level | | `wheelSensitivity` | `number` | `0.0015` | Mouse wheel zoom sensitivity | | `friction` | `number` | `0.92` | Inertia friction (per frame) | | `background` | `string` | `'#fff'` | Canvas background color | | `drawDocBorder` | `boolean` | `false` | Draw border around document | | `panClampMode` | `'margin' \| 'minVisible'` | `'minVisible'` | Pan restriction mode | #### Methods ```typescript // Zoom view.zoomToAtScreen(anchorX, anchorY, zoom) // Zoom to absolute level at screen point view.zoomByFactorAtScreen(anchorX, anchorY, factor) // Zoom by factor at screen point view.zoomInAtCenter() // Zoom in at canvas center view.zoomOutAtCenter() // Zoom out at canvas center // Coordinate conversion view.toWorld(screenX, screenY) // Screen → World coordinates view.toScreen(worldX, worldY) // World → Screen coordinates // Pan control view.enablePan() view.disablePan() // Zoom control view.enableZoom() view.disableZoom() // Document mode (constrained panning) view.setDocument(x, y, width, height) view.setDocumentMargins(left, right, top, bottom) // Reset view.reset() // Smooth reset to initial state // Cleanup view.destroy() ``` ### ContentLayerManager Manages layers that render in world space. ```typescript const layerManager = new ContentLayerManager() // Create image layer from URL/File/Blob const imageLayer = await layerManager.createImageLayer({ src: 'image.png', x: 0, y: 0, width: 500, // optional, uses natural size if omitted height: 300, // optional anchor: 'center', // 'topLeft' | 'center' rotation: 0 // radians }) // Add custom layer layerManager.addLayer(layer) // Remove layer layerManager.removeLayer(layer) // Cleanup layerManager.destroy() ``` ### CanvasLayer A drawable layer with an offscreen canvas. ```typescript const layer = new CanvasLayer({ width: 1200, height: 800, x: 0, y: 0, anchor: 'topLeft', space: 'world' // 'world' | 'screen' }) // Drawing API layer.beginStroke(worldX, worldY) layer.stroke(worldX, worldY, '#000000', 10, pressure, 'brush') // color, size, pressure, mode layer.endStroke() // Snapshot API for undo/redo const snapshot = layer.captureSnapshot() // Capture current state layer.restoreSnapshot(snapshot) // Restore to snapshot // Direct canvas access layer.context.fillRect(0, 0, 100, 100) ``` ### BitmapLayer Extends `CanvasLayer` for image display. ```typescript const layer = await BitmapLayer.fromImage({ src: 'image.png', // URL, File, or Blob x: 0, y: 0, width: 500, height: 300, anchor: 'center', rotation: Math.PI / 4 }) // Replace image source await layer.setSource('new-image.png') // Paint on the bitmap layer.paint((ctx, canvas) => { ctx.fillStyle = 'red' ctx.fillRect(10, 10, 50, 50) }) // Pixel access const imageData = layer.getImageData() layer.putImageData(imageData, 0, 0) ``` ### HistoryManager Command-based undo/redo system. ```typescript const historyManager = new HistoryManager({ maxHistorySize: 50, undoStack: [], // optional, pass reactive array for UI binding redoStack: [] }) // Add a command (already executed) historyManager.addCommand(command) // Execute and add a command historyManager.executeCommand(command) historyManager.undo() historyManager.redo() historyManager.canUndo() historyManager.canRedo() historyManager.clear() ``` ### SnapshotCommand Built-in command for canvas content undo/redo. ```typescript import { createSnapshotCommand } from '@lancercomet/zoom-pan' // Capture before action const before = layer.captureSnapshot() // ... perform drawing ... // Capture after action const after = layer.captureSnapshot() // Create command and add to history const command = createSnapshotCommand(layer, before, after) if (command) { historyManager.addCommand(command) } ``` ### Plugins The library uses a plugin system for extensibility. #### InteractionPlugin Handles pan, zoom, wheel, and inertia. ```typescript import { createInteractionPlugin } from '@lancercomet/zoom-pan' const interactionPlugin = createInteractionPlugin() view.use(interactionPlugin) // Control pan/zoom interactionPlugin.setPanEnabled(true) interactionPlugin.setZoomEnabled(true) ``` #### DocumentPlugin Handles document bounds and pan clamping. ```typescript import { createDocumentPlugin } from '@lancercomet/zoom-pan' const documentPlugin = createDocumentPlugin() view.use(documentPlugin) // Set document bounds documentPlugin.setRect(0, 0, 1200, 800) documentPlugin.setMargins(100, 100, 100, 100) documentPlugin.setPanClampMode('minVisible') // 'margin' | 'minVisible' documentPlugin.zoomToFit() ``` ### Custom Commands Implement `ICommand` interface for custom undo/redo actions. ```typescript import { ICommand } from '@lancercomet/zoom-pan' class MyCommand implements ICommand { readonly type = 'my-command' execute(): void { // Do the action } undo(): void { // Reverse the action } } ``` ## Examples Check out the `examples/` directory: - **`examples/viewer/`** - Basic image viewer with pan & zoom - **`examples/painter/`** - Full-featured painter with brush, eraser, undo/redo ### Painter Hotkeys | Key | Action | |-----|--------| | `B` | Brush tool | | `E` | Eraser tool | | `H` | Pan tool | | `Z` | Zoom tool | | `Space` (hold) | Temporary pan mode | | `Alt` (hold) | Color picker | | `Ctrl+Z` | Undo | | `Ctrl+Y` / `Ctrl+Shift+Z` | Redo | ## License MIT