@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 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 z = (r, o, t) => o in r ? x(r, o, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[o] = t;
3
- var f = (r, o, t) => z(r, typeof o != "symbol" ? o + "" : o, t);
4
- function I(r) {
5
- return r * Math.PI / 180;
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 R(r, o, t) {
8
- return Math.min(Math.max(r, o), t);
7
+ function C(o, h, t) {
8
+ return Math.min(Math.max(o, h), t);
9
9
  }
10
- function v(r, o, t) {
11
- const [e, s] = r, [a, n] = o, i = Math.cos(t) * (e - a) - Math.sin(t) * (s - n) + a, h = Math.sin(t) * (e - a) + Math.cos(t) * (s - n) + n;
12
- return [i, h];
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 w(r, o) {
15
- const t = Math.pow(10, o);
16
- return Math.round(r * t) / t;
14
+ function Y(o, h) {
15
+ const t = Math.pow(10, h);
16
+ return Math.round(o * t) / t;
17
17
  }
18
- function W(r) {
19
- var o = !1;
20
- return r.wheelDeltaY ? r.wheelDeltaY === r.deltaY * -3 && (o = !0) : r.deltaMode === 0 && (o = !0), o;
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 C(r, o) {
23
- const t = o.find((e) => r % e === 0);
24
- return t ? r / t : 1;
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(r, o, t, e, s) {
27
- let a = r.left - o, n = r.top - t;
28
- const i = Math.cos(-s), h = Math.sin(-s);
29
- let c = a * i - n * h, l = a * h + n * i;
30
- const d = r.width / e, u = r.height / e;
31
- return c /= e, l /= e, { x: w(c, 4), y: w(l, 4), width: w(d, 4), height: w(u, 4) };
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, e, s, a, n, i, h = 0.1, c = 10) {
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 = e, this.translateX = s, this.translateY = a, this.scale = n, this.rotate = i, this.minScale = h, this.maxScale = c;
46
- const l = new ResizeObserver(() => {
47
- const { x: u, y: p, width: g, height: S } = this.element.getBoundingClientRect();
48
- this.wrapperBounds = { x: u, y: p, width: g, height: S }, this.update();
49
- }), d = new ResizeObserver(() => {
50
- const { x: u, y: p, width: g, height: S } = y(this.canvasElement.getBoundingClientRect(), this.renderingTranslateX, this.renderingTranslateY, this.renderinScale, this.renderingRotate);
51
- this.canvasBounds = { x: u, y: p, width: g, height: S }, this.update();
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
- }), d.observe(this.canvasElement), l.observe(this.element), console.log("INIT!");
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: e, clientY: s } = t, a = t.rotation;
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.normalizeMatrixCoordinates(e, s);
91
- this.rotateCanvas(n[0], n[1], this.gestureStartRotate + I(a));
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 e = t.clientX - this.dragStart[0], s = t.clientY - this.dragStart[1], a = this.dragStartFrozenX - -e, n = this.dragStartFrozenY - -s;
104
- this.translateX = a, this.translateY = n, this.update();
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: e, deltaY: s, ctrlKey: a } = t;
109
- const n = [120, 100], i = 2;
110
- W(t) || ((Math.abs(e) === 120 || Math.abs(e) === 200) && (e = e / (100 / i * C(e, n)) * Math.sign(e)), (Math.abs(s) === 120 || Math.abs(s) === 200) && (s = s / (100 / i * C(s, n)) * Math.sign(s)));
111
- const h = this.scale;
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 = -s / 100 * h, l = R(h + c, this.minScale, this.maxScale), d = this.relativeWrapperCoordinatesFromClientCoords(t.clientX, t.clientY), [u, p] = this.calcProjectionTranslate(l, d, this.normalizeMatrixCoordinates(t.clientX, t.clientY));
114
- this.translateX = u, this.translateY = p, this.scale = l;
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.translateX = this.translateX - e, this.translateY = this.translateY - s;
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((e) => {
121
- const s = this.clientCoordsToWrapperCoords(e.clientX, e.clientY);
129
+ return Array.from(t).map((s) => {
130
+ const e = this.clientCoordsToWrapperCoords(s.clientX, s.clientY);
122
131
  return {
123
- client: [e.clientX, e.clientY],
124
- canvasRel: this.getCanvasCoordsRel(s[0], s[1])
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 e = Array.from(t.touches).map((s) => this.clientCoordsToWrapperCoords(s.clientX, s.clientY));
142
+ const s = Array.from(t.touches).map((e) => this.clientCoordsToWrapperCoords(e.clientX, e.clientY));
134
143
  if (this.touchStarts) {
135
- if (e.length >= 2 && this.touchStarts.length >= 2) {
136
- 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)), i = Math.sqrt(Math.pow(e[0][0] - e[1][0], 2) + Math.pow(e[0][1] - e[1][1], 2)) / this.naturalScale, h = R(i / n, this.minScale, this.maxScale), c = [e[0][0] / this.wrapperInnerWidth, e[0][1] / this.wrapperInnerHeight], l = this.touchStarts[0].canvasRel, [d, u] = this.calcProjectionTranslate(h, c, l, 0);
137
- let p = 0, g = 0, S = 0;
138
- const T = Math.atan2(a[1] - s[1], a[0] - s[0]);
139
- S = Math.atan2(e[1][1] - e[0][1], e[1][0] - e[0][0]) - T;
140
- const m = (M, P) => [this.offset.left + this.canvasBounds.width * M * this.naturalScale * h + d, this.offset.top + this.canvasBounds.height * P * this.naturalScale * h + u], Y = m(0, 0), B = m(this.touchStarts[0].canvasRel[0], this.touchStarts[0].canvasRel[1]), X = v(Y, B, S);
141
- p = X[0] - Y[0], g = X[1] - Y[1], this.scale = h, this.rotate = S, this.translateX = d + p, this.translateY = u + g;
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 s = t.touches[0].clientX - this.touchStarts[0].client[0], a = t.touches[0].clientY - this.touchStarts[0].client[1], n = this.touchStartTranslateX + s, i = this.touchStartTranslateY + a;
144
- this.translateX = n, this.translateY = i;
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, e, s, a) {
153
- const n = this.canvasBounds.width * this.naturalScale, i = this.canvasBounds.height * this.naturalScale, h = s[0] * n * t, c = s[1] * i * t, l = v([h, c], [0, 0], a ?? this.rotate), d = e[0] * this.wrapperInnerWidth, u = e[1] * this.wrapperInnerHeight, p = d - l[0], g = u - l[1];
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, e, s) {
157
- console.log("....apply transform");
158
- const a = this.calcProjectionTranslate(t, e, s, 0);
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, e, s, a, n, i) {
162
- s = s ?? this.scale, a = a ?? this.translateX, n = n ?? this.translateY, i = i ?? this.rotate;
163
- const h = [this.offset.left, this.offset.top], c = [this.offset.left + this.canvasBounds.width * (s * this.naturalScale) * t, this.offset.top + this.canvasBounds.height * (s * this.naturalScale) * e], l = v(c, h, i);
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, e) {
167
- const s = t / this.canvasBounds.width, a = e / this.canvasBounds.height;
168
- return this.composeRelPoint(s, a);
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, e, s, a, n = [0.5, 0.5]) {
171
- const i = this.calcProjectionTranslate(t, n, n, 0), h = [
172
- this.offset.left + i[0] + this.canvasBounds.width * (t * this.naturalScale) * n[0],
173
- this.offset.top + i[1] + this.canvasBounds.height * (t * this.naturalScale) * n[1]
174
- ], c = this.composeRelPoint(n[0], n[1], t, e, s, a), l = c[0] - h[0], d = c[1] - h[1];
175
- return [l, d];
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, e) {
179
- const s = [0, 0], a = [t - this.translateX, e - this.translateY], n = v(a, s, -this.rotate), i = [n[0] / this.renderinScale, n[1] / this.renderinScale];
180
- return [i[0] / this.canvasBounds.width, i[1] / this.canvasBounds.height];
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, e) {
184
- return [t - this.wrapperInnerX, e - this.wrapperInnerY];
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, e) {
188
- const [s, a] = this.clientCoordsToWrapperCoords(t, e);
189
- return [s / this.wrapperInnerWidth, a / this.wrapperInnerHeight];
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, e) {
193
- const s = this.clientCoordsToWrapperCoords(t, e);
194
- return this.getCanvasCoordsRel(s[0], s[1]);
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, e) {
198
- const [s, a] = this.normalizeMatrixCoordinates(t, e);
199
- return [s * this.canvasBounds.width, a * this.canvasBounds.height];
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, e, s) {
202
- const a = this.composeRelPoint(t, e, this.scale, 0, 0, s), n = this.composeRelPoint(t, e);
203
- this.translateX = n[0] - a[0], this.translateY = n[1] - a[1], this.rotate = s, this.update();
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(p,d){typeof exports=="object"&&typeof module<"u"?d(exports):typeof define=="function"&&define.amd?define(["exports"],d):(p=typeof globalThis<"u"?globalThis:p||self,d(p.Zoompinch={}))})(this,(function(p){"use strict";var D=Object.defineProperty;var F=(p,d,w)=>d in p?D(p,d,{enumerable:!0,configurable:!0,writable:!0,value:w}):p[d]=w;var S=(p,d,w)=>F(p,typeof d!="symbol"?d+"":d,w);function d(o){return o*Math.PI/180}function w(o,c,t){return Math.min(Math.max(o,c),t)}function Y(o,c,t){const[e,s]=o,[n,a]=c,r=Math.cos(t)*(e-n)-Math.sin(t)*(s-a)+n,i=Math.sin(t)*(e-n)+Math.cos(t)*(s-a)+a;return[r,i]}function X(o,c){const t=Math.pow(10,c);return Math.round(o*t)/t}function M(o){var c=!1;return o.wheelDeltaY?o.wheelDeltaY===o.deltaY*-3&&(c=!0):o.deltaMode===0&&(c=!0),c}function C(o,c){const t=c.find(e=>o%e===0);return t?o/t:1}function P(o,c,t,e,s){let n=o.left-c,a=o.top-t;const r=Math.cos(-s),i=Math.sin(-s);let l=n*r-a*i,h=n*i+a*r;const u=o.width/e,f=o.height/e;return l/=e,h/=e,{x:X(l,4),y:X(h,4),width:X(u,4),height:X(f,4)}}class x extends EventTarget{constructor(t,e,s,n,a,r,i=.1,l=10){super();S(this,"wrapperBounds");S(this,"canvasBounds");S(this,"gestureStartRotate",0);S(this,"dragStart",null);S(this,"dragStartFrozenX",null);S(this,"dragStartFrozenY",null);S(this,"touchStarts",null);S(this,"touchStartTranslateX",0);S(this,"touchStartTranslateY",0);this.element=t,this.offset=e,this.translateX=s,this.translateY=n,this.scale=a,this.rotate=r,this.minScale=i,this.maxScale=l;const h=new ResizeObserver(()=>{const{x:f,y:g,width:v,height:m}=this.element.getBoundingClientRect();this.wrapperBounds={x:f,y:g,width:v,height:m},this.update()}),u=new ResizeObserver(()=>{const{x:f,y:g,width:v,height:m}=P(this.canvasElement.getBoundingClientRect(),this.renderingTranslateX,this.renderingTranslateY,this.renderinScale,this.renderingRotate);this.canvasBounds={x:f,y:g,width:v,height:m},this.update()});requestAnimationFrame(()=>{this.wrapperBounds=this.element.getBoundingClientRect(),this.canvasBounds=this.canvasElement.getBoundingClientRect(),this.update(),this.dispatchEvent(new Event("init"))}),u.observe(this.canvasElement),h.observe(this.element),console.log("INIT!")}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}handleGesturestart(t){this.gestureStartRotate=this.rotate}handleGesturechange(t){const{clientX:e,clientY:s}=t,n=t.rotation;if(n===0)return;const a=this.normalizeMatrixCoordinates(e,s);this.rotateCanvas(a[0],a[1],this.gestureStartRotate+d(n))}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],n=this.dragStartFrozenX- -e,a=this.dragStartFrozenY- -s;this.translateX=n,this.translateY=a,this.update()}}handleWheel(t){let{deltaX:e,deltaY:s,ctrlKey:n}=t;const a=[120,100],r=2;M(t)||((Math.abs(e)===120||Math.abs(e)===200)&&(e=e/(100/r*C(e,a))*Math.sign(e)),(Math.abs(s)===120||Math.abs(s)===200)&&(s=s/(100/r*C(s,a))*Math.sign(s)));const i=this.scale;if(n){const l=-s/100*i,h=w(i+l,this.minScale,this.maxScale),u=this.relativeWrapperCoordinatesFromClientCoords(t.clientX,t.clientY),[f,g]=this.calcProjectionTranslate(h,u,this.normalizeMatrixCoordinates(t.clientX,t.clientY));this.translateX=f,this.translateY=g,this.scale=h}else this.translateX=this.translateX-e,this.translateY=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],n=[this.touchStarts[1].canvasRel[0]*this.canvasBounds.width,this.touchStarts[1].canvasRel[1]*this.canvasBounds.height],a=Math.sqrt(Math.pow(s[0]-n[0],2)+Math.pow(s[1]-n[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=w(r/a,this.minScale,this.maxScale),l=[e[0][0]/this.wrapperInnerWidth,e[0][1]/this.wrapperInnerHeight],h=this.touchStarts[0].canvasRel,[u,f]=this.calcProjectionTranslate(i,l,h,0);let g=0,v=0,m=0;const y=Math.atan2(n[1]-s[1],n[0]-s[0]);m=Math.atan2(e[1][1]-e[0][1],e[1][0]-e[0][0])-y;const T=(I,W)=>[this.offset.left+this.canvasBounds.width*I*this.naturalScale*i+u,this.offset.top+this.canvasBounds.height*W*this.naturalScale*i+f],R=T(0,0),z=T(this.touchStarts[0].canvasRel[0],this.touchStarts[0].canvasRel[1]),B=Y(R,z,m);g=B[0]-R[0],v=B[1]-R[1],this.scale=i,this.rotate=m,this.translateX=u+g,this.translateY=f+v}else{const s=t.touches[0].clientX-this.touchStarts[0].client[0],n=t.touches[0].clientY-this.touchStarts[0].client[1],a=this.touchStartTranslateX+s,r=this.touchStartTranslateY+n;this.translateX=a,this.translateY=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,n){const a=this.canvasBounds.width*this.naturalScale,r=this.canvasBounds.height*this.naturalScale,i=s[0]*a*t,l=s[1]*r*t,h=Y([i,l],[0,0],n??this.rotate),u=e[0]*this.wrapperInnerWidth,f=e[1]*this.wrapperInnerHeight,g=u-h[0],v=f-h[1];return[g,v]}applyTransform(t,e,s){console.log("....apply transform");const n=this.calcProjectionTranslate(t,e,s,0);this.scale=t,this.translateX=n[0],this.translateY=n[1],this.update()}composeRelPoint(t,e,s,n,a,r){s=s??this.scale,n=n??this.translateX,a=a??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=Y(l,i,r);return[h[0]+n,h[1]+a]}composePoint(t,e){const s=t/this.canvasBounds.width,n=e/this.canvasBounds.height;return this.composeRelPoint(s,n)}getAnchorOffset(t,e,s,n,a=[.5,.5]){const r=this.calcProjectionTranslate(t,a,a,0),i=[this.offset.left+r[0]+this.canvasBounds.width*(t*this.naturalScale)*a[0],this.offset.top+r[1]+this.canvasBounds.height*(t*this.naturalScale)*a[1]],l=this.composeRelPoint(a[0],a[1],t,e,s,n),h=l[0]-i[0],u=l[1]-i[1];return[h,u]}getCanvasCoordsRel(t,e){const s=[0,0],n=[t-this.translateX,e-this.translateY],a=Y(n,s,-this.rotate),r=[a[0]/this.renderinScale,a[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,n]=this.clientCoordsToWrapperCoords(t,e);return[s/this.wrapperInnerWidth,n/this.wrapperInnerHeight]}normalizeMatrixCoordinates(t,e){const s=this.clientCoordsToWrapperCoords(t,e);return this.getCanvasCoordsRel(s[0],s[1])}normalizeClientCoords(t,e){const[s,n]=this.normalizeMatrixCoordinates(t,e);return[s*this.canvasBounds.width,n*this.canvasBounds.height]}rotateCanvas(t,e,s){const n=this.composeRelPoint(t,e,this.scale,0,0,s),a=this.composeRelPoint(t,e);this.translateX=a[0]-n[0],this.translateY=a[1]-n[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}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(){}}p.Zoompinch=x,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})}));
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"})}));
@@ -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(): number;
72
- get renderingTranslateX(): number;
73
- get renderingTranslateY(): number;
74
- get renderingRotate(): number;
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.14",
4
+ "version": "0.0.16",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./dist/zoompinch-core.umd.js",