zoooom 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,770 @@
1
+ // src/core/constants.ts
2
+ var PAN_STEP = 50;
3
+ var ZOOM_FACTOR = 1.5;
4
+ var MIN_SCALE = 0.8;
5
+ var OVERSCALE_FACTOR = 2;
6
+ var VELOCITY_DAMPING = 0.85;
7
+ var TRACKPAD_SENSITIVITY = 2e-3;
8
+
9
+ // src/core/transform.ts
10
+ function updateTransform(state, elements) {
11
+ if (!elements.image) return;
12
+ elements.image.style.transform = `translate(calc(-50% + ${state.translateX}px), calc(-50% + ${state.translateY}px)) scale(${state.scale})`;
13
+ }
14
+ function animateMovement(state, elements, onPan) {
15
+ if (state.reducedMotion) {
16
+ state.velocityX = 0;
17
+ state.velocityY = 0;
18
+ return;
19
+ }
20
+ state.isAnimating = true;
21
+ function step() {
22
+ state.translateX += state.velocityX;
23
+ state.translateY += state.velocityY;
24
+ state.velocityX *= VELOCITY_DAMPING;
25
+ state.velocityY *= VELOCITY_DAMPING;
26
+ updateTransform(state, elements);
27
+ onPan?.(state.translateX, state.translateY);
28
+ if (Math.abs(state.velocityX) > 0.1 || Math.abs(state.velocityY) > 0.1) {
29
+ requestAnimationFrame(step);
30
+ } else {
31
+ state.velocityX = 0;
32
+ state.velocityY = 0;
33
+ state.isAnimating = false;
34
+ }
35
+ }
36
+ requestAnimationFrame(step);
37
+ }
38
+ function centerImage(state, elements) {
39
+ if (!elements.container || !elements.image) return;
40
+ state.translateX = 0;
41
+ state.translateY = 0;
42
+ updateTransform(state, elements);
43
+ }
44
+ function resetView(state, elements) {
45
+ state.scale = 1;
46
+ state.translateX = 0;
47
+ state.translateY = 0;
48
+ state.velocityX = 0;
49
+ state.velocityY = 0;
50
+ updateTransform(state, elements);
51
+ }
52
+ function calculateMaxScale(elements, overscaleFactor = OVERSCALE_FACTOR) {
53
+ if (!elements.image) return 10;
54
+ const naturalWidth = elements.image.naturalWidth;
55
+ const naturalHeight = elements.image.naturalHeight;
56
+ const displayWidth = elements.image.clientWidth;
57
+ const displayHeight = elements.image.clientHeight;
58
+ if (!naturalWidth || !naturalHeight || !displayWidth || !displayHeight) return 10;
59
+ const widthRatio = naturalWidth / displayWidth;
60
+ const heightRatio = naturalHeight / displayHeight;
61
+ const maxScaleFactor = Math.max(widthRatio, heightRatio);
62
+ const mobileOverscale = window.innerWidth <= 768 ? overscaleFactor * 2 : overscaleFactor;
63
+ return Math.max(maxScaleFactor * mobileOverscale, MIN_SCALE);
64
+ }
65
+ function zoomTowardsPoint(state, elements, delta, pointX, pointY, onZoom) {
66
+ if (!elements.container || !elements.image) return;
67
+ const currentScale = state.scale;
68
+ const newScale = Math.max(MIN_SCALE, Math.min(state.scale * delta, state.maxScale));
69
+ if (newScale === currentScale) return;
70
+ const containerRect = elements.container.getBoundingClientRect();
71
+ const containerCenterX = containerRect.width / 2;
72
+ const containerCenterY = containerRect.height / 2;
73
+ const targetX = pointX ?? containerCenterX;
74
+ const targetY = pointY ?? containerCenterY;
75
+ const imageX = (targetX - containerCenterX - state.translateX) / currentScale;
76
+ const imageY = (targetY - containerCenterY - state.translateY) / currentScale;
77
+ state.translateX = targetX - containerCenterX - imageX * newScale;
78
+ state.translateY = targetY - containerCenterY - imageY * newScale;
79
+ if (!state.reducedMotion) {
80
+ elements.image.style.transition = "transform 0.2s ease-out";
81
+ }
82
+ state.scale = newScale;
83
+ updateTransform(state, elements);
84
+ onZoom?.(state.scale);
85
+ if (!state.reducedMotion) {
86
+ setTimeout(() => {
87
+ if (elements.image) {
88
+ elements.image.style.transition = "";
89
+ }
90
+ }, 200);
91
+ }
92
+ }
93
+
94
+ // src/core/loader.ts
95
+ function createLoadingOverlay(container, loading) {
96
+ if (loading === false) return null;
97
+ const overlay = document.createElement("div");
98
+ overlay.className = "zoooom-loading";
99
+ overlay.setAttribute("role", "status");
100
+ overlay.setAttribute("aria-label", "Loading image");
101
+ if (typeof loading === "string") {
102
+ overlay.innerHTML = loading;
103
+ } else {
104
+ overlay.innerHTML = `
105
+ <div class="zoooom-spinner"></div>
106
+ <div class="zoooom-loading-text">Loading...</div>
107
+ `;
108
+ }
109
+ container.appendChild(overlay);
110
+ return overlay;
111
+ }
112
+ function removeLoadingOverlay(elements) {
113
+ if (elements.loadingOverlay) {
114
+ elements.loadingOverlay.remove();
115
+ elements.loadingOverlay = null;
116
+ }
117
+ }
118
+ function loadImage(src, alt, state, elements, options, emit) {
119
+ state.scale = 1;
120
+ state.translateX = 0;
121
+ state.translateY = 0;
122
+ state.velocityX = 0;
123
+ state.velocityY = 0;
124
+ state.isLoaded = false;
125
+ removeLoadingOverlay(elements);
126
+ elements.loadingOverlay = createLoadingOverlay(elements.container, options.loading);
127
+ elements.image.style.opacity = "0";
128
+ elements.image.style.visibility = "hidden";
129
+ elements.image.setAttribute("aria-hidden", "true");
130
+ elements.image.setAttribute("alt", "");
131
+ const preloader = new Image();
132
+ preloader.onload = () => {
133
+ elements.image.src = preloader.src;
134
+ elements.image.onload = () => {
135
+ removeLoadingOverlay(elements);
136
+ elements.image.setAttribute("alt", alt);
137
+ elements.image.removeAttribute("aria-hidden");
138
+ if (state.reducedMotion) {
139
+ elements.image.style.opacity = "1";
140
+ elements.image.style.visibility = "visible";
141
+ } else {
142
+ setTimeout(() => {
143
+ elements.image.style.transition = "opacity 0.3s ease-in-out";
144
+ elements.image.style.opacity = "1";
145
+ elements.image.style.visibility = "visible";
146
+ setTimeout(() => {
147
+ elements.image.style.transition = "";
148
+ }, 300);
149
+ }, 50);
150
+ }
151
+ if (options.maxScale === "auto") {
152
+ state.maxScale = calculateMaxScale(elements, options.overscaleFactor);
153
+ }
154
+ centerImage(state, elements);
155
+ state.isLoaded = true;
156
+ emit("load");
157
+ options.onLoad?.();
158
+ };
159
+ elements.image.onerror = () => {
160
+ removeLoadingOverlay(elements);
161
+ const error = new Error(`Failed to load image: ${src}`);
162
+ emit("error", error);
163
+ options.onError?.(error);
164
+ };
165
+ };
166
+ preloader.onerror = () => {
167
+ removeLoadingOverlay(elements);
168
+ const error = new Error(`Failed to load image: ${src}`);
169
+ emit("error", error);
170
+ options.onError?.(error);
171
+ };
172
+ preloader.src = src;
173
+ }
174
+
175
+ // src/input/mouse.ts
176
+ function attachMouse(state, elements, onPan) {
177
+ const { container } = elements;
178
+ function handleMouseDown(e) {
179
+ if (e.button !== 0) return;
180
+ state.isDragging = true;
181
+ state.startX = e.clientX;
182
+ state.startY = e.clientY;
183
+ container.style.cursor = "grabbing";
184
+ e.preventDefault();
185
+ }
186
+ function handleMouseMove(e) {
187
+ if (!state.isDragging) return;
188
+ const dx = e.clientX - state.startX;
189
+ const dy = e.clientY - state.startY;
190
+ state.translateX += dx;
191
+ state.translateY += dy;
192
+ state.startX = e.clientX;
193
+ state.startY = e.clientY;
194
+ updateTransform(state, elements);
195
+ onPan?.(state.translateX, state.translateY);
196
+ }
197
+ function handleMouseUp() {
198
+ if (state.isDragging) {
199
+ state.isDragging = false;
200
+ container.style.cursor = "grab";
201
+ }
202
+ }
203
+ container.addEventListener("mousedown", handleMouseDown);
204
+ container.addEventListener("mousemove", handleMouseMove);
205
+ container.addEventListener("mouseup", handleMouseUp);
206
+ container.addEventListener("mouseleave", handleMouseUp);
207
+ return () => {
208
+ container.removeEventListener("mousedown", handleMouseDown);
209
+ container.removeEventListener("mousemove", handleMouseMove);
210
+ container.removeEventListener("mouseup", handleMouseUp);
211
+ container.removeEventListener("mouseleave", handleMouseUp);
212
+ };
213
+ }
214
+
215
+ // src/input/touch.ts
216
+ function getDistance(e) {
217
+ const t1 = e.touches[0];
218
+ const t2 = e.touches[1];
219
+ const dx = t1.clientX - t2.clientX;
220
+ const dy = t1.clientY - t2.clientY;
221
+ return Math.sqrt(dx * dx + dy * dy);
222
+ }
223
+ function attachTouch(state, elements, overscaleFactor, onPan, onZoom) {
224
+ const { container } = elements;
225
+ function handleTouchStart(e) {
226
+ if (e.touches.length === 1) {
227
+ state.isDragging = true;
228
+ state.startX = e.touches[0].clientX;
229
+ state.startY = e.touches[0].clientY;
230
+ state.initialDistance = 0;
231
+ } else if (e.touches.length === 2) {
232
+ state.isDragging = false;
233
+ state.initialDistance = getDistance(e);
234
+ state.initialScale = state.scale;
235
+ state.initialTranslateX = state.translateX;
236
+ state.initialTranslateY = state.translateY;
237
+ const t1 = e.touches[0];
238
+ const t2 = e.touches[1];
239
+ const rect = container.getBoundingClientRect();
240
+ state.pinchCenter = {
241
+ x: (t1.clientX + t2.clientX) / 2 - rect.left,
242
+ y: (t1.clientY + t2.clientY) / 2 - rect.top
243
+ };
244
+ }
245
+ }
246
+ function handleTouchMove(e) {
247
+ e.preventDefault();
248
+ if (e.touches.length === 1 && state.isDragging) {
249
+ const dx = e.touches[0].clientX - state.startX;
250
+ const dy = e.touches[0].clientY - state.startY;
251
+ state.translateX += dx;
252
+ state.translateY += dy;
253
+ state.startX = e.touches[0].clientX;
254
+ state.startY = e.touches[0].clientY;
255
+ updateTransform(state, elements);
256
+ onPan?.(state.translateX, state.translateY);
257
+ } else if (e.touches.length === 2 && state.initialDistance > 0) {
258
+ const currentDistance = getDistance(e);
259
+ const scaleFactor = currentDistance / state.initialDistance;
260
+ const targetScale = state.initialScale * scaleFactor;
261
+ state.maxScale = calculateMaxScale(elements, overscaleFactor);
262
+ const newScale = Math.max(MIN_SCALE, Math.min(targetScale, state.maxScale));
263
+ if (newScale === state.scale) return;
264
+ const t1 = e.touches[0];
265
+ const t2 = e.touches[1];
266
+ const rect = container.getBoundingClientRect();
267
+ const currentPinchCenter = {
268
+ x: (t1.clientX + t2.clientX) / 2 - rect.left,
269
+ y: (t1.clientY + t2.clientY) / 2 - rect.top
270
+ };
271
+ const containerCenterX = rect.width / 2;
272
+ const containerCenterY = rect.height / 2;
273
+ const imageX = (state.pinchCenter.x - containerCenterX - state.initialTranslateX) / state.initialScale;
274
+ const imageY = (state.pinchCenter.y - containerCenterY - state.initialTranslateY) / state.initialScale;
275
+ state.translateX = currentPinchCenter.x - containerCenterX - imageX * newScale;
276
+ state.translateY = currentPinchCenter.y - containerCenterY - imageY * newScale;
277
+ state.scale = newScale;
278
+ updateTransform(state, elements);
279
+ onZoom?.(state.scale);
280
+ state.pinchCenter = currentPinchCenter;
281
+ }
282
+ }
283
+ function handleTouchEnd() {
284
+ state.isDragging = false;
285
+ state.initialDistance = 0;
286
+ }
287
+ container.addEventListener("touchstart", handleTouchStart, { passive: false });
288
+ container.addEventListener("touchmove", handleTouchMove, { passive: false });
289
+ container.addEventListener("touchend", handleTouchEnd);
290
+ container.addEventListener("touchcancel", handleTouchEnd);
291
+ return () => {
292
+ container.removeEventListener("touchstart", handleTouchStart);
293
+ container.removeEventListener("touchmove", handleTouchMove);
294
+ container.removeEventListener("touchend", handleTouchEnd);
295
+ container.removeEventListener("touchcancel", handleTouchEnd);
296
+ };
297
+ }
298
+
299
+ // src/input/wheel.ts
300
+ function attachWheel(state, elements, zoomFactor = ZOOM_FACTOR, trackpadSensitivity = TRACKPAD_SENSITIVITY, onZoom) {
301
+ const { container } = elements;
302
+ function handleWheel(e) {
303
+ e.preventDefault();
304
+ const rect = container.getBoundingClientRect();
305
+ const pointX = e.clientX - rect.left;
306
+ const pointY = e.clientY - rect.top;
307
+ let zoomDelta;
308
+ if (e.ctrlKey) {
309
+ zoomDelta = e.deltaY < 0 ? zoomFactor : 1 / zoomFactor;
310
+ } else {
311
+ let normalizedDelta;
312
+ switch (e.deltaMode) {
313
+ case 1:
314
+ normalizedDelta = e.deltaY * 20;
315
+ break;
316
+ // LINE
317
+ case 2:
318
+ normalizedDelta = e.deltaY * 100;
319
+ break;
320
+ // PAGE
321
+ default:
322
+ normalizedDelta = e.deltaY;
323
+ }
324
+ if (Math.abs(normalizedDelta) < 40) {
325
+ zoomDelta = Math.exp(-normalizedDelta * trackpadSensitivity);
326
+ } else {
327
+ zoomDelta = normalizedDelta > 0 ? 1 / zoomFactor : zoomFactor;
328
+ }
329
+ }
330
+ container.classList.add("zoooom-scrolling");
331
+ if (state.wheelTimeout) clearTimeout(state.wheelTimeout);
332
+ state.wheelTimeout = setTimeout(() => {
333
+ container.classList.remove("zoooom-scrolling");
334
+ }, 200);
335
+ zoomTowardsPoint(state, elements, zoomDelta, pointX, pointY, onZoom);
336
+ }
337
+ container.addEventListener("wheel", handleWheel, { passive: false });
338
+ return () => {
339
+ container.removeEventListener("wheel", handleWheel);
340
+ if (state.wheelTimeout) clearTimeout(state.wheelTimeout);
341
+ };
342
+ }
343
+
344
+ // src/input/keyboard.ts
345
+ function attachKeyboard(state, elements, panStep = PAN_STEP, zoomFactor = ZOOM_FACTOR, onPan, onZoom) {
346
+ const { container } = elements;
347
+ function handleKeyDown(e) {
348
+ let handled = true;
349
+ switch (e.key) {
350
+ case "ArrowLeft":
351
+ state.velocityX = panStep * 0.2;
352
+ animateMovement(state, elements, onPan);
353
+ break;
354
+ case "ArrowRight":
355
+ state.velocityX = -panStep * 0.2;
356
+ animateMovement(state, elements, onPan);
357
+ break;
358
+ case "ArrowUp":
359
+ state.velocityY = panStep * 0.2;
360
+ animateMovement(state, elements, onPan);
361
+ break;
362
+ case "ArrowDown":
363
+ state.velocityY = -panStep * 0.2;
364
+ animateMovement(state, elements, onPan);
365
+ break;
366
+ case "+":
367
+ case "=":
368
+ zoomTowardsPoint(state, elements, zoomFactor, void 0, void 0, onZoom);
369
+ break;
370
+ case "-":
371
+ zoomTowardsPoint(state, elements, 1 / zoomFactor, void 0, void 0, onZoom);
372
+ break;
373
+ case "r":
374
+ case "R":
375
+ resetView(state, elements);
376
+ break;
377
+ default:
378
+ handled = false;
379
+ }
380
+ if (handled) e.preventDefault();
381
+ }
382
+ container.addEventListener("keydown", handleKeyDown);
383
+ return () => {
384
+ container.removeEventListener("keydown", handleKeyDown);
385
+ };
386
+ }
387
+
388
+ // src/input/gesture.ts
389
+ function attachGesture(state, elements, onZoom) {
390
+ const { container } = elements;
391
+ if (!("ongesturestart" in window)) {
392
+ return () => {
393
+ };
394
+ }
395
+ function handleGestureStart(e) {
396
+ e.preventDefault();
397
+ state.scale;
398
+ }
399
+ function handleGestureChange(e) {
400
+ e.preventDefault();
401
+ const ge = e;
402
+ const rect = container.getBoundingClientRect();
403
+ const delta = ge.scale > 1 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
404
+ zoomTowardsPoint(state, elements, delta, rect.width / 2, rect.height / 2, onZoom);
405
+ }
406
+ function handleGestureEnd(e) {
407
+ e.preventDefault();
408
+ }
409
+ container.addEventListener("gesturestart", handleGestureStart, { passive: false });
410
+ container.addEventListener("gesturechange", handleGestureChange, { passive: false });
411
+ container.addEventListener("gestureend", handleGestureEnd, { passive: false });
412
+ return () => {
413
+ container.removeEventListener("gesturestart", handleGestureStart);
414
+ container.removeEventListener("gesturechange", handleGestureChange);
415
+ container.removeEventListener("gestureend", handleGestureEnd);
416
+ };
417
+ }
418
+
419
+ // src/styles/core.ts
420
+ var ZOOOOM_CSS = `
421
+ [data-zoooom] {
422
+ --zoooom-bg: #000;
423
+ --zoooom-spinner-color: #2196f3;
424
+ --zoooom-spinner-track: rgba(255, 255, 255, 0.3);
425
+ --zoooom-spinner-size: 40px;
426
+ --zoooom-loading-bg: rgba(0, 0, 0, 0.85);
427
+ --zoooom-loading-radius: 10px;
428
+ --zoooom-cursor: grab;
429
+ --zoooom-cursor-active: grabbing;
430
+ --zoooom-transition-speed: 0.2s;
431
+ --zoooom-fade-speed: 0.3s;
432
+
433
+ position: relative;
434
+ width: 100%;
435
+ height: 100%;
436
+ background: var(--zoooom-bg);
437
+ overflow: hidden;
438
+ touch-action: none;
439
+ cursor: var(--zoooom-cursor);
440
+ user-select: none;
441
+ -webkit-user-select: none;
442
+ }
443
+
444
+ [data-zoooom]:focus-visible {
445
+ outline: 2px solid var(--zoooom-spinner-color);
446
+ outline-offset: -2px;
447
+ }
448
+
449
+ [data-zoooom] .zoooom-image {
450
+ position: absolute;
451
+ top: 50%;
452
+ left: 50%;
453
+ transform: translate(-50%, -50%) scale(1);
454
+ max-width: 100%;
455
+ max-height: 100%;
456
+ object-fit: contain;
457
+ user-select: none;
458
+ -webkit-user-select: none;
459
+ pointer-events: none;
460
+ }
461
+
462
+ [data-zoooom] .zoooom-loading {
463
+ position: absolute;
464
+ top: 50%;
465
+ left: 50%;
466
+ transform: translate(-50%, -50%);
467
+ display: flex;
468
+ flex-direction: column;
469
+ align-items: center;
470
+ justify-content: center;
471
+ background: var(--zoooom-loading-bg);
472
+ padding: 20px;
473
+ border-radius: var(--zoooom-loading-radius);
474
+ z-index: 10;
475
+ min-width: 120px;
476
+ }
477
+
478
+ [data-zoooom] .zoooom-spinner {
479
+ width: var(--zoooom-spinner-size);
480
+ height: var(--zoooom-spinner-size);
481
+ border: 4px solid var(--zoooom-spinner-track);
482
+ border-radius: 50%;
483
+ border-top-color: var(--zoooom-spinner-color);
484
+ animation: zoooom-spin 1s linear infinite;
485
+ }
486
+
487
+ [data-zoooom] .zoooom-loading-text {
488
+ margin-top: 12px;
489
+ font-size: 14px;
490
+ color: #fff;
491
+ font-family: system-ui, -apple-system, sans-serif;
492
+ }
493
+
494
+ @keyframes zoooom-spin {
495
+ to { transform: rotate(360deg); }
496
+ }
497
+
498
+ @media (prefers-reduced-motion: reduce) {
499
+ [data-zoooom] .zoooom-spinner {
500
+ animation: none;
501
+ border-top-color: var(--zoooom-spinner-track);
502
+ border-right-color: var(--zoooom-spinner-color);
503
+ }
504
+
505
+ [data-zoooom] .zoooom-image {
506
+ transition: none !important;
507
+ }
508
+ }
509
+ `;
510
+ var injected = false;
511
+ function injectCoreStyles() {
512
+ if (injected || typeof document === "undefined") return;
513
+ const style = document.createElement("style");
514
+ style.setAttribute("data-zoooom-core", "");
515
+ style.textContent = ZOOOOM_CSS;
516
+ document.head.appendChild(style);
517
+ injected = true;
518
+ }
519
+
520
+ // src/core/Zoooom.ts
521
+ var DEFAULTS = {
522
+ src: "",
523
+ alt: "Image",
524
+ minScale: MIN_SCALE,
525
+ maxScale: "auto",
526
+ overscaleFactor: OVERSCALE_FACTOR,
527
+ zoomFactor: ZOOM_FACTOR,
528
+ panStep: PAN_STEP,
529
+ velocityDamping: VELOCITY_DAMPING,
530
+ trackpadSensitivity: TRACKPAD_SENSITIVITY,
531
+ mouse: true,
532
+ touch: true,
533
+ wheel: true,
534
+ keyboard: true,
535
+ loading: true,
536
+ injectStyles: true,
537
+ respectReducedMotion: true
538
+ };
539
+ var Zoooom = class {
540
+ constructor(container, options) {
541
+ this.cleanups = [];
542
+ this.listeners = /* @__PURE__ */ new Map();
543
+ this.resizeHandler = null;
544
+ this.options = { ...DEFAULTS, ...options };
545
+ if (this.options.injectStyles) {
546
+ injectCoreStyles();
547
+ }
548
+ const el = typeof container === "string" ? document.querySelector(container) : container;
549
+ if (!el) {
550
+ throw new Error(`Zoooom: container "${container}" not found`);
551
+ }
552
+ if (el.clientWidth === 0 || el.clientHeight === 0) {
553
+ console.warn("Zoooom: container has zero dimensions. The viewer needs explicit width/height.");
554
+ }
555
+ el.setAttribute("data-zoooom", "");
556
+ if (!el.hasAttribute("tabindex")) {
557
+ el.setAttribute("tabindex", "0");
558
+ }
559
+ el.setAttribute("role", "application");
560
+ el.setAttribute("aria-label", "Image viewer \u2014 use arrow keys to pan, +/- to zoom");
561
+ const img = document.createElement("img");
562
+ img.className = "zoooom-image";
563
+ img.setAttribute("draggable", "false");
564
+ el.appendChild(img);
565
+ this.elements = { container: el, image: img, loadingOverlay: null };
566
+ const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
567
+ const reducedMotion = this.options.respectReducedMotion && motionQuery.matches;
568
+ this.state = {
569
+ scale: 1,
570
+ translateX: 0,
571
+ translateY: 0,
572
+ velocityX: 0,
573
+ velocityY: 0,
574
+ maxScale: typeof this.options.maxScale === "number" ? this.options.maxScale : 10,
575
+ isDragging: false,
576
+ isAnimating: false,
577
+ isLoaded: false,
578
+ startX: 0,
579
+ startY: 0,
580
+ initialDistance: 0,
581
+ initialScale: 1,
582
+ initialTranslateX: 0,
583
+ initialTranslateY: 0,
584
+ pinchCenter: { x: 0, y: 0 },
585
+ wheelTimeout: null,
586
+ reducedMotion
587
+ };
588
+ if (this.options.respectReducedMotion) {
589
+ motionQuery.addEventListener("change", (e) => {
590
+ this.state.reducedMotion = e.matches;
591
+ });
592
+ }
593
+ if (this.options.mouse) {
594
+ this.cleanups.push(attachMouse(this.state, this.elements, this.options.onPan));
595
+ }
596
+ if (this.options.touch) {
597
+ this.cleanups.push(attachTouch(
598
+ this.state,
599
+ this.elements,
600
+ this.options.overscaleFactor,
601
+ this.options.onPan,
602
+ this.options.onZoom
603
+ ));
604
+ }
605
+ if (this.options.wheel) {
606
+ this.cleanups.push(attachWheel(
607
+ this.state,
608
+ this.elements,
609
+ this.options.zoomFactor,
610
+ this.options.trackpadSensitivity,
611
+ this.options.onZoom
612
+ ));
613
+ }
614
+ if (this.options.keyboard) {
615
+ this.cleanups.push(attachKeyboard(
616
+ this.state,
617
+ this.elements,
618
+ this.options.panStep,
619
+ this.options.zoomFactor,
620
+ this.options.onPan,
621
+ this.options.onZoom
622
+ ));
623
+ }
624
+ this.cleanups.push(attachGesture(this.state, this.elements, this.options.onZoom));
625
+ this.resizeHandler = () => centerImage(this.state, this.elements);
626
+ window.addEventListener("resize", this.resizeHandler);
627
+ if (this.options.src) {
628
+ this.load(this.options.src, this.options.alt);
629
+ }
630
+ }
631
+ // --- Public state ---
632
+ get scale() {
633
+ return this.state.scale;
634
+ }
635
+ get translateX() {
636
+ return this.state.translateX;
637
+ }
638
+ get translateY() {
639
+ return this.state.translateY;
640
+ }
641
+ get isLoaded() {
642
+ return this.state.isLoaded;
643
+ }
644
+ // --- Public methods ---
645
+ zoomIn() {
646
+ zoomTowardsPoint(
647
+ this.state,
648
+ this.elements,
649
+ this.options.zoomFactor,
650
+ void 0,
651
+ void 0,
652
+ this.options.onZoom
653
+ );
654
+ this.emit("zoom", this.state.scale);
655
+ }
656
+ zoomOut() {
657
+ zoomTowardsPoint(
658
+ this.state,
659
+ this.elements,
660
+ 1 / this.options.zoomFactor,
661
+ void 0,
662
+ void 0,
663
+ this.options.onZoom
664
+ );
665
+ this.emit("zoom", this.state.scale);
666
+ }
667
+ /** "Enhance." */
668
+ enhance() {
669
+ this.zoomIn();
670
+ }
671
+ zoomTo(scale) {
672
+ const delta = scale / this.state.scale;
673
+ zoomTowardsPoint(this.state, this.elements, delta, void 0, void 0, this.options.onZoom);
674
+ this.emit("zoom", this.state.scale);
675
+ }
676
+ zoomToPoint(scale, x, y) {
677
+ const delta = scale / this.state.scale;
678
+ zoomTowardsPoint(this.state, this.elements, delta, x, y, this.options.onZoom);
679
+ this.emit("zoom", this.state.scale);
680
+ }
681
+ panTo(x, y) {
682
+ this.state.translateX = x;
683
+ this.state.translateY = y;
684
+ updateTransform(this.state, this.elements);
685
+ this.options.onPan?.(x, y);
686
+ this.emit("pan", x, y);
687
+ }
688
+ panBy(dx, dy) {
689
+ this.state.translateX += dx;
690
+ this.state.translateY += dy;
691
+ updateTransform(this.state, this.elements);
692
+ this.options.onPan?.(this.state.translateX, this.state.translateY);
693
+ this.emit("pan", this.state.translateX, this.state.translateY);
694
+ }
695
+ reset() {
696
+ resetView(this.state, this.elements);
697
+ this.emit("reset");
698
+ }
699
+ center() {
700
+ centerImage(this.state, this.elements);
701
+ }
702
+ load(src, alt) {
703
+ loadImage(
704
+ src,
705
+ alt ?? this.options.alt,
706
+ this.state,
707
+ this.elements,
708
+ this.options,
709
+ this.emit.bind(this)
710
+ );
711
+ }
712
+ /**
713
+ * Apply velocity directly (used by joystick plugin).
714
+ * Sets velocity and lets the rAF loop handle movement.
715
+ */
716
+ applyVelocity(vx, vy) {
717
+ this.state.translateX += vx;
718
+ this.state.translateY += vy;
719
+ updateTransform(this.state, this.elements);
720
+ }
721
+ /** Get the internal state (for plugin access) */
722
+ getState() {
723
+ return this.state;
724
+ }
725
+ /** Get the managed elements (for plugin access) */
726
+ getElements() {
727
+ return this.elements;
728
+ }
729
+ // --- Events ---
730
+ on(event, handler) {
731
+ if (!this.listeners.has(event)) {
732
+ this.listeners.set(event, /* @__PURE__ */ new Set());
733
+ }
734
+ this.listeners.get(event).add(handler);
735
+ }
736
+ off(event, handler) {
737
+ this.listeners.get(event)?.delete(handler);
738
+ }
739
+ emit(event, ...args) {
740
+ this.listeners.get(event)?.forEach((fn) => fn(...args));
741
+ }
742
+ // --- Lifecycle ---
743
+ destroy() {
744
+ this.cleanups.forEach((fn) => fn());
745
+ this.cleanups = [];
746
+ if (this.resizeHandler) {
747
+ window.removeEventListener("resize", this.resizeHandler);
748
+ this.resizeHandler = null;
749
+ }
750
+ if (this.state.wheelTimeout) {
751
+ clearTimeout(this.state.wheelTimeout);
752
+ }
753
+ this.elements.image.remove();
754
+ if (this.elements.loadingOverlay) {
755
+ this.elements.loadingOverlay.remove();
756
+ }
757
+ this.elements.container.removeAttribute("data-zoooom");
758
+ this.elements.container.removeAttribute("role");
759
+ this.elements.container.removeAttribute("aria-label");
760
+ this.listeners.clear();
761
+ this.emit("destroy");
762
+ }
763
+ };
764
+
765
+ // src/index.ts
766
+ var src_default = Zoooom;
767
+
768
+ export { Zoooom, src_default as default };
769
+ //# sourceMappingURL=index.js.map
770
+ //# sourceMappingURL=index.js.map