@zoompinch/core 0.0.14 → 0.0.16
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 +507 -0
- package/dist/zoompinch-core.es.js +113 -95
- package/dist/zoompinch-core.umd.js +1 -1
- package/dist/zoompinch.d.ts +12 -5
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# @zoompinch/core
|
|
2
|
+
|
|
3
|
+
Core engine for pinch-to-zoom, pan and rotate experiences on any canvas-like content. Framework-agnostic JavaScript library.
|
|
4
|
+
|
|
5
|
+
**Play with the demo:** [https://zoompinch.pages.dev](https://zoompinch.pages.dev)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @zoompinch/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Complete Example
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<!DOCTYPE html>
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<style>
|
|
20
|
+
#wrapper {
|
|
21
|
+
width: 800px;
|
|
22
|
+
height: 600px;
|
|
23
|
+
border: 1px solid #ddd;
|
|
24
|
+
touch-action: none;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
28
|
+
.canvas {
|
|
29
|
+
display: inline-block;
|
|
30
|
+
will-change: transform;
|
|
31
|
+
}
|
|
32
|
+
.matrix {
|
|
33
|
+
position: absolute;
|
|
34
|
+
top: 0;
|
|
35
|
+
left: 0;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 100%;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="wrapper">
|
|
44
|
+
<div class="canvas">
|
|
45
|
+
<img width="1536" height="2048" src="https://imagedelivery.net/mudX-CmAqIANL8bxoNCToA/489df5b2-38ce-46e7-32e0-d50170e8d800/public" />
|
|
46
|
+
</div>
|
|
47
|
+
<div class="matrix">
|
|
48
|
+
<svg width="100%" height="100%">
|
|
49
|
+
<circle id="centerMarker" r="8" fill="red" />
|
|
50
|
+
</svg>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<script type="module">
|
|
55
|
+
import { Zoompinch } from '@zoompinch/core';
|
|
56
|
+
|
|
57
|
+
const wrapper = document.getElementById('wrapper');
|
|
58
|
+
|
|
59
|
+
// Initialize engine
|
|
60
|
+
const engine = new Zoompinch(
|
|
61
|
+
wrapper,
|
|
62
|
+
{ top: 0, left: 0, right: 0, bottom: 0 }, // offset
|
|
63
|
+
0, // translateX
|
|
64
|
+
0, // translateY
|
|
65
|
+
1, // scale
|
|
66
|
+
0, // rotate
|
|
67
|
+
0.5, // minScale
|
|
68
|
+
4, // maxScale
|
|
69
|
+
false, // clampBounds
|
|
70
|
+
true // rotation
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Set up event listeners
|
|
74
|
+
wrapper.addEventListener('wheel', (e) => engine.handleWheel(e));
|
|
75
|
+
wrapper.addEventListener('mousedown', (e) => engine.handleMousedown(e));
|
|
76
|
+
window.addEventListener('mousemove', (e) => engine.handleMousemove(e));
|
|
77
|
+
window.addEventListener('mouseup', (e) => engine.handleMouseup(e));
|
|
78
|
+
|
|
79
|
+
wrapper.addEventListener('touchstart', (e) => engine.handleTouchstart(e));
|
|
80
|
+
window.addEventListener('touchmove', (e) => engine.handleTouchmove(e));
|
|
81
|
+
window.addEventListener('touchend', (e) => engine.handleTouchend(e));
|
|
82
|
+
|
|
83
|
+
wrapper.addEventListener('gesturestart', (e) => engine.handleGesturestart(e));
|
|
84
|
+
window.addEventListener('gesturechange', (e) => engine.handleGesturechange(e));
|
|
85
|
+
window.addEventListener('gestureend', (e) => engine.handleGestureend(e));
|
|
86
|
+
|
|
87
|
+
// Listen for events
|
|
88
|
+
engine.addEventListener('init', () => {
|
|
89
|
+
console.log('Initialized, canvas size:', engine.canvasBounds);
|
|
90
|
+
// Center canvas
|
|
91
|
+
engine.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
engine.addEventListener('update', () => {
|
|
95
|
+
console.log('Transform:', {
|
|
96
|
+
translateX: engine.translateX,
|
|
97
|
+
translateY: engine.translateY,
|
|
98
|
+
scale: engine.scale,
|
|
99
|
+
rotate: engine.rotate
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Update matrix overlay
|
|
103
|
+
updateMatrix();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Handle clicks
|
|
107
|
+
wrapper.addEventListener('click', (e) => {
|
|
108
|
+
const [x, y] = engine.normalizeClientCoords(e.clientX, e.clientY);
|
|
109
|
+
console.log('Canvas position:', x, y);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function updateMatrix() {
|
|
113
|
+
const marker = document.getElementById('centerMarker');
|
|
114
|
+
const [cx, cy] = engine.composePoint(
|
|
115
|
+
engine.canvasBounds.width / 2,
|
|
116
|
+
engine.canvasBounds.height / 2
|
|
117
|
+
);
|
|
118
|
+
marker.setAttribute('cx', cx);
|
|
119
|
+
marker.setAttribute('cy', cy);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Clean up when done
|
|
123
|
+
// engine.destroy();
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API Reference
|
|
130
|
+
|
|
131
|
+
### Constructor
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
new Zoompinch(
|
|
135
|
+
element: HTMLElement,
|
|
136
|
+
offset: Offset,
|
|
137
|
+
translateX: number,
|
|
138
|
+
translateY: number,
|
|
139
|
+
scale: number,
|
|
140
|
+
rotate: number,
|
|
141
|
+
minScale?: number,
|
|
142
|
+
maxScale?: number,
|
|
143
|
+
clampBounds?: boolean,
|
|
144
|
+
rotation?: boolean
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Parameters:**
|
|
149
|
+
|
|
150
|
+
| Parameter | Type | Default | Description |
|
|
151
|
+
|-----------|------|---------|-------------|
|
|
152
|
+
| `element` | `HTMLElement` | - | Wrapper element (must contain `.canvas` child) |
|
|
153
|
+
| `offset` | `Offset` | - | Inner padding: `{ top, right, bottom, left }` |
|
|
154
|
+
| `translateX` | `number` | - | Initial X translation in pixels |
|
|
155
|
+
| `translateY` | `number` | - | Initial Y translation in pixels |
|
|
156
|
+
| `scale` | `number` | - | Initial scale factor |
|
|
157
|
+
| `rotate` | `number` | - | Initial rotation in radians |
|
|
158
|
+
| `minScale` | `number` | `0.1` | Minimum scale (user gestures only) |
|
|
159
|
+
| `maxScale` | `number` | `10` | Maximum scale (user gestures only) |
|
|
160
|
+
| `clampBounds` | `boolean` | `false` | Clamp panning within bounds (user gestures only) |
|
|
161
|
+
| `rotation` | `boolean` | `true` | Enable rotation gestures |
|
|
162
|
+
|
|
163
|
+
**HTML Structure Required:**
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<div id="wrapper">
|
|
167
|
+
<div class="canvas">
|
|
168
|
+
<!-- Your content here -->
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Note:** `minScale`, `maxScale`, `rotation`, and `clampBounds` only apply during user interaction. Direct property changes are unrestricted.
|
|
174
|
+
|
|
175
|
+
### Properties
|
|
176
|
+
|
|
177
|
+
Access and modify transform state:
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
engine.translateX // number - X translation
|
|
181
|
+
engine.translateY // number - Y translation
|
|
182
|
+
engine.scale // number - Scale factor
|
|
183
|
+
engine.rotate // number - Rotation in radians
|
|
184
|
+
|
|
185
|
+
engine.minScale // number - Minimum scale
|
|
186
|
+
engine.maxScale // number - Maximum scale
|
|
187
|
+
engine.clampBounds // boolean - Clamp bounds flag
|
|
188
|
+
engine.rotation // boolean - Rotation enabled flag
|
|
189
|
+
|
|
190
|
+
engine.offset // Offset - Inner padding object
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Read-only properties:**
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
engine.canvasBounds // Bounds - Canvas dimensions: { x, y, width, height }
|
|
197
|
+
engine.wrapperBounds // Bounds - Wrapper dimensions: { x, y, width, height }
|
|
198
|
+
engine.naturalScale // number - Scale to fit canvas in wrapper
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Events
|
|
204
|
+
|
|
205
|
+
The engine extends `EventTarget` and emits two events:
|
|
206
|
+
|
|
207
|
+
| Event | Description |
|
|
208
|
+
|-------|-------------|
|
|
209
|
+
| `init` | Fired when canvas dimensions are available |
|
|
210
|
+
| `update` | Fired when transform changes |
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
engine.addEventListener('init', () => {
|
|
214
|
+
console.log('Canvas ready:', engine.canvasBounds);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
engine.addEventListener('update', () => {
|
|
218
|
+
console.log('Transform:', engine.translateX, engine.translateY, engine.scale, engine.rotate);
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Methods
|
|
223
|
+
|
|
224
|
+
#### `applyTransform(scale, wrapperCoords, canvasCoords)`
|
|
225
|
+
|
|
226
|
+
Apply transform by anchoring a canvas point to a wrapper point.
|
|
227
|
+
|
|
228
|
+
**Parameters:**
|
|
229
|
+
- `scale: number` - Target scale
|
|
230
|
+
- `wrapperCoords: [number, number]` - Wrapper position (0-1, 0.5 = center)
|
|
231
|
+
- `canvasCoords: [number, number]` - Canvas position (0-1, 0.5 = center)
|
|
232
|
+
|
|
233
|
+
**Examples:**
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Center canvas at scale 1
|
|
237
|
+
engine.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
|
|
238
|
+
|
|
239
|
+
// Zoom to 2x, keep centered
|
|
240
|
+
engine.applyTransform(2, [0.5, 0.5], [0.5, 0.5]);
|
|
241
|
+
|
|
242
|
+
// Anchor canvas top-left to wrapper center
|
|
243
|
+
engine.applyTransform(1.5, [0.5, 0.5], [0, 0]);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### `normalizeClientCoords(clientX, clientY)`
|
|
247
|
+
|
|
248
|
+
Convert global client coordinates to canvas coordinates.
|
|
249
|
+
|
|
250
|
+
**Parameters:**
|
|
251
|
+
- `clientX: number` - Global X from event
|
|
252
|
+
- `clientY: number` - Global Y from event
|
|
253
|
+
|
|
254
|
+
**Returns:** `[number, number]` - Canvas coordinates in pixels
|
|
255
|
+
|
|
256
|
+
**Example:**
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
wrapper.addEventListener('click', (e) => {
|
|
260
|
+
const [x, y] = engine.normalizeClientCoords(e.clientX, e.clientY);
|
|
261
|
+
console.log('Canvas position:', x, y);
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `composePoint(x, y)`
|
|
266
|
+
|
|
267
|
+
Convert canvas coordinates to wrapper coordinates (accounts for transform).
|
|
268
|
+
|
|
269
|
+
**Parameters:**
|
|
270
|
+
- `x: number` - Canvas X in pixels
|
|
271
|
+
- `y: number` - Canvas Y in pixels
|
|
272
|
+
|
|
273
|
+
**Returns:** `[number, number]` - Wrapper coordinates in pixels
|
|
274
|
+
|
|
275
|
+
**Example:**
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
// Get wrapper position for canvas center
|
|
279
|
+
const [wrapperX, wrapperY] = engine.composePoint(
|
|
280
|
+
engine.canvasBounds.width / 2,
|
|
281
|
+
engine.canvasBounds.height / 2
|
|
282
|
+
);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### `rotateCanvas(x, y, radians)`
|
|
286
|
+
|
|
287
|
+
Rotate canvas around a specific canvas point.
|
|
288
|
+
|
|
289
|
+
**Parameters:**
|
|
290
|
+
- `x: number` - Canvas X (rotation center)
|
|
291
|
+
- `y: number` - Canvas Y (rotation center)
|
|
292
|
+
- `radians: number` - Rotation angle
|
|
293
|
+
|
|
294
|
+
**Example:**
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
// Rotate 90° around canvas center
|
|
298
|
+
const centerX = engine.canvasBounds.width / 2;
|
|
299
|
+
const centerY = engine.canvasBounds.height / 2;
|
|
300
|
+
engine.rotateCanvas(centerX, centerY, Math.PI / 2);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### `update()`
|
|
304
|
+
|
|
305
|
+
Manually trigger a transform update and render.
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Modify transform
|
|
309
|
+
engine.translateX = 100;
|
|
310
|
+
engine.translateY = 50;
|
|
311
|
+
engine.scale = 2;
|
|
312
|
+
|
|
313
|
+
// Apply changes
|
|
314
|
+
engine.update();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### `setTranslateFromUserGesture(x, y)`
|
|
318
|
+
|
|
319
|
+
Set translation with optional clamping based on `clampBounds` setting.
|
|
320
|
+
|
|
321
|
+
**Parameters:**
|
|
322
|
+
- `x: number` - X translation
|
|
323
|
+
- `y: number` - Y translation
|
|
324
|
+
|
|
325
|
+
**Example:**
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
engine.setTranslateFromUserGesture(100, 50);
|
|
329
|
+
engine.update();
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### `destroy()`
|
|
333
|
+
|
|
334
|
+
Clean up the engine and remove internal observers.
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
engine.destroy();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Event Handlers
|
|
341
|
+
|
|
342
|
+
Handle user input by calling these methods:
|
|
343
|
+
|
|
344
|
+
#### Mouse Events
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
wrapper.addEventListener('wheel', (e) => engine.handleWheel(e));
|
|
348
|
+
wrapper.addEventListener('mousedown', (e) => engine.handleMousedown(e));
|
|
349
|
+
window.addEventListener('mousemove', (e) => engine.handleMousemove(e));
|
|
350
|
+
window.addEventListener('mouseup', (e) => engine.handleMouseup(e));
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Touch Events
|
|
354
|
+
|
|
355
|
+
```javascript
|
|
356
|
+
wrapper.addEventListener('touchstart', (e) => engine.handleTouchstart(e));
|
|
357
|
+
window.addEventListener('touchmove', (e) => engine.handleTouchmove(e));
|
|
358
|
+
window.addEventListener('touchend', (e) => engine.handleTouchend(e));
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### Gesture Events (Safari)
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
wrapper.addEventListener('gesturestart', (e) => engine.handleGesturestart(e));
|
|
365
|
+
window.addEventListener('gesturechange', (e) => engine.handleGesturechange(e));
|
|
366
|
+
window.addEventListener('gestureend', (e) => engine.handleGestureend(e));
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
## Coordinate Systems
|
|
371
|
+
|
|
372
|
+
### 1. Canvas Coordinates (Absolute)
|
|
373
|
+
|
|
374
|
+
Absolute pixels within canvas content.
|
|
375
|
+
- Origin: `(0, 0)` at top-left
|
|
376
|
+
- Range: `0` to `canvasBounds.width`, `0` to `canvasBounds.height`
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
const [canvasX, canvasY] = engine.normalizeClientCoords(event.clientX, event.clientY);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### 2. Wrapper Coordinates (Absolute)
|
|
383
|
+
|
|
384
|
+
Absolute pixels within viewport/wrapper.
|
|
385
|
+
- Origin: `(0, 0)` at top-left (accounting for offset)
|
|
386
|
+
- Range: `0` to `wrapperBounds.width`, `0` to `wrapperBounds.height`
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
const [wrapperX, wrapperY] = engine.composePoint(canvasX, canvasY);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### 3. Relative Coordinates (0-1)
|
|
393
|
+
|
|
394
|
+
Normalized coordinates for `applyTransform`.
|
|
395
|
+
- Range: `0.0` to `1.0`
|
|
396
|
+
- `0.5` = center, `1.0` = bottom-right
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
[0, 0] // top-left
|
|
400
|
+
[0.5, 0.5] // center
|
|
401
|
+
[1, 1] // bottom-right
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Conversion Flow:**
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
Client Coords → normalizeClientCoords() → Canvas Coords → composePoint() → Wrapper Coords
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Best Practices
|
|
411
|
+
|
|
412
|
+
1. **Required HTML structure:**
|
|
413
|
+
```html
|
|
414
|
+
<div id="wrapper">
|
|
415
|
+
<div class="canvas">
|
|
416
|
+
<!-- content -->
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
2. **Required CSS:**
|
|
422
|
+
```css
|
|
423
|
+
#wrapper {
|
|
424
|
+
touch-action: none;
|
|
425
|
+
overflow: hidden;
|
|
426
|
+
position: relative;
|
|
427
|
+
}
|
|
428
|
+
.canvas {
|
|
429
|
+
will-change: transform;
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
3. **Attach event listeners to window for mouse/touch move/end:**
|
|
434
|
+
```javascript
|
|
435
|
+
wrapper.addEventListener('mousedown', ...);
|
|
436
|
+
window.addEventListener('mousemove', ...); // window, not wrapper
|
|
437
|
+
window.addEventListener('mouseup', ...); // window, not wrapper
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
4. **Center content on init:**
|
|
441
|
+
```javascript
|
|
442
|
+
engine.addEventListener('init', () => {
|
|
443
|
+
engine.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
5. **Clean up when done:**
|
|
448
|
+
```javascript
|
|
449
|
+
engine.destroy();
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Advanced Usage
|
|
455
|
+
|
|
456
|
+
### Custom Transform Logic
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
// Direct property manipulation
|
|
460
|
+
engine.translateX = 100;
|
|
461
|
+
engine.translateY = 50;
|
|
462
|
+
engine.scale = 2;
|
|
463
|
+
engine.rotate = Math.PI / 4;
|
|
464
|
+
|
|
465
|
+
// Apply changes
|
|
466
|
+
engine.update();
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Computed Properties
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
// Get wrapper inner dimensions
|
|
473
|
+
const innerWidth = engine.wrapperInnerWidth;
|
|
474
|
+
const innerHeight = engine.wrapperInnerHeight;
|
|
475
|
+
|
|
476
|
+
// Get natural scale (scale to fit)
|
|
477
|
+
const fitScale = engine.naturalScale;
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Clamping Behavior
|
|
481
|
+
|
|
482
|
+
```javascript
|
|
483
|
+
// Enable/disable clamping
|
|
484
|
+
engine.clampBounds = true;
|
|
485
|
+
|
|
486
|
+
// Use clamp-aware setter
|
|
487
|
+
engine.setTranslateFromUserGesture(translateX, translateY);
|
|
488
|
+
engine.update();
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Browser Support
|
|
492
|
+
|
|
493
|
+
- ✅ Chrome/Edge (latest)
|
|
494
|
+
- ✅ Firefox (latest)
|
|
495
|
+
- ✅ Safari (latest, including iOS)
|
|
496
|
+
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
|
|
497
|
+
|
|
498
|
+
## License
|
|
499
|
+
|
|
500
|
+
MIT
|
|
501
|
+
|
|
502
|
+
## Related
|
|
503
|
+
|
|
504
|
+
- [@zoompinch/vue](https://www.npmjs.com/package/@zoompinch/vue) - Vue 3 bindings
|
|
505
|
+
- [@zoompinch/elements](https://www.npmjs.com/package/@zoompinch/elements) - Web Components
|
|
506
|
+
|
|
507
|
+
Built with ❤️ by Elya Maurice Conrad
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
var x = Object.defineProperty;
|
|
2
|
-
var
|
|
3
|
-
var f = (
|
|
4
|
-
function
|
|
5
|
-
return
|
|
2
|
+
var I = (o, h, t) => h in o ? x(o, h, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[h] = t;
|
|
3
|
+
var f = (o, h, t) => I(o, typeof h != "symbol" ? h + "" : h, t);
|
|
4
|
+
function W(o) {
|
|
5
|
+
return o * Math.PI / 180;
|
|
6
6
|
}
|
|
7
|
-
function
|
|
8
|
-
return Math.min(Math.max(
|
|
7
|
+
function C(o, h, t) {
|
|
8
|
+
return Math.min(Math.max(o, h), t);
|
|
9
9
|
}
|
|
10
|
-
function
|
|
11
|
-
const [
|
|
12
|
-
return [
|
|
10
|
+
function w(o, h, t) {
|
|
11
|
+
const [s, e] = o, [a, n] = h, r = Math.cos(t) * (s - a) - Math.sin(t) * (e - n) + a, i = Math.sin(t) * (s - a) + Math.cos(t) * (e - n) + n;
|
|
12
|
+
return [r, i];
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
const t = Math.pow(10,
|
|
16
|
-
return Math.round(
|
|
14
|
+
function Y(o, h) {
|
|
15
|
+
const t = Math.pow(10, h);
|
|
16
|
+
return Math.round(o * t) / t;
|
|
17
17
|
}
|
|
18
|
-
function
|
|
19
|
-
var
|
|
20
|
-
return
|
|
18
|
+
function z(o) {
|
|
19
|
+
var h = !1;
|
|
20
|
+
return o.wheelDeltaY ? o.wheelDeltaY === o.deltaY * -3 && (h = !0) : o.deltaMode === 0 && (h = !0), h;
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
const t =
|
|
24
|
-
return t ?
|
|
22
|
+
function B(o, h) {
|
|
23
|
+
const t = h.find((s) => o % s === 0);
|
|
24
|
+
return t ? o / t : 1;
|
|
25
25
|
}
|
|
26
|
-
function y(
|
|
27
|
-
let a =
|
|
28
|
-
const
|
|
29
|
-
let c = a *
|
|
30
|
-
const
|
|
31
|
-
return c /=
|
|
26
|
+
function y(o, h, t, s, e) {
|
|
27
|
+
let a = o.left - h, n = o.top - t;
|
|
28
|
+
const r = Math.cos(-e), i = Math.sin(-e);
|
|
29
|
+
let c = a * r - n * i, l = a * i + n * r;
|
|
30
|
+
const u = o.width / s, d = o.height / s;
|
|
31
|
+
return c /= s, l /= s, { x: Y(c, 4), y: Y(l, 4), width: Y(u, 4), height: Y(d, 4) };
|
|
32
32
|
}
|
|
33
33
|
class E extends EventTarget {
|
|
34
|
-
constructor(t,
|
|
34
|
+
constructor(t, s, e, a, n, r, i = 0.1, c = 10, l = !1, u = !0) {
|
|
35
35
|
super();
|
|
36
36
|
f(this, "wrapperBounds");
|
|
37
37
|
f(this, "canvasBounds");
|
|
@@ -42,17 +42,17 @@ class E extends EventTarget {
|
|
|
42
42
|
f(this, "touchStarts", null);
|
|
43
43
|
f(this, "touchStartTranslateX", 0);
|
|
44
44
|
f(this, "touchStartTranslateY", 0);
|
|
45
|
-
this.element = t, this.offset =
|
|
46
|
-
const
|
|
47
|
-
const { x:
|
|
48
|
-
this.wrapperBounds = { x:
|
|
49
|
-
}),
|
|
50
|
-
const { x:
|
|
51
|
-
this.canvasBounds = { x:
|
|
45
|
+
this.element = t, this.offset = s, this.translateX = e, this.translateY = a, this.scale = n, this.rotate = r, this.minScale = i, this.maxScale = c, this.clampBounds = l, this.rotation = u;
|
|
46
|
+
const d = new ResizeObserver(() => {
|
|
47
|
+
const { x: g, y: m, width: S, height: v } = this.element.getBoundingClientRect();
|
|
48
|
+
this.wrapperBounds = { x: g, y: m, width: S, height: v }, this.update();
|
|
49
|
+
}), p = new ResizeObserver(() => {
|
|
50
|
+
const { x: g, y: m, width: S, height: v } = y(this.canvasElement.getBoundingClientRect(), this.renderingTranslateX, this.renderingTranslateY, this.renderinScale, this.renderingRotate);
|
|
51
|
+
this.canvasBounds = { x: g, y: m, width: S, height: v }, this.update();
|
|
52
52
|
});
|
|
53
53
|
requestAnimationFrame(() => {
|
|
54
54
|
this.wrapperBounds = this.element.getBoundingClientRect(), this.canvasBounds = this.canvasElement.getBoundingClientRect(), this.update(), this.dispatchEvent(new Event("init"));
|
|
55
|
-
}),
|
|
55
|
+
}), p.observe(this.canvasElement), d.observe(this.element);
|
|
56
56
|
}
|
|
57
57
|
get canvasElement() {
|
|
58
58
|
return this.element.querySelector(".canvas");
|
|
@@ -80,15 +80,24 @@ class E extends EventTarget {
|
|
|
80
80
|
get naturalScale() {
|
|
81
81
|
return this.canvasNaturalRatio >= this.wrapperInnerRatio ? this.wrapperInnerWidth / this.canvasBounds.width : this.wrapperInnerHeight / this.canvasBounds.height;
|
|
82
82
|
}
|
|
83
|
+
// The clamping is an explicit user intention
|
|
84
|
+
// The reason is that we do not want side effects when toggling the clamp
|
|
85
|
+
setTranslateFromUserGesture(t, s) {
|
|
86
|
+
if (this.clampBounds) {
|
|
87
|
+
const e = this.clampTranslate({ translateX: t, translateY: s, scale: this.scale, rotate: this.rotate });
|
|
88
|
+
this.translateX = e.translateX, this.translateY = e.translateY, this.rotate = 0;
|
|
89
|
+
} else
|
|
90
|
+
this.translateX = t, this.translateY = s;
|
|
91
|
+
}
|
|
83
92
|
handleGesturestart(t) {
|
|
84
93
|
this.gestureStartRotate = this.rotate;
|
|
85
94
|
}
|
|
86
95
|
handleGesturechange(t) {
|
|
87
|
-
const { clientX:
|
|
88
|
-
if (a === 0)
|
|
96
|
+
const { clientX: s, clientY: e } = t, a = t.rotation;
|
|
97
|
+
if (a === 0 || !this.rotation)
|
|
89
98
|
return;
|
|
90
|
-
const n = this.
|
|
91
|
-
this.rotateCanvas(n[0], n[1], this.gestureStartRotate +
|
|
99
|
+
const n = this.normalizeClientCoords(s, e);
|
|
100
|
+
this.rotateCanvas(n[0], n[1], this.gestureStartRotate + W(a));
|
|
92
101
|
}
|
|
93
102
|
handleGestureend(t) {
|
|
94
103
|
}
|
|
@@ -100,28 +109,28 @@ class E extends EventTarget {
|
|
|
100
109
|
}
|
|
101
110
|
handleMousemove(t) {
|
|
102
111
|
if (t.preventDefault(), this.dragStart && this.dragStartFrozenX !== null && this.dragStartFrozenY !== null) {
|
|
103
|
-
const
|
|
104
|
-
this.
|
|
112
|
+
const s = t.clientX - this.dragStart[0], e = t.clientY - this.dragStart[1], a = this.dragStartFrozenX - -s, n = this.dragStartFrozenY - -e;
|
|
113
|
+
this.setTranslateFromUserGesture(a, n), this.update();
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
handleWheel(t) {
|
|
108
|
-
let { deltaX:
|
|
109
|
-
const n = [120, 100],
|
|
110
|
-
|
|
111
|
-
const
|
|
117
|
+
let { deltaX: s, deltaY: e, ctrlKey: a } = t;
|
|
118
|
+
const n = [120, 100], r = 2;
|
|
119
|
+
z(t) || ((Math.abs(s) === 120 || Math.abs(s) === 200) && (s = s / (100 / r * B(s, n)) * Math.sign(s)), (Math.abs(e) === 120 || Math.abs(e) === 200) && (e = e / (100 / r * B(e, n)) * Math.sign(e)));
|
|
120
|
+
const i = this.scale;
|
|
112
121
|
if (a) {
|
|
113
|
-
const c = -
|
|
114
|
-
this.
|
|
122
|
+
const c = -e / 100 * i, l = C(i + c, this.minScale, this.maxScale), u = this.relativeWrapperCoordinatesFromClientCoords(t.clientX, t.clientY), [d, p] = this.calcProjectionTranslate(l, u, this.normalizeMatrixCoordinates(t.clientX, t.clientY));
|
|
123
|
+
this.setTranslateFromUserGesture(d, p), this.scale = l;
|
|
115
124
|
} else
|
|
116
|
-
this.
|
|
125
|
+
this.setTranslateFromUserGesture(this.translateX - s, this.translateY - e);
|
|
117
126
|
this.update(), t.preventDefault();
|
|
118
127
|
}
|
|
119
128
|
freezeTouches(t) {
|
|
120
|
-
return Array.from(t).map((
|
|
121
|
-
const
|
|
129
|
+
return Array.from(t).map((s) => {
|
|
130
|
+
const e = this.clientCoordsToWrapperCoords(s.clientX, s.clientY);
|
|
122
131
|
return {
|
|
123
|
-
client: [
|
|
124
|
-
canvasRel: this.getCanvasCoordsRel(
|
|
132
|
+
client: [s.clientX, s.clientY],
|
|
133
|
+
canvasRel: this.getCanvasCoordsRel(e[0], e[1])
|
|
125
134
|
};
|
|
126
135
|
});
|
|
127
136
|
}
|
|
@@ -130,18 +139,21 @@ class E extends EventTarget {
|
|
|
130
139
|
}
|
|
131
140
|
handleTouchmove(t) {
|
|
132
141
|
t.preventDefault();
|
|
133
|
-
const
|
|
142
|
+
const s = Array.from(t.touches).map((e) => this.clientCoordsToWrapperCoords(e.clientX, e.clientY));
|
|
134
143
|
if (this.touchStarts) {
|
|
135
|
-
if (
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
if (s.length >= 2 && this.touchStarts.length >= 2) {
|
|
145
|
+
const e = [this.touchStarts[0].canvasRel[0] * this.canvasBounds.width, this.touchStarts[0].canvasRel[1] * this.canvasBounds.height], a = [this.touchStarts[1].canvasRel[0] * this.canvasBounds.width, this.touchStarts[1].canvasRel[1] * this.canvasBounds.height], n = Math.sqrt(Math.pow(e[0] - a[0], 2) + Math.pow(e[1] - a[1], 2)), r = Math.sqrt(Math.pow(s[0][0] - s[1][0], 2) + Math.pow(s[0][1] - s[1][1], 2)) / this.naturalScale, i = C(r / n, this.minScale, this.maxScale), c = [s[0][0] / this.wrapperInnerWidth, s[0][1] / this.wrapperInnerHeight], l = this.touchStarts[0].canvasRel, [u, d] = this.calcProjectionTranslate(i, c, l, 0);
|
|
146
|
+
if (this.rotation) {
|
|
147
|
+
let p = 0, g = 0, m = 0;
|
|
148
|
+
const S = Math.atan2(a[1] - e[1], a[0] - e[0]);
|
|
149
|
+
m = Math.atan2(s[1][1] - s[0][1], s[1][0] - s[0][0]) - S;
|
|
150
|
+
const T = (P, F) => [this.offset.left + this.canvasBounds.width * P * this.naturalScale * i + u, this.offset.top + this.canvasBounds.height * F * this.naturalScale * i + d], X = T(0, 0), M = T(this.touchStarts[0].canvasRel[0], this.touchStarts[0].canvasRel[1]), R = w(X, M, m);
|
|
151
|
+
p = R[0] - X[0], g = R[1] - X[1], this.scale = i, this.rotate = m, this.setTranslateFromUserGesture(u + p, d + g);
|
|
152
|
+
} else
|
|
153
|
+
this.scale = i, this.setTranslateFromUserGesture(u, d);
|
|
142
154
|
} else {
|
|
143
|
-
const
|
|
144
|
-
this.
|
|
155
|
+
const e = t.touches[0].clientX - this.touchStarts[0].client[0], a = t.touches[0].clientY - this.touchStarts[0].client[1], n = this.touchStartTranslateX + e, r = this.touchStartTranslateY + a;
|
|
156
|
+
this.setTranslateFromUserGesture(n, r);
|
|
145
157
|
}
|
|
146
158
|
this.update();
|
|
147
159
|
}
|
|
@@ -149,58 +161,57 @@ class E extends EventTarget {
|
|
|
149
161
|
handleTouchend(t) {
|
|
150
162
|
t.touches.length === 0 ? this.touchStarts = null : (this.touchStarts = this.freezeTouches(t.touches), this.touchStartTranslateX = this.translateX, this.touchStartTranslateY = this.translateY);
|
|
151
163
|
}
|
|
152
|
-
calcProjectionTranslate(t,
|
|
153
|
-
const n = this.canvasBounds.width * this.naturalScale,
|
|
164
|
+
calcProjectionTranslate(t, s, e, a) {
|
|
165
|
+
const n = this.canvasBounds.width * this.naturalScale, r = this.canvasBounds.height * this.naturalScale, i = e[0] * n * t, c = e[1] * r * t, l = w([i, c], [0, 0], a ?? this.rotate), u = s[0] * this.wrapperInnerWidth, d = s[1] * this.wrapperInnerHeight, p = u - l[0], g = d - l[1];
|
|
154
166
|
return [p, g];
|
|
155
167
|
}
|
|
156
|
-
applyTransform(t,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.scale = t, this.translateX = a[0], this.translateY = a[1], this.update();
|
|
168
|
+
applyTransform(t, s, e) {
|
|
169
|
+
const a = this.calcProjectionTranslate(t, s, e, 0);
|
|
170
|
+
this.scale = t, this.setTranslateFromUserGesture(a[0], a[1]), this.update();
|
|
160
171
|
}
|
|
161
|
-
composeRelPoint(t,
|
|
162
|
-
|
|
163
|
-
const
|
|
172
|
+
composeRelPoint(t, s, e, a, n, r) {
|
|
173
|
+
e = e ?? this.scale, a = a ?? this.translateX, n = n ?? this.translateY, r = r ?? this.rotate;
|
|
174
|
+
const i = [this.offset.left, this.offset.top], c = [this.offset.left + this.canvasBounds.width * (e * this.naturalScale) * t, this.offset.top + this.canvasBounds.height * (e * this.naturalScale) * s], l = w(c, i, r);
|
|
164
175
|
return [l[0] + a, l[1] + n];
|
|
165
176
|
}
|
|
166
|
-
composePoint(t,
|
|
167
|
-
const
|
|
168
|
-
return this.composeRelPoint(
|
|
177
|
+
composePoint(t, s) {
|
|
178
|
+
const e = t / this.canvasBounds.width, a = s / this.canvasBounds.height;
|
|
179
|
+
return this.composeRelPoint(e, a);
|
|
169
180
|
}
|
|
170
|
-
getAnchorOffset(t,
|
|
171
|
-
const
|
|
172
|
-
this.offset.left +
|
|
173
|
-
this.offset.top +
|
|
174
|
-
], c = this.composeRelPoint(n[0], n[1], t,
|
|
175
|
-
return [l,
|
|
181
|
+
getAnchorOffset(t, s, e, a, n = [0.5, 0.5]) {
|
|
182
|
+
const r = this.calcProjectionTranslate(t, n, n, 0), i = [
|
|
183
|
+
this.offset.left + r[0] + this.canvasBounds.width * (t * this.naturalScale) * n[0],
|
|
184
|
+
this.offset.top + r[1] + this.canvasBounds.height * (t * this.naturalScale) * n[1]
|
|
185
|
+
], c = this.composeRelPoint(n[0], n[1], t, s, e, a), l = c[0] - i[0], u = c[1] - i[1];
|
|
186
|
+
return [l, u];
|
|
176
187
|
}
|
|
177
188
|
// Converts absolute inner wrapper coordinates to relative canvas coordinates (0-1, 0-1)
|
|
178
|
-
getCanvasCoordsRel(t,
|
|
179
|
-
const
|
|
180
|
-
return [
|
|
189
|
+
getCanvasCoordsRel(t, s) {
|
|
190
|
+
const e = [0, 0], a = [t - this.translateX, s - this.translateY], n = w(a, e, -this.rotate), r = [n[0] / this.renderinScale, n[1] / this.renderinScale];
|
|
191
|
+
return [r[0] / this.canvasBounds.width, r[1] / this.canvasBounds.height];
|
|
181
192
|
}
|
|
182
193
|
// Converts absolute client to coordinates to absolute inner-wrapper coorinates
|
|
183
|
-
clientCoordsToWrapperCoords(t,
|
|
184
|
-
return [t - this.wrapperInnerX,
|
|
194
|
+
clientCoordsToWrapperCoords(t, s) {
|
|
195
|
+
return [t - this.wrapperInnerX, s - this.wrapperInnerY];
|
|
185
196
|
}
|
|
186
197
|
// Converts absolute client coordinates to relative wrapper coordinates (0-1, 0-1)
|
|
187
|
-
relativeWrapperCoordinatesFromClientCoords(t,
|
|
188
|
-
const [
|
|
189
|
-
return [
|
|
198
|
+
relativeWrapperCoordinatesFromClientCoords(t, s) {
|
|
199
|
+
const [e, a] = this.clientCoordsToWrapperCoords(t, s);
|
|
200
|
+
return [e / this.wrapperInnerWidth, a / this.wrapperInnerHeight];
|
|
190
201
|
}
|
|
191
202
|
// Converts client coordinates to relative canvas coordinates (0-1, 0-1)
|
|
192
|
-
normalizeMatrixCoordinates(t,
|
|
193
|
-
const
|
|
194
|
-
return this.getCanvasCoordsRel(
|
|
203
|
+
normalizeMatrixCoordinates(t, s) {
|
|
204
|
+
const e = this.clientCoordsToWrapperCoords(t, s);
|
|
205
|
+
return this.getCanvasCoordsRel(e[0], e[1]);
|
|
195
206
|
}
|
|
196
207
|
// Converts client coordinates to absolute canvas coordinates
|
|
197
|
-
normalizeClientCoords(t,
|
|
198
|
-
const [
|
|
199
|
-
return [
|
|
208
|
+
normalizeClientCoords(t, s) {
|
|
209
|
+
const [e, a] = this.normalizeMatrixCoordinates(t, s);
|
|
210
|
+
return [e * this.canvasBounds.width, a * this.canvasBounds.height];
|
|
200
211
|
}
|
|
201
|
-
rotateCanvas(t,
|
|
202
|
-
const a = this.composeRelPoint(
|
|
203
|
-
this.
|
|
212
|
+
rotateCanvas(t, s, e) {
|
|
213
|
+
const a = t / this.canvasBounds.width, n = s / this.canvasBounds.height, r = this.composeRelPoint(a, n, this.scale, 0, 0, e), i = this.composeRelPoint(a, n);
|
|
214
|
+
this.setTranslateFromUserGesture(i[0] - r[0], i[1] - r[1]), this.rotate = e, this.update();
|
|
204
215
|
}
|
|
205
216
|
get renderinScale() {
|
|
206
217
|
return this.naturalScale * this.scale;
|
|
@@ -214,6 +225,13 @@ class E extends EventTarget {
|
|
|
214
225
|
get renderingRotate() {
|
|
215
226
|
return this.rotate;
|
|
216
227
|
}
|
|
228
|
+
clampTranslate(t, s = [0.5, 0.5]) {
|
|
229
|
+
const e = this.canvasBounds.width * this.naturalScale * t.scale, a = this.canvasBounds.height * this.naturalScale * t.scale, n = e - this.wrapperInnerWidth, r = a - this.wrapperInnerHeight, i = n > 0 ? -n : 0, c = Math.min(0, Math.max(t.translateX, i)), l = r > 0 ? -r : 0, u = Math.min(0, Math.max(t.translateY, l)), d = -Math.min(0, n) * s[0], p = -Math.min(0, r) * s[1];
|
|
230
|
+
return {
|
|
231
|
+
translateX: c + d,
|
|
232
|
+
translateY: u + p
|
|
233
|
+
};
|
|
234
|
+
}
|
|
217
235
|
update() {
|
|
218
236
|
this.canvasElement.style.transformOrigin = "top left", this.canvasElement.style.transform = `translateX(${this.renderingTranslateX}px) translateY(${this.renderingTranslateY}px) scale(${this.renderinScale}) rotate(${this.renderingRotate}rad)`, this.dispatchEvent(new Event("update"));
|
|
219
237
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(f,d){typeof exports=="object"&&typeof module<"u"?d(exports):typeof define=="function"&&define.amd?define(["exports"],d):(f=typeof globalThis<"u"?globalThis:f||self,d(f.Zoompinch={}))})(this,(function(f){"use strict";var D=Object.defineProperty;var b=(f,d,v)=>d in f?D(f,d,{enumerable:!0,configurable:!0,writable:!0,value:v}):f[d]=v;var m=(f,d,v)=>b(f,typeof d!="symbol"?d+"":d,v);function d(o){return o*Math.PI/180}function v(o,u,t){return Math.min(Math.max(o,u),t)}function X(o,u,t){const[e,s]=o,[a,n]=u,r=Math.cos(t)*(e-a)-Math.sin(t)*(s-n)+a,i=Math.sin(t)*(e-a)+Math.cos(t)*(s-n)+n;return[r,i]}function T(o,u){const t=Math.pow(10,u);return Math.round(o*t)/t}function x(o){var u=!1;return o.wheelDeltaY?o.wheelDeltaY===o.deltaY*-3&&(u=!0):o.deltaMode===0&&(u=!0),u}function B(o,u){const t=u.find(e=>o%e===0);return t?o/t:1}function F(o,u,t,e,s){let a=o.left-u,n=o.top-t;const r=Math.cos(-s),i=Math.sin(-s);let l=a*r-n*i,h=a*i+n*r;const c=o.width/e,p=o.height/e;return l/=e,h/=e,{x:T(l,4),y:T(h,4),width:T(c,4),height:T(p,4)}}class I extends EventTarget{constructor(t,e,s,a,n,r,i=.1,l=10,h=!1,c=!0){super();m(this,"wrapperBounds");m(this,"canvasBounds");m(this,"gestureStartRotate",0);m(this,"dragStart",null);m(this,"dragStartFrozenX",null);m(this,"dragStartFrozenY",null);m(this,"touchStarts",null);m(this,"touchStartTranslateX",0);m(this,"touchStartTranslateY",0);this.element=t,this.offset=e,this.translateX=s,this.translateY=a,this.scale=n,this.rotate=r,this.minScale=i,this.maxScale=l,this.clampBounds=h,this.rotation=c;const p=new ResizeObserver(()=>{const{x:S,y:w,width:Y,height:R}=this.element.getBoundingClientRect();this.wrapperBounds={x:S,y:w,width:Y,height:R},this.update()}),g=new ResizeObserver(()=>{const{x:S,y:w,width:Y,height:R}=F(this.canvasElement.getBoundingClientRect(),this.renderingTranslateX,this.renderingTranslateY,this.renderinScale,this.renderingRotate);this.canvasBounds={x:S,y:w,width:Y,height:R},this.update()});requestAnimationFrame(()=>{this.wrapperBounds=this.element.getBoundingClientRect(),this.canvasBounds=this.canvasElement.getBoundingClientRect(),this.update(),this.dispatchEvent(new Event("init"))}),g.observe(this.canvasElement),p.observe(this.element)}get canvasElement(){return this.element.querySelector(".canvas")}get wrapperInnerX(){return this.wrapperBounds.x+this.offset.left}get wrapperInnerY(){return this.wrapperBounds.y+this.offset.top}get wrapperInnerWidth(){return this.wrapperBounds.width-this.offset.left-this.offset.right}get wrapperInnerHeight(){return this.wrapperBounds.height-this.offset.top-this.offset.bottom}get wrapperInnerRatio(){return this.wrapperInnerWidth/this.wrapperInnerHeight}get canvasNaturalRatio(){return this.canvasBounds.width/this.canvasBounds.height}get naturalScale(){return this.canvasNaturalRatio>=this.wrapperInnerRatio?this.wrapperInnerWidth/this.canvasBounds.width:this.wrapperInnerHeight/this.canvasBounds.height}setTranslateFromUserGesture(t,e){if(this.clampBounds){const s=this.clampTranslate({translateX:t,translateY:e,scale:this.scale,rotate:this.rotate});this.translateX=s.translateX,this.translateY=s.translateY,this.rotate=0}else this.translateX=t,this.translateY=e}handleGesturestart(t){this.gestureStartRotate=this.rotate}handleGesturechange(t){const{clientX:e,clientY:s}=t,a=t.rotation;if(a===0||!this.rotation)return;const n=this.normalizeClientCoords(e,s);this.rotateCanvas(n[0],n[1],this.gestureStartRotate+d(a))}handleGestureend(t){}handleMousedown(t){t.preventDefault(),this.dragStart=[t.clientX,t.clientY],this.dragStartFrozenX=this.translateX,this.dragStartFrozenY=this.translateY}handleMouseup(t){t.preventDefault(),this.dragStart=null,this.dragStartFrozenX=null,this.dragStartFrozenY=null}handleMousemove(t){if(t.preventDefault(),this.dragStart&&this.dragStartFrozenX!==null&&this.dragStartFrozenY!==null){const e=t.clientX-this.dragStart[0],s=t.clientY-this.dragStart[1],a=this.dragStartFrozenX- -e,n=this.dragStartFrozenY- -s;this.setTranslateFromUserGesture(a,n),this.update()}}handleWheel(t){let{deltaX:e,deltaY:s,ctrlKey:a}=t;const n=[120,100],r=2;x(t)||((Math.abs(e)===120||Math.abs(e)===200)&&(e=e/(100/r*B(e,n))*Math.sign(e)),(Math.abs(s)===120||Math.abs(s)===200)&&(s=s/(100/r*B(s,n))*Math.sign(s)));const i=this.scale;if(a){const l=-s/100*i,h=v(i+l,this.minScale,this.maxScale),c=this.relativeWrapperCoordinatesFromClientCoords(t.clientX,t.clientY),[p,g]=this.calcProjectionTranslate(h,c,this.normalizeMatrixCoordinates(t.clientX,t.clientY));this.setTranslateFromUserGesture(p,g),this.scale=h}else this.setTranslateFromUserGesture(this.translateX-e,this.translateY-s);this.update(),t.preventDefault()}freezeTouches(t){return Array.from(t).map(e=>{const s=this.clientCoordsToWrapperCoords(e.clientX,e.clientY);return{client:[e.clientX,e.clientY],canvasRel:this.getCanvasCoordsRel(s[0],s[1])}})}handleTouchstart(t){this.touchStarts=this.freezeTouches(t.touches),this.touchStartTranslateX=this.translateX,this.touchStartTranslateY=this.translateY,t.preventDefault()}handleTouchmove(t){t.preventDefault();const e=Array.from(t.touches).map(s=>this.clientCoordsToWrapperCoords(s.clientX,s.clientY));if(this.touchStarts){if(e.length>=2&&this.touchStarts.length>=2){const s=[this.touchStarts[0].canvasRel[0]*this.canvasBounds.width,this.touchStarts[0].canvasRel[1]*this.canvasBounds.height],a=[this.touchStarts[1].canvasRel[0]*this.canvasBounds.width,this.touchStarts[1].canvasRel[1]*this.canvasBounds.height],n=Math.sqrt(Math.pow(s[0]-a[0],2)+Math.pow(s[1]-a[1],2)),r=Math.sqrt(Math.pow(e[0][0]-e[1][0],2)+Math.pow(e[0][1]-e[1][1],2))/this.naturalScale,i=v(r/n,this.minScale,this.maxScale),l=[e[0][0]/this.wrapperInnerWidth,e[0][1]/this.wrapperInnerHeight],h=this.touchStarts[0].canvasRel,[c,p]=this.calcProjectionTranslate(i,l,h,0);if(this.rotation){let g=0,S=0,w=0;const Y=Math.atan2(a[1]-s[1],a[0]-s[0]);w=Math.atan2(e[1][1]-e[0][1],e[1][0]-e[0][0])-Y;const M=(y,z)=>[this.offset.left+this.canvasBounds.width*y*this.naturalScale*i+c,this.offset.top+this.canvasBounds.height*z*this.naturalScale*i+p],C=M(0,0),W=M(this.touchStarts[0].canvasRel[0],this.touchStarts[0].canvasRel[1]),P=X(C,W,w);g=P[0]-C[0],S=P[1]-C[1],this.scale=i,this.rotate=w,this.setTranslateFromUserGesture(c+g,p+S)}else this.scale=i,this.setTranslateFromUserGesture(c,p)}else{const s=t.touches[0].clientX-this.touchStarts[0].client[0],a=t.touches[0].clientY-this.touchStarts[0].client[1],n=this.touchStartTranslateX+s,r=this.touchStartTranslateY+a;this.setTranslateFromUserGesture(n,r)}this.update()}}handleTouchend(t){t.touches.length===0?this.touchStarts=null:(this.touchStarts=this.freezeTouches(t.touches),this.touchStartTranslateX=this.translateX,this.touchStartTranslateY=this.translateY)}calcProjectionTranslate(t,e,s,a){const n=this.canvasBounds.width*this.naturalScale,r=this.canvasBounds.height*this.naturalScale,i=s[0]*n*t,l=s[1]*r*t,h=X([i,l],[0,0],a??this.rotate),c=e[0]*this.wrapperInnerWidth,p=e[1]*this.wrapperInnerHeight,g=c-h[0],S=p-h[1];return[g,S]}applyTransform(t,e,s){const a=this.calcProjectionTranslate(t,e,s,0);this.scale=t,this.setTranslateFromUserGesture(a[0],a[1]),this.update()}composeRelPoint(t,e,s,a,n,r){s=s??this.scale,a=a??this.translateX,n=n??this.translateY,r=r??this.rotate;const i=[this.offset.left,this.offset.top],l=[this.offset.left+this.canvasBounds.width*(s*this.naturalScale)*t,this.offset.top+this.canvasBounds.height*(s*this.naturalScale)*e],h=X(l,i,r);return[h[0]+a,h[1]+n]}composePoint(t,e){const s=t/this.canvasBounds.width,a=e/this.canvasBounds.height;return this.composeRelPoint(s,a)}getAnchorOffset(t,e,s,a,n=[.5,.5]){const r=this.calcProjectionTranslate(t,n,n,0),i=[this.offset.left+r[0]+this.canvasBounds.width*(t*this.naturalScale)*n[0],this.offset.top+r[1]+this.canvasBounds.height*(t*this.naturalScale)*n[1]],l=this.composeRelPoint(n[0],n[1],t,e,s,a),h=l[0]-i[0],c=l[1]-i[1];return[h,c]}getCanvasCoordsRel(t,e){const s=[0,0],a=[t-this.translateX,e-this.translateY],n=X(a,s,-this.rotate),r=[n[0]/this.renderinScale,n[1]/this.renderinScale];return[r[0]/this.canvasBounds.width,r[1]/this.canvasBounds.height]}clientCoordsToWrapperCoords(t,e){return[t-this.wrapperInnerX,e-this.wrapperInnerY]}relativeWrapperCoordinatesFromClientCoords(t,e){const[s,a]=this.clientCoordsToWrapperCoords(t,e);return[s/this.wrapperInnerWidth,a/this.wrapperInnerHeight]}normalizeMatrixCoordinates(t,e){const s=this.clientCoordsToWrapperCoords(t,e);return this.getCanvasCoordsRel(s[0],s[1])}normalizeClientCoords(t,e){const[s,a]=this.normalizeMatrixCoordinates(t,e);return[s*this.canvasBounds.width,a*this.canvasBounds.height]}rotateCanvas(t,e,s){const a=t/this.canvasBounds.width,n=e/this.canvasBounds.height,r=this.composeRelPoint(a,n,this.scale,0,0,s),i=this.composeRelPoint(a,n);this.setTranslateFromUserGesture(i[0]-r[0],i[1]-r[1]),this.rotate=s,this.update()}get renderinScale(){return this.naturalScale*this.scale}get renderingTranslateX(){return this.offset.left+this.translateX}get renderingTranslateY(){return this.offset.top+this.translateY}get renderingRotate(){return this.rotate}clampTranslate(t,e=[.5,.5]){const s=this.canvasBounds.width*this.naturalScale*t.scale,a=this.canvasBounds.height*this.naturalScale*t.scale,n=s-this.wrapperInnerWidth,r=a-this.wrapperInnerHeight,i=n>0?-n:0,l=Math.min(0,Math.max(t.translateX,i)),h=r>0?-r:0,c=Math.min(0,Math.max(t.translateY,h)),p=-Math.min(0,n)*e[0],g=-Math.min(0,r)*e[1];return{translateX:l+p,translateY:c+g}}update(){this.canvasElement.style.transformOrigin="top left",this.canvasElement.style.transform=`translateX(${this.renderingTranslateX}px) translateY(${this.renderingTranslateY}px) scale(${this.renderinScale}) rotate(${this.renderingRotate}rad)`,this.dispatchEvent(new Event("update"))}destroy(){}}f.Zoompinch=I,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
|
package/dist/zoompinch.d.ts
CHANGED
|
@@ -25,9 +25,11 @@ export declare class Zoompinch extends EventTarget {
|
|
|
25
25
|
rotate: number;
|
|
26
26
|
minScale: number;
|
|
27
27
|
maxScale: number;
|
|
28
|
+
clampBounds: boolean;
|
|
29
|
+
rotation: boolean;
|
|
28
30
|
wrapperBounds: Bounds;
|
|
29
31
|
canvasBounds: Bounds;
|
|
30
|
-
constructor(element: HTMLElement, offset: Offset, translateX: number, translateY: number, scale: number, rotate: number, minScale?: number, maxScale?: number);
|
|
32
|
+
constructor(element: HTMLElement, offset: Offset, translateX: number, translateY: number, scale: number, rotate: number, minScale?: number, maxScale?: number, clampBounds?: boolean, rotation?: boolean);
|
|
31
33
|
get canvasElement(): HTMLElement;
|
|
32
34
|
get wrapperInnerX(): number;
|
|
33
35
|
get wrapperInnerY(): number;
|
|
@@ -36,6 +38,7 @@ export declare class Zoompinch extends EventTarget {
|
|
|
36
38
|
get wrapperInnerRatio(): number;
|
|
37
39
|
get canvasNaturalRatio(): number;
|
|
38
40
|
get naturalScale(): number;
|
|
41
|
+
setTranslateFromUserGesture(x: number, y: number): void;
|
|
39
42
|
private gestureStartRotate;
|
|
40
43
|
handleGesturestart(event: UIEvent): void;
|
|
41
44
|
handleGesturechange(event: UIEvent): void;
|
|
@@ -68,10 +71,14 @@ export declare class Zoompinch extends EventTarget {
|
|
|
68
71
|
private normalizeMatrixCoordinates;
|
|
69
72
|
normalizeClientCoords(clientX: number, clientY: number): [number, number];
|
|
70
73
|
rotateCanvas(x: number, y: number, rotate: number): void;
|
|
71
|
-
get renderinScale()
|
|
72
|
-
get renderingTranslateX()
|
|
73
|
-
get renderingTranslateY()
|
|
74
|
-
get renderingRotate()
|
|
74
|
+
private get renderinScale();
|
|
75
|
+
private get renderingTranslateX();
|
|
76
|
+
private get renderingTranslateY();
|
|
77
|
+
private get renderingRotate();
|
|
78
|
+
clampTranslate(rawTransform: Transform, origin?: [number, number]): {
|
|
79
|
+
translateX: number;
|
|
80
|
+
translateY: number;
|
|
81
|
+
};
|
|
75
82
|
update(): void;
|
|
76
83
|
destroy(): void;
|
|
77
84
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoompinch/core",
|
|
3
3
|
"description": "Pinch-and-zoom experience that's feels native and communicates the transform reactively and lets you project any layer on top of the transformed canvas",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.16",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/zoompinch-core.umd.js",
|