@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 +387 -0
- package/dist/zoom-pan.d.ts +831 -0
- package/dist/zoom-pan.mjs +1194 -0
- package/dist/zoom-pan.umd.js +1 -0
- package/package.json +28 -0
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
|