@lancercomet/zoom-pan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,387 @@
1
+ # ZoomPan
2
+
3
+ A lightweight **camera & input control core** for 2D rendering systems.
4
+ It provides smooth **panning**, **zooming**, and **inertia** behaviors — completely independent of any rendering engine.
5
+
6
+ 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.
7
+
8
+ Although it currently uses **Canvas2D** to render images, it can technically work with any rendering engine such as **Pixi.js** or others.
9
+
10
+ I built this to replace **Fabric.js** in my own project.
11
+
12
+ ## Design Goals
13
+
14
+ | Goal | Description |
15
+ |------|--------------|
16
+ | **Independent coordinate space** | Keeps world coordinates separate from screen space |
17
+ | **Smooth and physical motion** | Uses exponential decay and EMA (Exponential Moving Average) for velocity smoothing |
18
+ | **Engine-agnostic** | Works without depending on any specific graphics library |
19
+ | **Continuous motion** | Supports inertia and smooth decay after user input ends |
20
+ | **Predictable zooming** | Logarithmic interpolation ensures consistent zoom behavior |
21
+ | **Touch-friendly** | Supports both mouse and multi-touch pinch gestures |
22
+ | **Layer-based system** | Designed like Photoshop-style layers for world and UI separation |
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @lancercomet/zoom-pan
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### Basic Viewer (Read-only)
33
+
34
+ ```typescript
35
+ import { ViewManager, ContentLayerManager } from '@lancercomet/zoom-pan'
36
+
37
+ // Create layer manager
38
+ const layerManager = new ContentLayerManager()
39
+
40
+ // Initialize view
41
+ const view = new ViewManager(
42
+ canvas,
43
+ (view) => {
44
+ layerManager.renderAllLayersIn(view)
45
+ },
46
+ {
47
+ minZoom: 0.2,
48
+ maxZoom: 10,
49
+ background: '#ffffff'
50
+ }
51
+ )
52
+
53
+ // Register layer manager
54
+ view.registerLayerManager(layerManager)
55
+
56
+ // Add an image layer
57
+ await layerManager.createImageLayer({
58
+ src: 'path/to/image.png',
59
+ x: 0,
60
+ y: 0
61
+ })
62
+ ```
63
+
64
+ ### Painter with Undo/Redo
65
+
66
+ ```typescript
67
+ import {
68
+ ViewManager,
69
+ ContentLayerManager,
70
+ CanvasLayer,
71
+ HistoryManager,
72
+ createSnapshotCommand,
73
+ createInteractionPlugin,
74
+ createDocumentPlugin
75
+ } from '@lancercomet/zoom-pan'
76
+
77
+ const layerManager = new ContentLayerManager()
78
+ const historyManager = new HistoryManager({ maxHistorySize: 50 })
79
+
80
+ const view = new ViewManager(canvas, (view) => {
81
+ layerManager.renderAllLayersIn(view)
82
+ })
83
+
84
+ view.registerLayerManager(layerManager)
85
+
86
+ // Use plugins for interaction
87
+ const interactionPlugin = createInteractionPlugin()
88
+ view.use(interactionPlugin)
89
+
90
+ // Create a drawable canvas layer
91
+ const drawLayer = new CanvasLayer({
92
+ width: 1200,
93
+ height: 800,
94
+ x: 0,
95
+ y: 0
96
+ })
97
+ layerManager.addLayer(drawLayer)
98
+
99
+ // Drawing with snapshot-based undo/redo
100
+ let snapshotBefore: ImageData | null = null
101
+
102
+ function onPointerDown(wx: number, wy: number) {
103
+ snapshotBefore = drawLayer.captureSnapshot()
104
+ drawLayer.beginStroke(wx, wy)
105
+ }
106
+
107
+ function onPointerMove(wx: number, wy: number, pressure: number) {
108
+ drawLayer.stroke(wx, wy, '#ff0000', 10, pressure, 'brush')
109
+ }
110
+
111
+ function onPointerUp() {
112
+ drawLayer.endStroke()
113
+ const snapshotAfter = drawLayer.captureSnapshot()
114
+ const command = createSnapshotCommand(drawLayer, snapshotBefore, snapshotAfter)
115
+ if (command) {
116
+ historyManager.addCommand(command)
117
+ }
118
+ }
119
+
120
+ // Undo / Redo
121
+ historyManager.undo()
122
+ historyManager.redo()
123
+ ```
124
+
125
+ ## API Reference
126
+
127
+ ### ViewManager
128
+
129
+ The core class that manages the canvas viewport.
130
+
131
+ ```typescript
132
+ const view = new ViewManager(canvas, renderFn, options?)
133
+ ```
134
+
135
+ #### Options
136
+
137
+ | Option | Type | Default | Description |
138
+ |--------|------|---------|-------------|
139
+ | `minZoom` | `number` | `0.5` | Minimum zoom level |
140
+ | `maxZoom` | `number` | `10` | Maximum zoom level |
141
+ | `wheelSensitivity` | `number` | `0.0015` | Mouse wheel zoom sensitivity |
142
+ | `friction` | `number` | `0.92` | Inertia friction (per frame) |
143
+ | `background` | `string` | `'#fff'` | Canvas background color |
144
+ | `drawDocBorder` | `boolean` | `false` | Draw border around document |
145
+ | `panClampMode` | `'margin' \| 'minVisible'` | `'minVisible'` | Pan restriction mode |
146
+
147
+ #### Methods
148
+
149
+ ```typescript
150
+ // Zoom
151
+ view.zoomToAtScreen(anchorX, anchorY, zoom) // Zoom to absolute level at screen point
152
+ view.zoomByFactorAtScreen(anchorX, anchorY, factor) // Zoom by factor at screen point
153
+ view.zoomInAtCenter() // Zoom in at canvas center
154
+ view.zoomOutAtCenter() // Zoom out at canvas center
155
+
156
+ // Coordinate conversion
157
+ view.toWorld(screenX, screenY) // Screen → World coordinates
158
+ view.toScreen(worldX, worldY) // World → Screen coordinates
159
+
160
+ // Pan control
161
+ view.enablePan()
162
+ view.disablePan()
163
+
164
+ // Zoom control
165
+ view.enableZoom()
166
+ view.disableZoom()
167
+
168
+ // Document mode (constrained panning)
169
+ view.setDocument(x, y, width, height)
170
+ view.setDocumentMargins(left, right, top, bottom)
171
+
172
+ // Reset
173
+ view.reset() // Smooth reset to initial state
174
+
175
+ // Cleanup
176
+ view.destroy()
177
+ ```
178
+
179
+ ### ContentLayerManager
180
+
181
+ Manages layers that render in world space.
182
+
183
+ ```typescript
184
+ const layerManager = new ContentLayerManager()
185
+
186
+ // Create image layer from URL/File/Blob
187
+ const imageLayer = await layerManager.createImageLayer({
188
+ src: 'image.png',
189
+ x: 0,
190
+ y: 0,
191
+ width: 500, // optional, uses natural size if omitted
192
+ height: 300, // optional
193
+ anchor: 'center', // 'topLeft' | 'center'
194
+ rotation: 0 // radians
195
+ })
196
+
197
+ // Add custom layer
198
+ layerManager.addLayer(layer)
199
+
200
+ // Remove layer
201
+ layerManager.removeLayer(layer)
202
+
203
+ // Cleanup
204
+ layerManager.destroy()
205
+ ```
206
+
207
+ ### CanvasLayer
208
+
209
+ A drawable layer with an offscreen canvas.
210
+
211
+ ```typescript
212
+ const layer = new CanvasLayer({
213
+ width: 1200,
214
+ height: 800,
215
+ x: 0,
216
+ y: 0,
217
+ anchor: 'topLeft',
218
+ space: 'world' // 'world' | 'screen'
219
+ })
220
+
221
+ // Drawing API
222
+ layer.beginStroke(worldX, worldY)
223
+ layer.stroke(worldX, worldY, '#000000', 10, pressure, 'brush') // color, size, pressure, mode
224
+ layer.endStroke()
225
+
226
+ // Snapshot API for undo/redo
227
+ const snapshot = layer.captureSnapshot() // Capture current state
228
+ layer.restoreSnapshot(snapshot) // Restore to snapshot
229
+
230
+ // Direct canvas access
231
+ layer.context.fillRect(0, 0, 100, 100)
232
+ ```
233
+
234
+ ### BitmapLayer
235
+
236
+ Extends `CanvasLayer` for image display.
237
+
238
+ ```typescript
239
+ const layer = await BitmapLayer.fromImage({
240
+ src: 'image.png', // URL, File, or Blob
241
+ x: 0,
242
+ y: 0,
243
+ width: 500,
244
+ height: 300,
245
+ anchor: 'center',
246
+ rotation: Math.PI / 4
247
+ })
248
+
249
+ // Replace image source
250
+ await layer.setSource('new-image.png')
251
+
252
+ // Paint on the bitmap
253
+ layer.paint((ctx, canvas) => {
254
+ ctx.fillStyle = 'red'
255
+ ctx.fillRect(10, 10, 50, 50)
256
+ })
257
+
258
+ // Pixel access
259
+ const imageData = layer.getImageData()
260
+ layer.putImageData(imageData, 0, 0)
261
+ ```
262
+
263
+ ### HistoryManager
264
+
265
+ Command-based undo/redo system.
266
+
267
+ ```typescript
268
+ const historyManager = new HistoryManager({
269
+ maxHistorySize: 50,
270
+ undoStack: [], // optional, pass reactive array for UI binding
271
+ redoStack: []
272
+ })
273
+
274
+ // Add a command (already executed)
275
+ historyManager.addCommand(command)
276
+
277
+ // Execute and add a command
278
+ historyManager.executeCommand(command)
279
+
280
+ historyManager.undo()
281
+ historyManager.redo()
282
+ historyManager.canUndo()
283
+ historyManager.canRedo()
284
+ historyManager.clear()
285
+ ```
286
+
287
+ ### SnapshotCommand
288
+
289
+ Built-in command for canvas content undo/redo.
290
+
291
+ ```typescript
292
+ import { createSnapshotCommand } from '@lancercomet/zoom-pan'
293
+
294
+ // Capture before action
295
+ const before = layer.captureSnapshot()
296
+
297
+ // ... perform drawing ...
298
+
299
+ // Capture after action
300
+ const after = layer.captureSnapshot()
301
+
302
+ // Create command and add to history
303
+ const command = createSnapshotCommand(layer, before, after)
304
+ if (command) {
305
+ historyManager.addCommand(command)
306
+ }
307
+ ```
308
+
309
+ ### Plugins
310
+
311
+ The library uses a plugin system for extensibility.
312
+
313
+ #### InteractionPlugin
314
+
315
+ Handles pan, zoom, wheel, and inertia.
316
+
317
+ ```typescript
318
+ import { createInteractionPlugin } from '@lancercomet/zoom-pan'
319
+
320
+ const interactionPlugin = createInteractionPlugin()
321
+ view.use(interactionPlugin)
322
+
323
+ // Control pan/zoom
324
+ interactionPlugin.setPanEnabled(true)
325
+ interactionPlugin.setZoomEnabled(true)
326
+ ```
327
+
328
+ #### DocumentPlugin
329
+
330
+ Handles document bounds and pan clamping.
331
+
332
+ ```typescript
333
+ import { createDocumentPlugin } from '@lancercomet/zoom-pan'
334
+
335
+ const documentPlugin = createDocumentPlugin()
336
+ view.use(documentPlugin)
337
+
338
+ // Set document bounds
339
+ documentPlugin.setRect(0, 0, 1200, 800)
340
+ documentPlugin.setMargins(100, 100, 100, 100)
341
+ documentPlugin.setPanClampMode('minVisible') // 'margin' | 'minVisible'
342
+ documentPlugin.zoomToFit()
343
+ ```
344
+
345
+ ### Custom Commands
346
+
347
+ Implement `ICommand` interface for custom undo/redo actions.
348
+
349
+ ```typescript
350
+ import { ICommand } from '@lancercomet/zoom-pan'
351
+
352
+ class MyCommand implements ICommand {
353
+ readonly type = 'my-command'
354
+
355
+ execute(): void {
356
+ // Do the action
357
+ }
358
+
359
+ undo(): void {
360
+ // Reverse the action
361
+ }
362
+ }
363
+ ```
364
+
365
+ ## Examples
366
+
367
+ Check out the `examples/` directory:
368
+
369
+ - **`examples/viewer/`** - Basic image viewer with pan & zoom
370
+ - **`examples/painter/`** - Full-featured painter with brush, eraser, undo/redo
371
+
372
+ ### Painter Hotkeys
373
+
374
+ | Key | Action |
375
+ |-----|--------|
376
+ | `B` | Brush tool |
377
+ | `E` | Eraser tool |
378
+ | `H` | Pan tool |
379
+ | `Z` | Zoom tool |
380
+ | `Space` (hold) | Temporary pan mode |
381
+ | `Alt` (hold) | Color picker |
382
+ | `Ctrl+Z` | Undo |
383
+ | `Ctrl+Y` / `Ctrl+Shift+Z` | Redo |
384
+
385
+ ## License
386
+
387
+ MIT