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.
@@ -0,0 +1,1272 @@
1
+ var Zoooom = (function (exports) {
2
+ 'use strict';
3
+
4
+ // src/core/constants.ts
5
+ var PAN_STEP = 50;
6
+ var ZOOM_FACTOR = 1.5;
7
+ var MIN_SCALE = 0.8;
8
+ var OVERSCALE_FACTOR = 2;
9
+ var VELOCITY_DAMPING = 0.85;
10
+ var TRACKPAD_SENSITIVITY = 2e-3;
11
+ var JOYSTICK_RADIUS = 60;
12
+ var JOYSTICK_DEADZONE = 0.1;
13
+ var MAX_JOYSTICK_SPEED = 10;
14
+ var DWELL_TIMEOUT = 100;
15
+
16
+ // src/core/transform.ts
17
+ function updateTransform(state, elements) {
18
+ if (!elements.image) return;
19
+ elements.image.style.transform = `translate(calc(-50% + ${state.translateX}px), calc(-50% + ${state.translateY}px)) scale(${state.scale})`;
20
+ }
21
+ function animateMovement(state, elements, onPan) {
22
+ if (state.reducedMotion) {
23
+ state.velocityX = 0;
24
+ state.velocityY = 0;
25
+ return;
26
+ }
27
+ state.isAnimating = true;
28
+ function step() {
29
+ state.translateX += state.velocityX;
30
+ state.translateY += state.velocityY;
31
+ state.velocityX *= VELOCITY_DAMPING;
32
+ state.velocityY *= VELOCITY_DAMPING;
33
+ updateTransform(state, elements);
34
+ onPan?.(state.translateX, state.translateY);
35
+ if (Math.abs(state.velocityX) > 0.1 || Math.abs(state.velocityY) > 0.1) {
36
+ requestAnimationFrame(step);
37
+ } else {
38
+ state.velocityX = 0;
39
+ state.velocityY = 0;
40
+ state.isAnimating = false;
41
+ }
42
+ }
43
+ requestAnimationFrame(step);
44
+ }
45
+ function centerImage(state, elements) {
46
+ if (!elements.container || !elements.image) return;
47
+ state.translateX = 0;
48
+ state.translateY = 0;
49
+ updateTransform(state, elements);
50
+ }
51
+ function resetView(state, elements) {
52
+ state.scale = 1;
53
+ state.translateX = 0;
54
+ state.translateY = 0;
55
+ state.velocityX = 0;
56
+ state.velocityY = 0;
57
+ updateTransform(state, elements);
58
+ }
59
+ function calculateMaxScale(elements, overscaleFactor = OVERSCALE_FACTOR) {
60
+ if (!elements.image) return 10;
61
+ const naturalWidth = elements.image.naturalWidth;
62
+ const naturalHeight = elements.image.naturalHeight;
63
+ const displayWidth = elements.image.clientWidth;
64
+ const displayHeight = elements.image.clientHeight;
65
+ if (!naturalWidth || !naturalHeight || !displayWidth || !displayHeight) return 10;
66
+ const widthRatio = naturalWidth / displayWidth;
67
+ const heightRatio = naturalHeight / displayHeight;
68
+ const maxScaleFactor = Math.max(widthRatio, heightRatio);
69
+ const mobileOverscale = window.innerWidth <= 768 ? overscaleFactor * 2 : overscaleFactor;
70
+ return Math.max(maxScaleFactor * mobileOverscale, MIN_SCALE);
71
+ }
72
+ function zoomTowardsPoint(state, elements, delta, pointX, pointY, onZoom) {
73
+ if (!elements.container || !elements.image) return;
74
+ const currentScale = state.scale;
75
+ const newScale = Math.max(MIN_SCALE, Math.min(state.scale * delta, state.maxScale));
76
+ if (newScale === currentScale) return;
77
+ const containerRect = elements.container.getBoundingClientRect();
78
+ const containerCenterX = containerRect.width / 2;
79
+ const containerCenterY = containerRect.height / 2;
80
+ const targetX = pointX ?? containerCenterX;
81
+ const targetY = pointY ?? containerCenterY;
82
+ const imageX = (targetX - containerCenterX - state.translateX) / currentScale;
83
+ const imageY = (targetY - containerCenterY - state.translateY) / currentScale;
84
+ state.translateX = targetX - containerCenterX - imageX * newScale;
85
+ state.translateY = targetY - containerCenterY - imageY * newScale;
86
+ if (!state.reducedMotion) {
87
+ elements.image.style.transition = "transform 0.2s ease-out";
88
+ }
89
+ state.scale = newScale;
90
+ updateTransform(state, elements);
91
+ onZoom?.(state.scale);
92
+ if (!state.reducedMotion) {
93
+ setTimeout(() => {
94
+ if (elements.image) {
95
+ elements.image.style.transition = "";
96
+ }
97
+ }, 200);
98
+ }
99
+ }
100
+
101
+ // src/core/loader.ts
102
+ function createLoadingOverlay(container, loading) {
103
+ if (loading === false) return null;
104
+ const overlay = document.createElement("div");
105
+ overlay.className = "zoooom-loading";
106
+ overlay.setAttribute("role", "status");
107
+ overlay.setAttribute("aria-label", "Loading image");
108
+ if (typeof loading === "string") {
109
+ overlay.innerHTML = loading;
110
+ } else {
111
+ overlay.innerHTML = `
112
+ <div class="zoooom-spinner"></div>
113
+ <div class="zoooom-loading-text">Loading...</div>
114
+ `;
115
+ }
116
+ container.appendChild(overlay);
117
+ return overlay;
118
+ }
119
+ function removeLoadingOverlay(elements) {
120
+ if (elements.loadingOverlay) {
121
+ elements.loadingOverlay.remove();
122
+ elements.loadingOverlay = null;
123
+ }
124
+ }
125
+ function loadImage(src, alt, state, elements, options, emit) {
126
+ state.scale = 1;
127
+ state.translateX = 0;
128
+ state.translateY = 0;
129
+ state.velocityX = 0;
130
+ state.velocityY = 0;
131
+ state.isLoaded = false;
132
+ removeLoadingOverlay(elements);
133
+ elements.loadingOverlay = createLoadingOverlay(elements.container, options.loading);
134
+ elements.image.style.opacity = "0";
135
+ elements.image.style.visibility = "hidden";
136
+ elements.image.setAttribute("aria-hidden", "true");
137
+ elements.image.setAttribute("alt", "");
138
+ const preloader = new Image();
139
+ preloader.onload = () => {
140
+ elements.image.src = preloader.src;
141
+ elements.image.onload = () => {
142
+ removeLoadingOverlay(elements);
143
+ elements.image.setAttribute("alt", alt);
144
+ elements.image.removeAttribute("aria-hidden");
145
+ if (state.reducedMotion) {
146
+ elements.image.style.opacity = "1";
147
+ elements.image.style.visibility = "visible";
148
+ } else {
149
+ setTimeout(() => {
150
+ elements.image.style.transition = "opacity 0.3s ease-in-out";
151
+ elements.image.style.opacity = "1";
152
+ elements.image.style.visibility = "visible";
153
+ setTimeout(() => {
154
+ elements.image.style.transition = "";
155
+ }, 300);
156
+ }, 50);
157
+ }
158
+ if (options.maxScale === "auto") {
159
+ state.maxScale = calculateMaxScale(elements, options.overscaleFactor);
160
+ }
161
+ centerImage(state, elements);
162
+ state.isLoaded = true;
163
+ emit("load");
164
+ options.onLoad?.();
165
+ };
166
+ elements.image.onerror = () => {
167
+ removeLoadingOverlay(elements);
168
+ const error = new Error(`Failed to load image: ${src}`);
169
+ emit("error", error);
170
+ options.onError?.(error);
171
+ };
172
+ };
173
+ preloader.onerror = () => {
174
+ removeLoadingOverlay(elements);
175
+ const error = new Error(`Failed to load image: ${src}`);
176
+ emit("error", error);
177
+ options.onError?.(error);
178
+ };
179
+ preloader.src = src;
180
+ }
181
+
182
+ // src/input/mouse.ts
183
+ function attachMouse(state, elements, onPan) {
184
+ const { container } = elements;
185
+ function handleMouseDown(e) {
186
+ if (e.button !== 0) return;
187
+ state.isDragging = true;
188
+ state.startX = e.clientX;
189
+ state.startY = e.clientY;
190
+ container.style.cursor = "grabbing";
191
+ e.preventDefault();
192
+ }
193
+ function handleMouseMove(e) {
194
+ if (!state.isDragging) return;
195
+ const dx = e.clientX - state.startX;
196
+ const dy = e.clientY - state.startY;
197
+ state.translateX += dx;
198
+ state.translateY += dy;
199
+ state.startX = e.clientX;
200
+ state.startY = e.clientY;
201
+ updateTransform(state, elements);
202
+ onPan?.(state.translateX, state.translateY);
203
+ }
204
+ function handleMouseUp() {
205
+ if (state.isDragging) {
206
+ state.isDragging = false;
207
+ container.style.cursor = "grab";
208
+ }
209
+ }
210
+ container.addEventListener("mousedown", handleMouseDown);
211
+ container.addEventListener("mousemove", handleMouseMove);
212
+ container.addEventListener("mouseup", handleMouseUp);
213
+ container.addEventListener("mouseleave", handleMouseUp);
214
+ return () => {
215
+ container.removeEventListener("mousedown", handleMouseDown);
216
+ container.removeEventListener("mousemove", handleMouseMove);
217
+ container.removeEventListener("mouseup", handleMouseUp);
218
+ container.removeEventListener("mouseleave", handleMouseUp);
219
+ };
220
+ }
221
+
222
+ // src/input/touch.ts
223
+ function getDistance(e) {
224
+ const t1 = e.touches[0];
225
+ const t2 = e.touches[1];
226
+ const dx = t1.clientX - t2.clientX;
227
+ const dy = t1.clientY - t2.clientY;
228
+ return Math.sqrt(dx * dx + dy * dy);
229
+ }
230
+ function attachTouch(state, elements, overscaleFactor, onPan, onZoom) {
231
+ const { container } = elements;
232
+ function handleTouchStart(e) {
233
+ if (e.touches.length === 1) {
234
+ state.isDragging = true;
235
+ state.startX = e.touches[0].clientX;
236
+ state.startY = e.touches[0].clientY;
237
+ state.initialDistance = 0;
238
+ } else if (e.touches.length === 2) {
239
+ state.isDragging = false;
240
+ state.initialDistance = getDistance(e);
241
+ state.initialScale = state.scale;
242
+ state.initialTranslateX = state.translateX;
243
+ state.initialTranslateY = state.translateY;
244
+ const t1 = e.touches[0];
245
+ const t2 = e.touches[1];
246
+ const rect = container.getBoundingClientRect();
247
+ state.pinchCenter = {
248
+ x: (t1.clientX + t2.clientX) / 2 - rect.left,
249
+ y: (t1.clientY + t2.clientY) / 2 - rect.top
250
+ };
251
+ }
252
+ }
253
+ function handleTouchMove(e) {
254
+ e.preventDefault();
255
+ if (e.touches.length === 1 && state.isDragging) {
256
+ const dx = e.touches[0].clientX - state.startX;
257
+ const dy = e.touches[0].clientY - state.startY;
258
+ state.translateX += dx;
259
+ state.translateY += dy;
260
+ state.startX = e.touches[0].clientX;
261
+ state.startY = e.touches[0].clientY;
262
+ updateTransform(state, elements);
263
+ onPan?.(state.translateX, state.translateY);
264
+ } else if (e.touches.length === 2 && state.initialDistance > 0) {
265
+ const currentDistance = getDistance(e);
266
+ const scaleFactor = currentDistance / state.initialDistance;
267
+ const targetScale = state.initialScale * scaleFactor;
268
+ state.maxScale = calculateMaxScale(elements, overscaleFactor);
269
+ const newScale = Math.max(MIN_SCALE, Math.min(targetScale, state.maxScale));
270
+ if (newScale === state.scale) return;
271
+ const t1 = e.touches[0];
272
+ const t2 = e.touches[1];
273
+ const rect = container.getBoundingClientRect();
274
+ const currentPinchCenter = {
275
+ x: (t1.clientX + t2.clientX) / 2 - rect.left,
276
+ y: (t1.clientY + t2.clientY) / 2 - rect.top
277
+ };
278
+ const containerCenterX = rect.width / 2;
279
+ const containerCenterY = rect.height / 2;
280
+ const imageX = (state.pinchCenter.x - containerCenterX - state.initialTranslateX) / state.initialScale;
281
+ const imageY = (state.pinchCenter.y - containerCenterY - state.initialTranslateY) / state.initialScale;
282
+ state.translateX = currentPinchCenter.x - containerCenterX - imageX * newScale;
283
+ state.translateY = currentPinchCenter.y - containerCenterY - imageY * newScale;
284
+ state.scale = newScale;
285
+ updateTransform(state, elements);
286
+ onZoom?.(state.scale);
287
+ state.pinchCenter = currentPinchCenter;
288
+ }
289
+ }
290
+ function handleTouchEnd() {
291
+ state.isDragging = false;
292
+ state.initialDistance = 0;
293
+ }
294
+ container.addEventListener("touchstart", handleTouchStart, { passive: false });
295
+ container.addEventListener("touchmove", handleTouchMove, { passive: false });
296
+ container.addEventListener("touchend", handleTouchEnd);
297
+ container.addEventListener("touchcancel", handleTouchEnd);
298
+ return () => {
299
+ container.removeEventListener("touchstart", handleTouchStart);
300
+ container.removeEventListener("touchmove", handleTouchMove);
301
+ container.removeEventListener("touchend", handleTouchEnd);
302
+ container.removeEventListener("touchcancel", handleTouchEnd);
303
+ };
304
+ }
305
+
306
+ // src/input/wheel.ts
307
+ function attachWheel(state, elements, zoomFactor = ZOOM_FACTOR, trackpadSensitivity = TRACKPAD_SENSITIVITY, onZoom) {
308
+ const { container } = elements;
309
+ function handleWheel(e) {
310
+ e.preventDefault();
311
+ const rect = container.getBoundingClientRect();
312
+ const pointX = e.clientX - rect.left;
313
+ const pointY = e.clientY - rect.top;
314
+ let zoomDelta;
315
+ if (e.ctrlKey) {
316
+ zoomDelta = e.deltaY < 0 ? zoomFactor : 1 / zoomFactor;
317
+ } else {
318
+ let normalizedDelta;
319
+ switch (e.deltaMode) {
320
+ case 1:
321
+ normalizedDelta = e.deltaY * 20;
322
+ break;
323
+ // LINE
324
+ case 2:
325
+ normalizedDelta = e.deltaY * 100;
326
+ break;
327
+ // PAGE
328
+ default:
329
+ normalizedDelta = e.deltaY;
330
+ }
331
+ if (Math.abs(normalizedDelta) < 40) {
332
+ zoomDelta = Math.exp(-normalizedDelta * trackpadSensitivity);
333
+ } else {
334
+ zoomDelta = normalizedDelta > 0 ? 1 / zoomFactor : zoomFactor;
335
+ }
336
+ }
337
+ container.classList.add("zoooom-scrolling");
338
+ if (state.wheelTimeout) clearTimeout(state.wheelTimeout);
339
+ state.wheelTimeout = setTimeout(() => {
340
+ container.classList.remove("zoooom-scrolling");
341
+ }, 200);
342
+ zoomTowardsPoint(state, elements, zoomDelta, pointX, pointY, onZoom);
343
+ }
344
+ container.addEventListener("wheel", handleWheel, { passive: false });
345
+ return () => {
346
+ container.removeEventListener("wheel", handleWheel);
347
+ if (state.wheelTimeout) clearTimeout(state.wheelTimeout);
348
+ };
349
+ }
350
+
351
+ // src/input/keyboard.ts
352
+ function attachKeyboard(state, elements, panStep = PAN_STEP, zoomFactor = ZOOM_FACTOR, onPan, onZoom) {
353
+ const { container } = elements;
354
+ function handleKeyDown(e) {
355
+ let handled = true;
356
+ switch (e.key) {
357
+ case "ArrowLeft":
358
+ state.velocityX = panStep * 0.2;
359
+ animateMovement(state, elements, onPan);
360
+ break;
361
+ case "ArrowRight":
362
+ state.velocityX = -panStep * 0.2;
363
+ animateMovement(state, elements, onPan);
364
+ break;
365
+ case "ArrowUp":
366
+ state.velocityY = panStep * 0.2;
367
+ animateMovement(state, elements, onPan);
368
+ break;
369
+ case "ArrowDown":
370
+ state.velocityY = -panStep * 0.2;
371
+ animateMovement(state, elements, onPan);
372
+ break;
373
+ case "+":
374
+ case "=":
375
+ zoomTowardsPoint(state, elements, zoomFactor, void 0, void 0, onZoom);
376
+ break;
377
+ case "-":
378
+ zoomTowardsPoint(state, elements, 1 / zoomFactor, void 0, void 0, onZoom);
379
+ break;
380
+ case "r":
381
+ case "R":
382
+ resetView(state, elements);
383
+ break;
384
+ default:
385
+ handled = false;
386
+ }
387
+ if (handled) e.preventDefault();
388
+ }
389
+ container.addEventListener("keydown", handleKeyDown);
390
+ return () => {
391
+ container.removeEventListener("keydown", handleKeyDown);
392
+ };
393
+ }
394
+
395
+ // src/input/gesture.ts
396
+ function attachGesture(state, elements, onZoom) {
397
+ const { container } = elements;
398
+ if (!("ongesturestart" in window)) {
399
+ return () => {
400
+ };
401
+ }
402
+ function handleGestureStart(e) {
403
+ e.preventDefault();
404
+ state.scale;
405
+ }
406
+ function handleGestureChange(e) {
407
+ e.preventDefault();
408
+ const ge = e;
409
+ const rect = container.getBoundingClientRect();
410
+ const delta = ge.scale > 1 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
411
+ zoomTowardsPoint(state, elements, delta, rect.width / 2, rect.height / 2, onZoom);
412
+ }
413
+ function handleGestureEnd(e) {
414
+ e.preventDefault();
415
+ }
416
+ container.addEventListener("gesturestart", handleGestureStart, { passive: false });
417
+ container.addEventListener("gesturechange", handleGestureChange, { passive: false });
418
+ container.addEventListener("gestureend", handleGestureEnd, { passive: false });
419
+ return () => {
420
+ container.removeEventListener("gesturestart", handleGestureStart);
421
+ container.removeEventListener("gesturechange", handleGestureChange);
422
+ container.removeEventListener("gestureend", handleGestureEnd);
423
+ };
424
+ }
425
+
426
+ // src/styles/core.ts
427
+ var ZOOOOM_CSS = `
428
+ [data-zoooom] {
429
+ --zoooom-bg: #000;
430
+ --zoooom-spinner-color: #2196f3;
431
+ --zoooom-spinner-track: rgba(255, 255, 255, 0.3);
432
+ --zoooom-spinner-size: 40px;
433
+ --zoooom-loading-bg: rgba(0, 0, 0, 0.85);
434
+ --zoooom-loading-radius: 10px;
435
+ --zoooom-cursor: grab;
436
+ --zoooom-cursor-active: grabbing;
437
+ --zoooom-transition-speed: 0.2s;
438
+ --zoooom-fade-speed: 0.3s;
439
+
440
+ position: relative;
441
+ width: 100%;
442
+ height: 100%;
443
+ background: var(--zoooom-bg);
444
+ overflow: hidden;
445
+ touch-action: none;
446
+ cursor: var(--zoooom-cursor);
447
+ user-select: none;
448
+ -webkit-user-select: none;
449
+ }
450
+
451
+ [data-zoooom]:focus-visible {
452
+ outline: 2px solid var(--zoooom-spinner-color);
453
+ outline-offset: -2px;
454
+ }
455
+
456
+ [data-zoooom] .zoooom-image {
457
+ position: absolute;
458
+ top: 50%;
459
+ left: 50%;
460
+ transform: translate(-50%, -50%) scale(1);
461
+ max-width: 100%;
462
+ max-height: 100%;
463
+ object-fit: contain;
464
+ user-select: none;
465
+ -webkit-user-select: none;
466
+ pointer-events: none;
467
+ }
468
+
469
+ [data-zoooom] .zoooom-loading {
470
+ position: absolute;
471
+ top: 50%;
472
+ left: 50%;
473
+ transform: translate(-50%, -50%);
474
+ display: flex;
475
+ flex-direction: column;
476
+ align-items: center;
477
+ justify-content: center;
478
+ background: var(--zoooom-loading-bg);
479
+ padding: 20px;
480
+ border-radius: var(--zoooom-loading-radius);
481
+ z-index: 10;
482
+ min-width: 120px;
483
+ }
484
+
485
+ [data-zoooom] .zoooom-spinner {
486
+ width: var(--zoooom-spinner-size);
487
+ height: var(--zoooom-spinner-size);
488
+ border: 4px solid var(--zoooom-spinner-track);
489
+ border-radius: 50%;
490
+ border-top-color: var(--zoooom-spinner-color);
491
+ animation: zoooom-spin 1s linear infinite;
492
+ }
493
+
494
+ [data-zoooom] .zoooom-loading-text {
495
+ margin-top: 12px;
496
+ font-size: 14px;
497
+ color: #fff;
498
+ font-family: system-ui, -apple-system, sans-serif;
499
+ }
500
+
501
+ @keyframes zoooom-spin {
502
+ to { transform: rotate(360deg); }
503
+ }
504
+
505
+ @media (prefers-reduced-motion: reduce) {
506
+ [data-zoooom] .zoooom-spinner {
507
+ animation: none;
508
+ border-top-color: var(--zoooom-spinner-track);
509
+ border-right-color: var(--zoooom-spinner-color);
510
+ }
511
+
512
+ [data-zoooom] .zoooom-image {
513
+ transition: none !important;
514
+ }
515
+ }
516
+ `;
517
+ var injected = false;
518
+ function injectCoreStyles() {
519
+ if (injected || typeof document === "undefined") return;
520
+ const style = document.createElement("style");
521
+ style.setAttribute("data-zoooom-core", "");
522
+ style.textContent = ZOOOOM_CSS;
523
+ document.head.appendChild(style);
524
+ injected = true;
525
+ }
526
+
527
+ // src/core/Zoooom.ts
528
+ var DEFAULTS = {
529
+ src: "",
530
+ alt: "Image",
531
+ minScale: MIN_SCALE,
532
+ maxScale: "auto",
533
+ overscaleFactor: OVERSCALE_FACTOR,
534
+ zoomFactor: ZOOM_FACTOR,
535
+ panStep: PAN_STEP,
536
+ velocityDamping: VELOCITY_DAMPING,
537
+ trackpadSensitivity: TRACKPAD_SENSITIVITY,
538
+ mouse: true,
539
+ touch: true,
540
+ wheel: true,
541
+ keyboard: true,
542
+ loading: true,
543
+ injectStyles: true,
544
+ respectReducedMotion: true
545
+ };
546
+ exports.default = class Zoooom {
547
+ constructor(container, options) {
548
+ this.cleanups = [];
549
+ this.listeners = /* @__PURE__ */ new Map();
550
+ this.resizeHandler = null;
551
+ this.options = { ...DEFAULTS, ...options };
552
+ if (this.options.injectStyles) {
553
+ injectCoreStyles();
554
+ }
555
+ const el = typeof container === "string" ? document.querySelector(container) : container;
556
+ if (!el) {
557
+ throw new Error(`Zoooom: container "${container}" not found`);
558
+ }
559
+ if (el.clientWidth === 0 || el.clientHeight === 0) {
560
+ console.warn("Zoooom: container has zero dimensions. The viewer needs explicit width/height.");
561
+ }
562
+ el.setAttribute("data-zoooom", "");
563
+ if (!el.hasAttribute("tabindex")) {
564
+ el.setAttribute("tabindex", "0");
565
+ }
566
+ el.setAttribute("role", "application");
567
+ el.setAttribute("aria-label", "Image viewer \u2014 use arrow keys to pan, +/- to zoom");
568
+ const img = document.createElement("img");
569
+ img.className = "zoooom-image";
570
+ img.setAttribute("draggable", "false");
571
+ el.appendChild(img);
572
+ this.elements = { container: el, image: img, loadingOverlay: null };
573
+ const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
574
+ const reducedMotion = this.options.respectReducedMotion && motionQuery.matches;
575
+ this.state = {
576
+ scale: 1,
577
+ translateX: 0,
578
+ translateY: 0,
579
+ velocityX: 0,
580
+ velocityY: 0,
581
+ maxScale: typeof this.options.maxScale === "number" ? this.options.maxScale : 10,
582
+ isDragging: false,
583
+ isAnimating: false,
584
+ isLoaded: false,
585
+ startX: 0,
586
+ startY: 0,
587
+ initialDistance: 0,
588
+ initialScale: 1,
589
+ initialTranslateX: 0,
590
+ initialTranslateY: 0,
591
+ pinchCenter: { x: 0, y: 0 },
592
+ wheelTimeout: null,
593
+ reducedMotion
594
+ };
595
+ if (this.options.respectReducedMotion) {
596
+ motionQuery.addEventListener("change", (e) => {
597
+ this.state.reducedMotion = e.matches;
598
+ });
599
+ }
600
+ if (this.options.mouse) {
601
+ this.cleanups.push(attachMouse(this.state, this.elements, this.options.onPan));
602
+ }
603
+ if (this.options.touch) {
604
+ this.cleanups.push(attachTouch(
605
+ this.state,
606
+ this.elements,
607
+ this.options.overscaleFactor,
608
+ this.options.onPan,
609
+ this.options.onZoom
610
+ ));
611
+ }
612
+ if (this.options.wheel) {
613
+ this.cleanups.push(attachWheel(
614
+ this.state,
615
+ this.elements,
616
+ this.options.zoomFactor,
617
+ this.options.trackpadSensitivity,
618
+ this.options.onZoom
619
+ ));
620
+ }
621
+ if (this.options.keyboard) {
622
+ this.cleanups.push(attachKeyboard(
623
+ this.state,
624
+ this.elements,
625
+ this.options.panStep,
626
+ this.options.zoomFactor,
627
+ this.options.onPan,
628
+ this.options.onZoom
629
+ ));
630
+ }
631
+ this.cleanups.push(attachGesture(this.state, this.elements, this.options.onZoom));
632
+ this.resizeHandler = () => centerImage(this.state, this.elements);
633
+ window.addEventListener("resize", this.resizeHandler);
634
+ if (this.options.src) {
635
+ this.load(this.options.src, this.options.alt);
636
+ }
637
+ }
638
+ // --- Public state ---
639
+ get scale() {
640
+ return this.state.scale;
641
+ }
642
+ get translateX() {
643
+ return this.state.translateX;
644
+ }
645
+ get translateY() {
646
+ return this.state.translateY;
647
+ }
648
+ get isLoaded() {
649
+ return this.state.isLoaded;
650
+ }
651
+ // --- Public methods ---
652
+ zoomIn() {
653
+ zoomTowardsPoint(
654
+ this.state,
655
+ this.elements,
656
+ this.options.zoomFactor,
657
+ void 0,
658
+ void 0,
659
+ this.options.onZoom
660
+ );
661
+ this.emit("zoom", this.state.scale);
662
+ }
663
+ zoomOut() {
664
+ zoomTowardsPoint(
665
+ this.state,
666
+ this.elements,
667
+ 1 / this.options.zoomFactor,
668
+ void 0,
669
+ void 0,
670
+ this.options.onZoom
671
+ );
672
+ this.emit("zoom", this.state.scale);
673
+ }
674
+ /** "Enhance." */
675
+ enhance() {
676
+ this.zoomIn();
677
+ }
678
+ zoomTo(scale) {
679
+ const delta = scale / this.state.scale;
680
+ zoomTowardsPoint(this.state, this.elements, delta, void 0, void 0, this.options.onZoom);
681
+ this.emit("zoom", this.state.scale);
682
+ }
683
+ zoomToPoint(scale, x, y) {
684
+ const delta = scale / this.state.scale;
685
+ zoomTowardsPoint(this.state, this.elements, delta, x, y, this.options.onZoom);
686
+ this.emit("zoom", this.state.scale);
687
+ }
688
+ panTo(x, y) {
689
+ this.state.translateX = x;
690
+ this.state.translateY = y;
691
+ updateTransform(this.state, this.elements);
692
+ this.options.onPan?.(x, y);
693
+ this.emit("pan", x, y);
694
+ }
695
+ panBy(dx, dy) {
696
+ this.state.translateX += dx;
697
+ this.state.translateY += dy;
698
+ updateTransform(this.state, this.elements);
699
+ this.options.onPan?.(this.state.translateX, this.state.translateY);
700
+ this.emit("pan", this.state.translateX, this.state.translateY);
701
+ }
702
+ reset() {
703
+ resetView(this.state, this.elements);
704
+ this.emit("reset");
705
+ }
706
+ center() {
707
+ centerImage(this.state, this.elements);
708
+ }
709
+ load(src, alt) {
710
+ loadImage(
711
+ src,
712
+ alt ?? this.options.alt,
713
+ this.state,
714
+ this.elements,
715
+ this.options,
716
+ this.emit.bind(this)
717
+ );
718
+ }
719
+ /**
720
+ * Apply velocity directly (used by joystick plugin).
721
+ * Sets velocity and lets the rAF loop handle movement.
722
+ */
723
+ applyVelocity(vx, vy) {
724
+ this.state.translateX += vx;
725
+ this.state.translateY += vy;
726
+ updateTransform(this.state, this.elements);
727
+ }
728
+ /** Get the internal state (for plugin access) */
729
+ getState() {
730
+ return this.state;
731
+ }
732
+ /** Get the managed elements (for plugin access) */
733
+ getElements() {
734
+ return this.elements;
735
+ }
736
+ // --- Events ---
737
+ on(event, handler) {
738
+ if (!this.listeners.has(event)) {
739
+ this.listeners.set(event, /* @__PURE__ */ new Set());
740
+ }
741
+ this.listeners.get(event).add(handler);
742
+ }
743
+ off(event, handler) {
744
+ this.listeners.get(event)?.delete(handler);
745
+ }
746
+ emit(event, ...args) {
747
+ this.listeners.get(event)?.forEach((fn) => fn(...args));
748
+ }
749
+ // --- Lifecycle ---
750
+ destroy() {
751
+ this.cleanups.forEach((fn) => fn());
752
+ this.cleanups = [];
753
+ if (this.resizeHandler) {
754
+ window.removeEventListener("resize", this.resizeHandler);
755
+ this.resizeHandler = null;
756
+ }
757
+ if (this.state.wheelTimeout) {
758
+ clearTimeout(this.state.wheelTimeout);
759
+ }
760
+ this.elements.image.remove();
761
+ if (this.elements.loadingOverlay) {
762
+ this.elements.loadingOverlay.remove();
763
+ }
764
+ this.elements.container.removeAttribute("data-zoooom");
765
+ this.elements.container.removeAttribute("role");
766
+ this.elements.container.removeAttribute("aria-label");
767
+ this.listeners.clear();
768
+ this.emit("destroy");
769
+ }
770
+ };
771
+
772
+ // src/joystick/dom.ts
773
+ function createJoystickDOM(container) {
774
+ const toggle = document.createElement("button");
775
+ toggle.className = "zoooom-joystick-toggle";
776
+ toggle.setAttribute("aria-label", "Toggle navigation joystick");
777
+ toggle.setAttribute("aria-expanded", "false");
778
+ toggle.textContent = "\u2316";
779
+ container.appendChild(toggle);
780
+ const wrap = document.createElement("div");
781
+ wrap.className = "zoooom-joystick-wrap";
782
+ wrap.setAttribute("aria-hidden", "true");
783
+ const disc = document.createElement("div");
784
+ disc.className = "zoooom-disc";
785
+ disc.setAttribute("role", "slider");
786
+ disc.setAttribute("aria-label", "Pan navigation control");
787
+ disc.setAttribute("aria-valuenow", "0");
788
+ disc.setAttribute("aria-valuemin", "0");
789
+ disc.setAttribute("aria-valuemax", "1");
790
+ disc.setAttribute("aria-valuetext", "Center position \u2014 not moving");
791
+ disc.setAttribute("tabindex", "0");
792
+ const innerCircle = document.createElement("div");
793
+ innerCircle.className = "zoooom-inner-circle";
794
+ const zoomOut = document.createElement("div");
795
+ zoomOut.className = "zoooom-zoom-half zoooom-zoom-out";
796
+ zoomOut.setAttribute("role", "button");
797
+ zoomOut.setAttribute("aria-label", "Zoom out");
798
+ zoomOut.setAttribute("tabindex", "0");
799
+ zoomOut.textContent = "\u2212";
800
+ const zoomIn = document.createElement("div");
801
+ zoomIn.className = "zoooom-zoom-half zoooom-zoom-in";
802
+ zoomIn.setAttribute("role", "button");
803
+ zoomIn.setAttribute("aria-label", "Zoom in");
804
+ zoomIn.setAttribute("tabindex", "0");
805
+ zoomIn.textContent = "+";
806
+ innerCircle.appendChild(zoomOut);
807
+ innerCircle.appendChild(zoomIn);
808
+ const arrows = document.createElement("div");
809
+ arrows.className = "zoooom-arrows";
810
+ arrows.setAttribute("aria-hidden", "true");
811
+ for (const dir of ["n", "e", "s", "w"]) {
812
+ const arrow = document.createElement("div");
813
+ arrow.className = `zoooom-arrow zoooom-arrow-${dir}`;
814
+ arrows.appendChild(arrow);
815
+ }
816
+ disc.appendChild(innerCircle);
817
+ disc.appendChild(arrows);
818
+ wrap.appendChild(disc);
819
+ container.appendChild(wrap);
820
+ return { wrap, toggle, disc, innerCircle, zoomIn, zoomOut };
821
+ }
822
+ function destroyJoystickDOM(dom) {
823
+ dom.wrap.remove();
824
+ dom.toggle.remove();
825
+ }
826
+
827
+ // src/styles/joystick.ts
828
+ var JOYSTICK_CSS = `
829
+ .zoooom-joystick-wrap {
830
+ position: fixed;
831
+ bottom: 20px;
832
+ left: 50%;
833
+ transform: translateX(-50%);
834
+ z-index: 100;
835
+ touch-action: none;
836
+ opacity: 0;
837
+ pointer-events: none;
838
+ transition: opacity 0.3s ease-out, transform 0.3s ease-out;
839
+ }
840
+
841
+ .zoooom-joystick-wrap.visible {
842
+ opacity: 1;
843
+ pointer-events: auto;
844
+ }
845
+
846
+ .zoooom-joystick-toggle {
847
+ position: fixed;
848
+ bottom: 20px;
849
+ left: 50%;
850
+ transform: translateX(-50%);
851
+ width: 56px;
852
+ height: 56px;
853
+ border-radius: 50%;
854
+ background: rgba(0, 0, 0, 0.5);
855
+ border: 2px solid rgba(255, 255, 255, 0.6);
856
+ cursor: pointer;
857
+ z-index: 99;
858
+ display: flex;
859
+ align-items: center;
860
+ justify-content: center;
861
+ color: #fff;
862
+ font-size: 20px;
863
+ font-weight: bold;
864
+ backdrop-filter: blur(4px);
865
+ transition: background 0.2s, box-shadow 0.2s;
866
+ }
867
+
868
+ .zoooom-joystick-toggle:hover {
869
+ background: rgba(0, 0, 0, 0.7);
870
+ box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
871
+ }
872
+
873
+ .zoooom-joystick-toggle:focus-visible {
874
+ outline: 2px solid #2196f3;
875
+ outline-offset: 2px;
876
+ }
877
+
878
+ .zoooom-disc {
879
+ width: 200px;
880
+ height: 200px;
881
+ border-radius: 50%;
882
+ background: rgba(0, 0, 0, 0.4);
883
+ border: 2px solid rgba(255, 255, 255, 0.6);
884
+ position: relative;
885
+ display: flex;
886
+ align-items: center;
887
+ justify-content: center;
888
+ touch-action: none;
889
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
890
+ }
891
+
892
+ .zoooom-disc.active {
893
+ border-color: rgba(255, 255, 255, 0.9);
894
+ }
895
+
896
+ .zoooom-inner-circle {
897
+ width: 72px;
898
+ height: 72px;
899
+ border-radius: 50%;
900
+ background: rgba(255, 255, 255, 0.15);
901
+ border: 1px solid rgba(255, 255, 255, 0.5);
902
+ display: flex;
903
+ position: relative;
904
+ z-index: 3;
905
+ }
906
+
907
+ .zoooom-zoom-half {
908
+ width: 50%;
909
+ height: 100%;
910
+ display: flex;
911
+ align-items: center;
912
+ justify-content: center;
913
+ cursor: pointer;
914
+ font-weight: bold;
915
+ font-size: 22px;
916
+ color: #fff;
917
+ user-select: none;
918
+ transition: background 0.15s;
919
+ }
920
+
921
+ .zoooom-zoom-half:hover {
922
+ background: rgba(255, 255, 255, 0.25);
923
+ }
924
+
925
+ .zoooom-zoom-out {
926
+ border-radius: 36px 0 0 36px;
927
+ border-right: 1px solid rgba(255, 255, 255, 0.4);
928
+ }
929
+
930
+ .zoooom-zoom-in {
931
+ border-radius: 0 36px 36px 0;
932
+ }
933
+
934
+ .zoooom-arrows {
935
+ position: absolute;
936
+ width: 100%;
937
+ height: 100%;
938
+ border-radius: 50%;
939
+ pointer-events: none;
940
+ overflow: hidden;
941
+ }
942
+
943
+ .zoooom-arrow {
944
+ position: absolute;
945
+ width: 0;
946
+ height: 0;
947
+ opacity: 0;
948
+ transition: opacity 0.2s;
949
+ }
950
+
951
+ .zoooom-arrow-n {
952
+ top: 14px;
953
+ left: 50%;
954
+ transform: translateX(-50%);
955
+ border-left: 10px solid transparent;
956
+ border-right: 10px solid transparent;
957
+ border-bottom: 14px solid rgba(255, 255, 255, 0.7);
958
+ }
959
+
960
+ .zoooom-arrow-e {
961
+ top: 50%;
962
+ right: 14px;
963
+ transform: translateY(-50%);
964
+ border-top: 10px solid transparent;
965
+ border-bottom: 10px solid transparent;
966
+ border-left: 14px solid rgba(255, 255, 255, 0.7);
967
+ }
968
+
969
+ .zoooom-arrow-s {
970
+ bottom: 14px;
971
+ left: 50%;
972
+ transform: translateX(-50%);
973
+ border-left: 10px solid transparent;
974
+ border-right: 10px solid transparent;
975
+ border-top: 14px solid rgba(255, 255, 255, 0.7);
976
+ }
977
+
978
+ .zoooom-arrow-w {
979
+ top: 50%;
980
+ left: 14px;
981
+ transform: translateY(-50%);
982
+ border-top: 10px solid transparent;
983
+ border-bottom: 10px solid transparent;
984
+ border-right: 14px solid rgba(255, 255, 255, 0.7);
985
+ }
986
+
987
+ .zoooom-disc.north .zoooom-arrow-n,
988
+ .zoooom-disc.south .zoooom-arrow-s,
989
+ .zoooom-disc.east .zoooom-arrow-e,
990
+ .zoooom-disc.west .zoooom-arrow-w,
991
+ .zoooom-disc.north-east .zoooom-arrow-n,
992
+ .zoooom-disc.north-east .zoooom-arrow-e,
993
+ .zoooom-disc.south-east .zoooom-arrow-s,
994
+ .zoooom-disc.south-east .zoooom-arrow-e,
995
+ .zoooom-disc.south-west .zoooom-arrow-s,
996
+ .zoooom-disc.south-west .zoooom-arrow-w,
997
+ .zoooom-disc.north-west .zoooom-arrow-n,
998
+ .zoooom-disc.north-west .zoooom-arrow-w {
999
+ opacity: 1;
1000
+ }
1001
+
1002
+ @media (max-width: 768px) {
1003
+ .zoooom-disc {
1004
+ width: 140px;
1005
+ height: 140px;
1006
+ }
1007
+
1008
+ .zoooom-inner-circle {
1009
+ width: 56px;
1010
+ height: 56px;
1011
+ }
1012
+
1013
+ .zoooom-joystick-toggle {
1014
+ width: 48px;
1015
+ height: 48px;
1016
+ font-size: 16px;
1017
+ }
1018
+ }
1019
+
1020
+ @media (prefers-reduced-motion: reduce) {
1021
+ .zoooom-joystick-wrap {
1022
+ transition: none;
1023
+ }
1024
+
1025
+ .zoooom-arrow {
1026
+ transition: none;
1027
+ }
1028
+ }
1029
+ `;
1030
+ var injected2 = false;
1031
+ function injectJoystickStyles() {
1032
+ if (injected2 || typeof document === "undefined") return;
1033
+ const style = document.createElement("style");
1034
+ style.setAttribute("data-zoooom-joystick", "");
1035
+ style.textContent = JOYSTICK_CSS;
1036
+ document.head.appendChild(style);
1037
+ injected2 = true;
1038
+ }
1039
+
1040
+ // src/joystick/joystick.ts
1041
+ function angleToDirection(angle) {
1042
+ if (angle >= -22.5 && angle < 22.5) return "east";
1043
+ if (angle >= 22.5 && angle < 67.5) return "south-east";
1044
+ if (angle >= 67.5 && angle < 112.5) return "south";
1045
+ if (angle >= 112.5 && angle < 157.5) return "south-west";
1046
+ if (angle >= 157.5 || angle < -157.5) return "west";
1047
+ if (angle >= -157.5 && angle < -112.5) return "north-west";
1048
+ if (angle >= -112.5 && angle < -67.5) return "north";
1049
+ return "north-east";
1050
+ }
1051
+ exports.ZoooomJoystick = class ZoooomJoystick {
1052
+ constructor(viewer, options) {
1053
+ this.visible = false;
1054
+ this.active = false;
1055
+ this.animationId = null;
1056
+ this.joystickX = 0;
1057
+ this.joystickY = 0;
1058
+ this.currentDirection = "";
1059
+ this.dwellTimer = null;
1060
+ this.isDwelling = false;
1061
+ this.hideTimer = null;
1062
+ this.viewer = viewer;
1063
+ this.options = {
1064
+ radius: options?.radius ?? JOYSTICK_RADIUS,
1065
+ deadzone: options?.deadzone ?? JOYSTICK_DEADZONE,
1066
+ maxSpeed: options?.maxSpeed ?? MAX_JOYSTICK_SPEED,
1067
+ position: options?.position ?? "bottom-center",
1068
+ showToggle: options?.showToggle ?? true,
1069
+ dwellTimeout: options?.dwellTimeout ?? DWELL_TIMEOUT
1070
+ };
1071
+ injectJoystickStyles();
1072
+ const elements = this.viewer.getElements();
1073
+ this.dom = createJoystickDOM(elements.container);
1074
+ this.bindEvents();
1075
+ }
1076
+ show() {
1077
+ this.visible = true;
1078
+ this.dom.wrap.classList.add("visible");
1079
+ this.dom.wrap.setAttribute("aria-hidden", "false");
1080
+ this.dom.toggle.setAttribute("aria-expanded", "true");
1081
+ if (this.hideTimer) clearTimeout(this.hideTimer);
1082
+ }
1083
+ hide() {
1084
+ this.visible = false;
1085
+ this.dom.wrap.classList.remove("visible");
1086
+ this.dom.wrap.setAttribute("aria-hidden", "true");
1087
+ this.dom.toggle.setAttribute("aria-expanded", "false");
1088
+ this.stopMovement();
1089
+ this.clearDirection();
1090
+ }
1091
+ destroy() {
1092
+ this.hide();
1093
+ this.stopMovement();
1094
+ if (this.dwellTimer) clearTimeout(this.dwellTimer);
1095
+ if (this.hideTimer) clearTimeout(this.hideTimer);
1096
+ destroyJoystickDOM(this.dom);
1097
+ }
1098
+ bindEvents() {
1099
+ const { toggle, disc, zoomIn, zoomOut, innerCircle } = this.dom;
1100
+ toggle.addEventListener("click", () => {
1101
+ if (this.visible) this.hide();
1102
+ else this.show();
1103
+ });
1104
+ toggle.addEventListener("mouseenter", () => this.show());
1105
+ toggle.addEventListener("mouseleave", () => {
1106
+ this.hideTimer = setTimeout(() => this.hide(), 15e3);
1107
+ });
1108
+ disc.addEventListener("mousemove", (e) => {
1109
+ if (e.target !== disc) return;
1110
+ if (!this.isDwelling) {
1111
+ this.startDwell(e);
1112
+ } else {
1113
+ this.handleMove(e);
1114
+ }
1115
+ });
1116
+ disc.addEventListener("mousedown", (e) => {
1117
+ if (e.target !== disc) return;
1118
+ e.preventDefault();
1119
+ this.handleMove(e);
1120
+ const moveHandler = (me) => this.handleMove(me);
1121
+ document.addEventListener("mousemove", moveHandler);
1122
+ document.addEventListener("mouseup", () => {
1123
+ document.removeEventListener("mousemove", moveHandler);
1124
+ this.stopDwell();
1125
+ this.resetJoystick();
1126
+ }, { once: true });
1127
+ });
1128
+ disc.addEventListener("mouseleave", () => {
1129
+ this.stopDwell();
1130
+ this.resetJoystick();
1131
+ });
1132
+ disc.addEventListener("touchstart", (e) => {
1133
+ if (e.target !== disc) return;
1134
+ e.preventDefault();
1135
+ this.handleMove(e);
1136
+ const moveHandler = (te) => this.handleMove(te);
1137
+ document.addEventListener("touchmove", moveHandler, { passive: false });
1138
+ document.addEventListener("touchend", () => {
1139
+ document.removeEventListener("touchmove", moveHandler);
1140
+ this.stopDwell();
1141
+ this.resetJoystick();
1142
+ }, { once: true });
1143
+ }, { passive: false });
1144
+ zoomIn.addEventListener("click", (e) => {
1145
+ e.stopPropagation();
1146
+ this.viewer.zoomIn();
1147
+ });
1148
+ zoomOut.addEventListener("click", (e) => {
1149
+ e.stopPropagation();
1150
+ this.viewer.zoomOut();
1151
+ });
1152
+ innerCircle.addEventListener("mouseenter", () => {
1153
+ this.stopDwell();
1154
+ this.resetJoystick();
1155
+ });
1156
+ innerCircle.addEventListener("mousedown", (e) => e.stopPropagation());
1157
+ innerCircle.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: false });
1158
+ }
1159
+ handleMove(event) {
1160
+ if (!this.visible) return;
1161
+ const clientX = "clientX" in event ? event.clientX : event.touches[0]?.clientX ?? 0;
1162
+ const clientY = "clientY" in event ? event.clientY : event.touches[0]?.clientY ?? 0;
1163
+ const rect = this.dom.disc.getBoundingClientRect();
1164
+ const centerX = rect.left + rect.width / 2;
1165
+ const centerY = rect.top + rect.height / 2;
1166
+ const distX = clientX - centerX;
1167
+ const distY = clientY - centerY;
1168
+ const distance = Math.sqrt(distX * distX + distY * distY);
1169
+ let normalizedX = distX / this.options.radius;
1170
+ let normalizedY = distY / this.options.radius;
1171
+ if (distance > this.options.radius) {
1172
+ normalizedX = normalizedX * (this.options.radius / distance);
1173
+ normalizedY = normalizedY * (this.options.radius / distance);
1174
+ }
1175
+ if (Math.abs(normalizedX) < this.options.deadzone) normalizedX = 0;
1176
+ if (Math.abs(normalizedY) < this.options.deadzone) normalizedY = 0;
1177
+ this.joystickX = normalizedX;
1178
+ this.joystickY = normalizedY;
1179
+ if (distance > this.options.radius * this.options.deadzone) {
1180
+ const angle = Math.atan2(distY, distX) * 180 / Math.PI;
1181
+ const dir = angleToDirection(angle);
1182
+ if (dir !== this.currentDirection) {
1183
+ this.clearDirection();
1184
+ this.dom.disc.classList.add(dir);
1185
+ this.currentDirection = dir;
1186
+ }
1187
+ } else {
1188
+ this.clearDirection();
1189
+ }
1190
+ if (!this.animationId && (Math.abs(normalizedX) > this.options.deadzone || Math.abs(normalizedY) > this.options.deadzone)) {
1191
+ this.active = true;
1192
+ this.dom.disc.classList.add("active");
1193
+ this.startMovement();
1194
+ this.updateAria(normalizedX, normalizedY);
1195
+ } else if (Math.abs(normalizedX) <= this.options.deadzone && Math.abs(normalizedY) <= this.options.deadzone) {
1196
+ this.active = false;
1197
+ this.dom.disc.classList.remove("active");
1198
+ this.stopMovement();
1199
+ this.dom.disc.setAttribute("aria-valuenow", "0");
1200
+ this.dom.disc.setAttribute("aria-valuetext", "Center position \u2014 not moving");
1201
+ }
1202
+ }
1203
+ startDwell(e) {
1204
+ if (this.dwellTimer) clearTimeout(this.dwellTimer);
1205
+ this.dwellTimer = setTimeout(() => {
1206
+ this.isDwelling = true;
1207
+ this.handleMove(e);
1208
+ }, this.options.dwellTimeout);
1209
+ }
1210
+ stopDwell() {
1211
+ if (this.dwellTimer) {
1212
+ clearTimeout(this.dwellTimer);
1213
+ this.dwellTimer = null;
1214
+ }
1215
+ this.isDwelling = false;
1216
+ }
1217
+ startMovement() {
1218
+ if (this.animationId) return;
1219
+ const step = () => {
1220
+ if (!this.active) {
1221
+ this.stopMovement();
1222
+ return;
1223
+ }
1224
+ const vx = -this.joystickX * this.options.maxSpeed;
1225
+ const vy = -this.joystickY * this.options.maxSpeed;
1226
+ this.viewer.applyVelocity(vx, vy);
1227
+ this.animationId = requestAnimationFrame(step);
1228
+ };
1229
+ this.animationId = requestAnimationFrame(step);
1230
+ }
1231
+ stopMovement() {
1232
+ if (this.animationId) {
1233
+ cancelAnimationFrame(this.animationId);
1234
+ this.animationId = null;
1235
+ }
1236
+ }
1237
+ resetJoystick() {
1238
+ this.joystickX = 0;
1239
+ this.joystickY = 0;
1240
+ this.active = false;
1241
+ this.stopMovement();
1242
+ this.clearDirection();
1243
+ this.dom.disc.classList.remove("active");
1244
+ }
1245
+ clearDirection() {
1246
+ if (this.currentDirection) {
1247
+ this.dom.disc.classList.remove(this.currentDirection);
1248
+ this.currentDirection = "";
1249
+ }
1250
+ }
1251
+ updateAria(x, y) {
1252
+ const magnitude = Math.sqrt(x * x + y * y);
1253
+ this.dom.disc.setAttribute("aria-valuenow", magnitude.toFixed(2));
1254
+ let direction;
1255
+ if (Math.abs(x) > Math.abs(y)) {
1256
+ direction = x > 0 ? "right" : "left";
1257
+ } else {
1258
+ direction = y > 0 ? "down" : "up";
1259
+ }
1260
+ const intensity = magnitude > 0.7 ? "fast" : magnitude > 0.3 ? "medium" : "slow";
1261
+ this.dom.disc.setAttribute("aria-valuetext", `Moving ${intensity} ${direction}`);
1262
+ }
1263
+ };
1264
+ exports.ZoooomJoystick = exports.default.ZoooomJoystick; exports.default = exports.default.default;
1265
+
1266
+ Object.defineProperty(exports, '__esModule', { value: true });
1267
+
1268
+ return exports;
1269
+
1270
+ })({});
1271
+ //# sourceMappingURL=zoooom-full.iife.global.js.map
1272
+ //# sourceMappingURL=zoooom-full.iife.global.js.map