1 | import { extend } from "../shared/utils/extend.js";
|
2 | import { round } from "../shared/utils/round.js";
|
3 | import { isScrollable } from "../shared/utils/isScrollable.js";
|
4 |
|
5 | import { ResizeObserver } from "../shared/utils/ResizeObserver.js";
|
6 | import { PointerTracker, getMidpoint, getDistance } from "../shared/utils/PointerTracker.js";
|
7 |
|
8 | import { getTextNodeFromPoint } from "../shared/utils/getTextNodeFromPoint.js";
|
9 |
|
10 | import { getFullWidth, getFullHeight, calculateAspectRatioFit } from "../shared/utils/getDimensions.js";
|
11 |
|
12 | import { Base } from "../shared/Base/Base.js";
|
13 |
|
14 | import { Plugins } from "./plugins/index.js";
|
15 |
|
16 | const defaults = {
|
17 |
|
18 | touch: true,
|
19 |
|
20 |
|
21 | zoom: true,
|
22 |
|
23 |
|
24 | pinchToZoom: true,
|
25 |
|
26 |
|
27 | panOnlyZoomed: false,
|
28 |
|
29 |
|
30 |
|
31 | lockAxis: false,
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | friction: 0.64,
|
38 |
|
39 |
|
40 | decelFriction: 0.88,
|
41 |
|
42 |
|
43 | zoomFriction: 0.74,
|
44 |
|
45 |
|
46 | bounceForce: 0.2,
|
47 |
|
48 |
|
49 | baseScale: 1,
|
50 |
|
51 |
|
52 | minScale: 1,
|
53 |
|
54 |
|
55 | maxScale: 2,
|
56 |
|
57 |
|
58 | step: 0.5,
|
59 |
|
60 |
|
61 |
|
62 | textSelection: false,
|
63 |
|
64 |
|
65 |
|
66 | click: "toggleZoom",
|
67 |
|
68 |
|
69 |
|
70 | wheel: "zoom",
|
71 |
|
72 |
|
73 | wheelFactor: 42,
|
74 |
|
75 |
|
76 | wheelLimit: 5,
|
77 |
|
78 |
|
79 | draggableClass: "is-draggable",
|
80 |
|
81 |
|
82 | draggingClass: "is-dragging",
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | ratio: 1,
|
88 | };
|
89 |
|
90 | export class Panzoom extends Base {
|
91 | |
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | constructor($container, options = {}) {
|
98 | super(extend(true, {}, defaults, options));
|
99 |
|
100 | this.state = "init";
|
101 |
|
102 | this.$container = $container;
|
103 |
|
104 |
|
105 | for (const methodName of ["onLoad", "onWheel", "onClick"]) {
|
106 | this[methodName] = this[methodName].bind(this);
|
107 | }
|
108 |
|
109 | this.initLayout();
|
110 |
|
111 | this.resetValues();
|
112 |
|
113 | this.attachPlugins(Panzoom.Plugins);
|
114 |
|
115 | this.trigger("init");
|
116 |
|
117 | this.updateMetrics();
|
118 |
|
119 | this.attachEvents();
|
120 |
|
121 | this.trigger("ready");
|
122 |
|
123 | if (this.option("centerOnStart") === false) {
|
124 | this.state = "ready";
|
125 | } else {
|
126 | this.panTo({
|
127 | friction: 0,
|
128 | });
|
129 | }
|
130 |
|
131 | $container.__Panzoom = this;
|
132 | }
|
133 |
|
134 | |
135 |
|
136 |
|
137 | initLayout() {
|
138 | const $container = this.$container;
|
139 |
|
140 |
|
141 | if (!($container instanceof HTMLElement)) {
|
142 | throw new Error("Panzoom: Container not found");
|
143 | }
|
144 |
|
145 | const $content = this.option("content") || $container.querySelector(".panzoom__content");
|
146 |
|
147 |
|
148 | if (!$content) {
|
149 | throw new Error("Panzoom: Content not found");
|
150 | }
|
151 |
|
152 | this.$content = $content;
|
153 |
|
154 | let $viewport = this.option("viewport") || $container.querySelector(".panzoom__viewport");
|
155 |
|
156 | if (!$viewport && this.option("wrapInner") !== false) {
|
157 | $viewport = document.createElement("div");
|
158 | $viewport.classList.add("panzoom__viewport");
|
159 |
|
160 | $viewport.append(...$container.childNodes);
|
161 |
|
162 | $container.appendChild($viewport);
|
163 | }
|
164 |
|
165 | this.$viewport = $viewport || $content.parentNode;
|
166 | }
|
167 |
|
168 | |
169 |
|
170 |
|
171 | resetValues() {
|
172 | this.updateRate = this.option("updateRate", /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) ? 250 : 24);
|
173 |
|
174 | this.container = {
|
175 | width: 0,
|
176 | height: 0,
|
177 | };
|
178 |
|
179 | this.viewport = {
|
180 | width: 0,
|
181 | height: 0,
|
182 | };
|
183 |
|
184 | this.content = {
|
185 |
|
186 | origWidth: 0,
|
187 | origHeight: 0,
|
188 |
|
189 |
|
190 | width: 0,
|
191 | height: 0,
|
192 |
|
193 |
|
194 | x: this.option("x", 0),
|
195 | y: this.option("y", 0),
|
196 |
|
197 |
|
198 | scale: this.option("baseScale"),
|
199 | };
|
200 |
|
201 |
|
202 | this.transform = {
|
203 | x: 0,
|
204 | y: 0,
|
205 | scale: 1,
|
206 | };
|
207 |
|
208 | this.resetDragPosition();
|
209 | }
|
210 |
|
211 | |
212 |
|
213 |
|
214 |
|
215 | onLoad(event) {
|
216 | this.updateMetrics();
|
217 |
|
218 | this.panTo({ scale: this.option("baseScale"), friction: 0 });
|
219 |
|
220 | this.trigger("load", event);
|
221 | }
|
222 |
|
223 | |
224 |
|
225 |
|
226 |
|
227 | onClick(event) {
|
228 | if (event.defaultPrevented) {
|
229 | return;
|
230 | }
|
231 |
|
232 |
|
233 | if (this.option("textSelection") && window.getSelection().toString().length) {
|
234 | event.stopPropagation();
|
235 | return;
|
236 | }
|
237 |
|
238 | const rect = this.$content.getClientRects()[0];
|
239 |
|
240 |
|
241 | if (this.state !== "ready") {
|
242 | if (
|
243 | this.dragPosition.midPoint ||
|
244 | Math.abs(rect.top - this.dragStart.rect.top) > 1 ||
|
245 | Math.abs(rect.left - this.dragStart.rect.left) > 1
|
246 | ) {
|
247 | event.preventDefault();
|
248 | event.stopPropagation();
|
249 |
|
250 | return;
|
251 | }
|
252 | }
|
253 |
|
254 | if (this.trigger("click", event) === false) {
|
255 | return;
|
256 | }
|
257 |
|
258 | if (this.option("zoom") && this.option("click") === "toggleZoom") {
|
259 | event.preventDefault();
|
260 | event.stopPropagation();
|
261 |
|
262 | this.zoomWithClick(event);
|
263 | }
|
264 | }
|
265 |
|
266 | |
267 |
|
268 |
|
269 |
|
270 | onWheel(event) {
|
271 | if (this.trigger("wheel", event) === false) {
|
272 | return;
|
273 | }
|
274 |
|
275 | if (this.option("zoom") && this.option("wheel")) {
|
276 | this.zoomWithWheel(event);
|
277 | }
|
278 | }
|
279 |
|
280 | |
281 |
|
282 |
|
283 |
|
284 | zoomWithWheel(event) {
|
285 | if (this.changedDelta === undefined) {
|
286 | this.changedDelta = 0;
|
287 | }
|
288 |
|
289 | const delta = Math.max(-1, Math.min(1, -event.deltaY || -event.deltaX || event.wheelDelta || -event.detail));
|
290 | const scale = this.content.scale;
|
291 |
|
292 | let newScale = (scale * (100 + delta * this.option("wheelFactor"))) / 100;
|
293 |
|
294 | if (
|
295 | (delta < 0 && Math.abs(scale - this.option("minScale")) < 0.01) ||
|
296 | (delta > 0 && Math.abs(scale - this.option("maxScale")) < 0.01)
|
297 | ) {
|
298 | this.changedDelta += Math.abs(delta);
|
299 | newScale = scale;
|
300 | } else {
|
301 | this.changedDelta = 0;
|
302 | newScale = Math.max(Math.min(newScale, this.option("maxScale")), this.option("minScale"));
|
303 | }
|
304 |
|
305 | if (this.changedDelta > this.option("wheelLimit")) {
|
306 | return;
|
307 | }
|
308 |
|
309 | event.preventDefault();
|
310 |
|
311 | if (newScale === scale) {
|
312 | return;
|
313 | }
|
314 |
|
315 | const rect = this.$content.getBoundingClientRect();
|
316 |
|
317 | const x = event.clientX - rect.left;
|
318 | const y = event.clientY - rect.top;
|
319 |
|
320 | this.zoomTo(newScale, { x, y });
|
321 | }
|
322 |
|
323 | |
324 |
|
325 |
|
326 |
|
327 | zoomWithClick(event) {
|
328 | const rect = this.$content.getClientRects()[0];
|
329 |
|
330 | const x = event.clientX - rect.left;
|
331 | const y = event.clientY - rect.top;
|
332 |
|
333 | this.toggleZoom({ x, y });
|
334 | }
|
335 |
|
336 | |
337 |
|
338 |
|
339 | attachEvents() {
|
340 | this.$content.addEventListener("load", this.onLoad);
|
341 |
|
342 | this.$container.addEventListener("wheel", this.onWheel, { passive: false });
|
343 | this.$container.addEventListener("click", this.onClick, { passive: false });
|
344 |
|
345 | this.initObserver();
|
346 |
|
347 | const pointerTracker = new PointerTracker(this.$container, {
|
348 | start: (pointer, event) => {
|
349 | if (!this.option("touch")) {
|
350 | return false;
|
351 | }
|
352 |
|
353 | if (this.velocity.scale < 0) {
|
354 | return false;
|
355 | }
|
356 |
|
357 | const target = event.composedPath()[0];
|
358 |
|
359 | if (!pointerTracker.currentPointers.length) {
|
360 | const ignoreClickedElement =
|
361 | ["BUTTON", "TEXTAREA", "OPTION", "INPUT", "SELECT", "VIDEO"].indexOf(target.nodeName) !== -1;
|
362 |
|
363 | if (ignoreClickedElement) {
|
364 | return false;
|
365 | }
|
366 |
|
367 |
|
368 | if (this.option("textSelection") && getTextNodeFromPoint(target, pointer.clientX, pointer.clientY)) {
|
369 | return false;
|
370 | }
|
371 | }
|
372 |
|
373 | if (isScrollable(target)) {
|
374 | return false;
|
375 | }
|
376 |
|
377 | if (this.trigger("touchStart", event) === false) {
|
378 | return false;
|
379 | }
|
380 |
|
381 | if (event.type === "mousedown") {
|
382 | event.preventDefault();
|
383 | }
|
384 |
|
385 | this.state = "pointerdown";
|
386 |
|
387 | this.resetDragPosition();
|
388 |
|
389 | this.dragPosition.midPoint = null;
|
390 | this.dragPosition.time = Date.now();
|
391 |
|
392 | return true;
|
393 | },
|
394 | move: (previousPointers, currentPointers, event) => {
|
395 | if (this.state !== "pointerdown") {
|
396 | return;
|
397 | }
|
398 |
|
399 | if (this.trigger("touchMove", event) === false) {
|
400 | event.preventDefault();
|
401 | return;
|
402 | }
|
403 |
|
404 |
|
405 | if (
|
406 | currentPointers.length < 2 &&
|
407 | this.option("panOnlyZoomed") === true &&
|
408 | this.content.width <= this.viewport.width &&
|
409 | this.content.height <= this.viewport.height &&
|
410 | this.transform.scale <= this.option("baseScale")
|
411 | ) {
|
412 | return;
|
413 | }
|
414 |
|
415 | if (currentPointers.length > 1 && (!this.option("zoom") || this.option("pinchToZoom") === false)) {
|
416 | return;
|
417 | }
|
418 |
|
419 | const prevMidpoint = getMidpoint(previousPointers[0], previousPointers[1]);
|
420 | const newMidpoint = getMidpoint(currentPointers[0], currentPointers[1]);
|
421 |
|
422 | const panX = newMidpoint.clientX - prevMidpoint.clientX;
|
423 | const panY = newMidpoint.clientY - prevMidpoint.clientY;
|
424 |
|
425 | const prevDistance = getDistance(previousPointers[0], previousPointers[1]);
|
426 | const newDistance = getDistance(currentPointers[0], currentPointers[1]);
|
427 |
|
428 | const scaleDiff = prevDistance && newDistance ? newDistance / prevDistance : 1;
|
429 |
|
430 | this.dragOffset.x += panX;
|
431 | this.dragOffset.y += panY;
|
432 |
|
433 | this.dragOffset.scale *= scaleDiff;
|
434 |
|
435 | this.dragOffset.time = Date.now() - this.dragPosition.time;
|
436 |
|
437 | const axisToLock = this.dragStart.scale === 1 && this.option("lockAxis");
|
438 |
|
439 | if (axisToLock && !this.lockAxis) {
|
440 | if (Math.abs(this.dragOffset.x) < 6 && Math.abs(this.dragOffset.y) < 6) {
|
441 | event.preventDefault();
|
442 | return;
|
443 | }
|
444 |
|
445 | const angle = Math.abs((Math.atan2(this.dragOffset.y, this.dragOffset.x) * 180) / Math.PI);
|
446 |
|
447 | this.lockAxis = angle > 45 && angle < 135 ? "y" : "x";
|
448 | }
|
449 |
|
450 | if (axisToLock !== "xy" && this.lockAxis === "y") {
|
451 | return;
|
452 | }
|
453 |
|
454 | event.preventDefault();
|
455 | event.stopPropagation();
|
456 |
|
457 | event.stopImmediatePropagation();
|
458 |
|
459 | if (this.lockAxis) {
|
460 | this.dragOffset[this.lockAxis === "x" ? "y" : "x"] = 0;
|
461 | }
|
462 |
|
463 | this.$container.classList.add(this.option("draggingClass"));
|
464 |
|
465 | if (!(this.transform.scale === this.option("baseScale") && this.lockAxis === "y")) {
|
466 | this.dragPosition.x = this.dragStart.x + this.dragOffset.x;
|
467 | }
|
468 |
|
469 | if (!(this.transform.scale === this.option("baseScale") && this.lockAxis === "x")) {
|
470 | this.dragPosition.y = this.dragStart.y + this.dragOffset.y;
|
471 | }
|
472 |
|
473 | this.dragPosition.scale = this.dragStart.scale * this.dragOffset.scale;
|
474 |
|
475 | if (currentPointers.length > 1) {
|
476 | const startPoint = getMidpoint(pointerTracker.startPointers[0], pointerTracker.startPointers[1]);
|
477 |
|
478 | const xPos = startPoint.clientX - this.dragStart.rect.x;
|
479 | const yPos = startPoint.clientY - this.dragStart.rect.y;
|
480 |
|
481 | const { deltaX, deltaY } = this.getZoomDelta(this.content.scale * this.dragOffset.scale, xPos, yPos);
|
482 |
|
483 | this.dragPosition.x -= deltaX;
|
484 | this.dragPosition.y -= deltaY;
|
485 |
|
486 | this.dragPosition.midPoint = newMidpoint;
|
487 | } else {
|
488 | this.setDragResistance();
|
489 | }
|
490 |
|
491 |
|
492 | this.transform = {
|
493 | x: this.dragPosition.x,
|
494 | y: this.dragPosition.y,
|
495 | scale: this.dragPosition.scale,
|
496 | };
|
497 |
|
498 | this.startAnimation();
|
499 | },
|
500 | end: (pointer, event) => {
|
501 | if (this.state !== "pointerdown") {
|
502 | return;
|
503 | }
|
504 |
|
505 | this._dragOffset = { ...this.dragOffset };
|
506 |
|
507 | if (pointerTracker.currentPointers.length) {
|
508 | this.resetDragPosition();
|
509 |
|
510 | return;
|
511 | }
|
512 |
|
513 | this.state = "decel";
|
514 | this.friction = this.option("decelFriction");
|
515 |
|
516 | this.recalculateTransform();
|
517 |
|
518 | this.$container.classList.remove(this.option("draggingClass"));
|
519 |
|
520 | if (this.trigger("touchEnd", event) === false) {
|
521 | return;
|
522 | }
|
523 |
|
524 | if (this.state !== "decel") {
|
525 | return;
|
526 | }
|
527 |
|
528 |
|
529 |
|
530 |
|
531 | const minScale = this.option("minScale");
|
532 |
|
533 | if (this.transform.scale < minScale) {
|
534 | this.zoomTo(minScale, { friction: 0.64 });
|
535 |
|
536 | return;
|
537 | }
|
538 |
|
539 |
|
540 | const maxScale = this.option("maxScale");
|
541 |
|
542 | if (this.transform.scale - maxScale > 0.01) {
|
543 | const last = this.dragPosition.midPoint || pointer;
|
544 | const rect = this.$content.getClientRects()[0];
|
545 |
|
546 | this.zoomTo(maxScale, {
|
547 | friction: 0.64,
|
548 | x: last.clientX - rect.left,
|
549 | y: last.clientY - rect.top,
|
550 | });
|
551 |
|
552 | return;
|
553 | }
|
554 | },
|
555 | });
|
556 |
|
557 | this.pointerTracker = pointerTracker;
|
558 | }
|
559 |
|
560 | initObserver() {
|
561 | if (this.resizeObserver) {
|
562 | return;
|
563 | }
|
564 |
|
565 | this.resizeObserver = new ResizeObserver(() => {
|
566 | if (this.updateTimer) {
|
567 | return;
|
568 | }
|
569 |
|
570 | this.updateTimer = setTimeout(() => {
|
571 | const rect = this.$container.getBoundingClientRect();
|
572 |
|
573 | if (!(rect.width && rect.height)) {
|
574 | this.updateTimer = null;
|
575 | return;
|
576 | }
|
577 |
|
578 |
|
579 | if (Math.abs(rect.width - this.container.width) > 1 || Math.abs(rect.height - this.container.height) > 1) {
|
580 | if (this.isAnimating()) {
|
581 | this.endAnimation(true);
|
582 | }
|
583 |
|
584 | this.updateMetrics();
|
585 |
|
586 | this.panTo({
|
587 | x: this.content.x,
|
588 | y: this.content.y,
|
589 | scale: this.option("baseScale"),
|
590 | friction: 0,
|
591 | });
|
592 | }
|
593 |
|
594 | this.updateTimer = null;
|
595 | }, this.updateRate);
|
596 | });
|
597 |
|
598 | this.resizeObserver.observe(this.$container);
|
599 | }
|
600 |
|
601 | |
602 |
|
603 |
|
604 | resetDragPosition() {
|
605 | this.lockAxis = null;
|
606 | this.friction = this.option("friction");
|
607 |
|
608 | this.velocity = {
|
609 | x: 0,
|
610 | y: 0,
|
611 | scale: 0,
|
612 | };
|
613 |
|
614 | const { x, y, scale } = this.content;
|
615 |
|
616 | this.dragStart = {
|
617 | rect: this.$content.getBoundingClientRect(),
|
618 | x,
|
619 | y,
|
620 | scale,
|
621 | };
|
622 |
|
623 | this.dragPosition = {
|
624 | ...this.dragPosition,
|
625 | x,
|
626 | y,
|
627 | scale,
|
628 | };
|
629 |
|
630 | this.dragOffset = {
|
631 | x: 0,
|
632 | y: 0,
|
633 | scale: 1,
|
634 | time: 0,
|
635 | };
|
636 | }
|
637 |
|
638 | |
639 |
|
640 |
|
641 |
|
642 | updateMetrics(silently) {
|
643 | if (silently !== true) {
|
644 | this.trigger("beforeUpdate");
|
645 | }
|
646 |
|
647 | const $container = this.$container;
|
648 | const $content = this.$content;
|
649 | const $viewport = this.$viewport;
|
650 |
|
651 | const contentIsImage = $content instanceof HTMLImageElement;
|
652 | const contentIsZoomable = this.option("zoom");
|
653 | const shouldResizeParent = this.option("resizeParent", contentIsZoomable);
|
654 |
|
655 | let width = this.option("width");
|
656 | let height = this.option("height");
|
657 |
|
658 | let origWidth = width || getFullWidth($content);
|
659 | let origHeight = height || getFullHeight($content);
|
660 |
|
661 | Object.assign($content.style, {
|
662 | width: width ? `${width}px` : "",
|
663 | height: height ? `${height}px` : "",
|
664 | maxWidth: "",
|
665 | maxHeight: "",
|
666 | });
|
667 |
|
668 | if (shouldResizeParent) {
|
669 | Object.assign($viewport.style, { width: "", height: "" });
|
670 | }
|
671 |
|
672 | const ratio = this.option("ratio");
|
673 |
|
674 | origWidth = round(origWidth * ratio);
|
675 | origHeight = round(origHeight * ratio);
|
676 |
|
677 | width = origWidth;
|
678 | height = origHeight;
|
679 |
|
680 | const contentRect = $content.getBoundingClientRect();
|
681 | const viewportRect = $viewport.getBoundingClientRect();
|
682 |
|
683 | const containerRect = $viewport == $container ? viewportRect : $container.getBoundingClientRect();
|
684 |
|
685 | let viewportWidth = Math.max($viewport.offsetWidth, round(viewportRect.width));
|
686 | let viewportHeight = Math.max($viewport.offsetHeight, round(viewportRect.height));
|
687 |
|
688 | let viewportStyles = window.getComputedStyle($viewport);
|
689 | viewportWidth -= parseFloat(viewportStyles.paddingLeft) + parseFloat(viewportStyles.paddingRight);
|
690 | viewportHeight -= parseFloat(viewportStyles.paddingTop) + parseFloat(viewportStyles.paddingBottom);
|
691 |
|
692 | this.viewport.width = viewportWidth;
|
693 | this.viewport.height = viewportHeight;
|
694 |
|
695 | if (contentIsZoomable) {
|
696 | if (Math.abs(origWidth - contentRect.width) > 0.1 || Math.abs(origHeight - contentRect.height) > 0.1) {
|
697 | const rez = calculateAspectRatioFit(
|
698 | origWidth,
|
699 | origHeight,
|
700 | Math.min(origWidth, contentRect.width),
|
701 | Math.min(origHeight, contentRect.height)
|
702 | );
|
703 |
|
704 | width = round(rez.width);
|
705 | height = round(rez.height);
|
706 | }
|
707 |
|
708 | Object.assign($content.style, {
|
709 | width: `${width}px`,
|
710 | height: `${height}px`,
|
711 | transform: "",
|
712 | });
|
713 | }
|
714 |
|
715 | if (shouldResizeParent) {
|
716 | Object.assign($viewport.style, { width: `${width}px`, height: `${height}px` });
|
717 |
|
718 | this.viewport = { ...this.viewport, width, height };
|
719 | }
|
720 |
|
721 | if (contentIsImage && contentIsZoomable && typeof this.options.maxScale !== "function") {
|
722 | const maxScale = this.option("maxScale");
|
723 |
|
724 | this.options.maxScale = function () {
|
725 | return this.content.origWidth > 0 && this.content.fitWidth > 0
|
726 | ? this.content.origWidth / this.content.fitWidth
|
727 | : maxScale;
|
728 | };
|
729 | }
|
730 |
|
731 | this.content = {
|
732 | ...this.content,
|
733 | origWidth,
|
734 | origHeight,
|
735 | fitWidth: width,
|
736 | fitHeight: height,
|
737 | width,
|
738 | height,
|
739 | scale: 1,
|
740 | isZoomable: contentIsZoomable,
|
741 | };
|
742 |
|
743 | this.container = { width: containerRect.width, height: containerRect.height };
|
744 |
|
745 | if (silently !== true) {
|
746 | this.trigger("afterUpdate");
|
747 | }
|
748 | }
|
749 |
|
750 | |
751 |
|
752 |
|
753 |
|
754 | zoomIn(step) {
|
755 | this.zoomTo(this.content.scale + (step || this.option("step")));
|
756 | }
|
757 |
|
758 | |
759 |
|
760 |
|
761 |
|
762 | zoomOut(step) {
|
763 | this.zoomTo(this.content.scale - (step || this.option("step")));
|
764 | }
|
765 |
|
766 | |
767 |
|
768 |
|
769 |
|
770 | toggleZoom(props = {}) {
|
771 | const maxScale = this.option("maxScale");
|
772 | const baseScale = this.option("baseScale");
|
773 |
|
774 | const scale = this.content.scale > baseScale + (maxScale - baseScale) * 0.5 ? baseScale : maxScale;
|
775 |
|
776 | this.zoomTo(scale, props);
|
777 | }
|
778 |
|
779 | |
780 |
|
781 |
|
782 |
|
783 |
|
784 | zoomTo(scale = this.option("baseScale"), { x = null, y = null } = {}) {
|
785 | scale = Math.max(Math.min(scale, this.option("maxScale")), this.option("minScale"));
|
786 |
|
787 |
|
788 | const currentScale = round(this.content.scale / (this.content.width / this.content.fitWidth), 10000000);
|
789 |
|
790 | if (x === null) {
|
791 | x = this.content.width * currentScale * 0.5;
|
792 | }
|
793 |
|
794 | if (y === null) {
|
795 | y = this.content.height * currentScale * 0.5;
|
796 | }
|
797 |
|
798 | const { deltaX, deltaY } = this.getZoomDelta(scale, x, y);
|
799 |
|
800 | x = this.content.x - deltaX;
|
801 | y = this.content.y - deltaY;
|
802 |
|
803 | this.panTo({ x, y, scale, friction: this.option("zoomFriction") });
|
804 | }
|
805 |
|
806 | |
807 |
|
808 |
|
809 |
|
810 |
|
811 |
|
812 |
|
813 | getZoomDelta(scale, x = 0, y = 0) {
|
814 | const currentWidth = this.content.fitWidth * this.content.scale;
|
815 | const currentHeight = this.content.fitHeight * this.content.scale;
|
816 |
|
817 | const percentXInCurrentBox = x > 0 && currentWidth ? x / currentWidth : 0;
|
818 | const percentYInCurrentBox = y > 0 && currentHeight ? y / currentHeight : 0;
|
819 |
|
820 | const nextWidth = this.content.fitWidth * scale;
|
821 | const nextHeight = this.content.fitHeight * scale;
|
822 |
|
823 | const deltaX = (nextWidth - currentWidth) * percentXInCurrentBox;
|
824 | const deltaY = (nextHeight - currentHeight) * percentYInCurrentBox;
|
825 |
|
826 | return { deltaX, deltaY };
|
827 | }
|
828 |
|
829 | |
830 |
|
831 |
|
832 |
|
833 | panTo({
|
834 | x = this.content.x,
|
835 | y = this.content.y,
|
836 | scale,
|
837 | friction = this.option("friction"),
|
838 | ignoreBounds = false,
|
839 | } = {}) {
|
840 | scale = scale || this.content.scale || 1;
|
841 |
|
842 | if (!ignoreBounds) {
|
843 | const { boundX, boundY } = this.getBounds(scale);
|
844 |
|
845 | if (boundX) {
|
846 | x = Math.max(Math.min(x, boundX.to), boundX.from);
|
847 | }
|
848 |
|
849 | if (boundY) {
|
850 | y = Math.max(Math.min(y, boundY.to), boundY.from);
|
851 | }
|
852 | }
|
853 |
|
854 | this.friction = friction;
|
855 |
|
856 | this.transform = {
|
857 | ...this.transform,
|
858 | x,
|
859 | y,
|
860 | scale,
|
861 | };
|
862 |
|
863 | if (friction) {
|
864 | this.state = "panning";
|
865 |
|
866 | this.velocity = {
|
867 | x: (1 / this.friction - 1) * (x - this.content.x),
|
868 | y: (1 / this.friction - 1) * (y - this.content.y),
|
869 | scale: (1 / this.friction - 1) * (scale - this.content.scale),
|
870 | };
|
871 |
|
872 | this.startAnimation();
|
873 | } else {
|
874 | this.endAnimation();
|
875 | }
|
876 | }
|
877 |
|
878 | |
879 |
|
880 |
|
881 | startAnimation() {
|
882 | if (!this.rAF) {
|
883 | this.trigger("startAnimation");
|
884 | } else {
|
885 | cancelAnimationFrame(this.rAF);
|
886 | }
|
887 |
|
888 | this.rAF = requestAnimationFrame(() => this.animate());
|
889 | }
|
890 |
|
891 | |
892 |
|
893 |
|
894 | animate() {
|
895 | this.setEdgeForce();
|
896 | this.setDragForce();
|
897 |
|
898 | this.velocity.x *= this.friction;
|
899 | this.velocity.y *= this.friction;
|
900 |
|
901 | this.velocity.scale *= this.friction;
|
902 |
|
903 | this.content.x += this.velocity.x;
|
904 | this.content.y += this.velocity.y;
|
905 |
|
906 | this.content.scale += this.velocity.scale;
|
907 |
|
908 | if (this.isAnimating()) {
|
909 | this.setTransform();
|
910 | } else if (this.state !== "pointerdown") {
|
911 | this.endAnimation();
|
912 |
|
913 | return;
|
914 | }
|
915 |
|
916 | this.rAF = requestAnimationFrame(() => this.animate());
|
917 | }
|
918 |
|
919 | |
920 |
|
921 |
|
922 | getBounds(scale) {
|
923 | let boundX = this.boundX;
|
924 | let boundY = this.boundY;
|
925 |
|
926 | if (boundX !== undefined && boundY !== undefined) {
|
927 | return { boundX, boundY };
|
928 | }
|
929 |
|
930 | boundX = { from: 0, to: 0 };
|
931 | boundY = { from: 0, to: 0 };
|
932 |
|
933 | scale = scale || this.transform.scale;
|
934 |
|
935 | const width = this.content.fitWidth * scale;
|
936 | const height = this.content.fitHeight * scale;
|
937 |
|
938 | const viewportWidth = this.viewport.width;
|
939 | const viewportHeight = this.viewport.height;
|
940 |
|
941 | if (width < viewportWidth) {
|
942 | const deltaX = round((viewportWidth - width) * 0.5);
|
943 |
|
944 | boundX.from = deltaX;
|
945 | boundX.to = deltaX;
|
946 | } else {
|
947 | boundX.from = round(viewportWidth - width);
|
948 | }
|
949 |
|
950 | if (height < viewportHeight) {
|
951 | const deltaY = (viewportHeight - height) * 0.5;
|
952 |
|
953 | boundY.from = deltaY;
|
954 | boundY.to = deltaY;
|
955 | } else {
|
956 | boundY.from = round(viewportHeight - height);
|
957 | }
|
958 |
|
959 | return { boundX, boundY };
|
960 | }
|
961 |
|
962 | |
963 |
|
964 |
|
965 | setEdgeForce() {
|
966 | if (this.state !== "decel") {
|
967 | return;
|
968 | }
|
969 |
|
970 | const bounceForce = this.option("bounceForce");
|
971 |
|
972 | const { boundX, boundY } = this.getBounds(Math.max(this.transform.scale, this.content.scale));
|
973 |
|
974 | let pastLeft, pastRight, pastTop, pastBottom;
|
975 |
|
976 | if (boundX) {
|
977 | pastLeft = this.content.x < boundX.from;
|
978 | pastRight = this.content.x > boundX.to;
|
979 | }
|
980 |
|
981 | if (boundY) {
|
982 | pastTop = this.content.y < boundY.from;
|
983 | pastBottom = this.content.y > boundY.to;
|
984 | }
|
985 |
|
986 | if (pastLeft || pastRight) {
|
987 | const bound = pastLeft ? boundX.from : boundX.to;
|
988 | const distance = bound - this.content.x;
|
989 |
|
990 | let force = distance * bounceForce;
|
991 |
|
992 | const restX = this.content.x + (this.velocity.x + force) / this.friction;
|
993 |
|
994 | if (restX >= boundX.from && restX <= boundX.to) {
|
995 | force += this.velocity.x;
|
996 | }
|
997 |
|
998 | this.velocity.x = force;
|
999 |
|
1000 | this.recalculateTransform();
|
1001 | }
|
1002 |
|
1003 | if (pastTop || pastBottom) {
|
1004 | const bound = pastTop ? boundY.from : boundY.to;
|
1005 | const distance = bound - this.content.y;
|
1006 |
|
1007 | let force = distance * bounceForce;
|
1008 |
|
1009 | const restY = this.content.y + (force + this.velocity.y) / this.friction;
|
1010 |
|
1011 | if (restY >= boundY.from && restY <= boundY.to) {
|
1012 | force += this.velocity.y;
|
1013 | }
|
1014 |
|
1015 | this.velocity.y = force;
|
1016 |
|
1017 | this.recalculateTransform();
|
1018 | }
|
1019 | }
|
1020 |
|
1021 | |
1022 |
|
1023 |
|
1024 | setDragResistance() {
|
1025 | if (this.state !== "pointerdown") {
|
1026 | return;
|
1027 | }
|
1028 |
|
1029 | const { boundX, boundY } = this.getBounds(this.dragPosition.scale);
|
1030 |
|
1031 | let pastLeft, pastRight, pastTop, pastBottom;
|
1032 |
|
1033 | if (boundX) {
|
1034 | pastLeft = this.dragPosition.x < boundX.from;
|
1035 | pastRight = this.dragPosition.x > boundX.to;
|
1036 | }
|
1037 |
|
1038 | if (boundY) {
|
1039 | pastTop = this.dragPosition.y < boundY.from;
|
1040 | pastBottom = this.dragPosition.y > boundY.to;
|
1041 | }
|
1042 |
|
1043 | if ((pastLeft || pastRight) && !(pastLeft && pastRight)) {
|
1044 | const bound = pastLeft ? boundX.from : boundX.to;
|
1045 | const distance = bound - this.dragPosition.x;
|
1046 |
|
1047 | this.dragPosition.x = bound - distance * 0.3;
|
1048 | }
|
1049 |
|
1050 | if ((pastTop || pastBottom) && !(pastTop && pastBottom)) {
|
1051 | const bound = pastTop ? boundY.from : boundY.to;
|
1052 | const distance = bound - this.dragPosition.y;
|
1053 |
|
1054 | this.dragPosition.y = bound - distance * 0.3;
|
1055 | }
|
1056 | }
|
1057 |
|
1058 | |
1059 |
|
1060 |
|
1061 | setDragForce() {
|
1062 | if (this.state === "pointerdown") {
|
1063 | this.velocity.x = this.dragPosition.x - this.content.x;
|
1064 | this.velocity.y = this.dragPosition.y - this.content.y;
|
1065 | this.velocity.scale = this.dragPosition.scale - this.content.scale;
|
1066 | }
|
1067 | }
|
1068 |
|
1069 | |
1070 |
|
1071 |
|
1072 | recalculateTransform() {
|
1073 | this.transform.x = this.content.x + this.velocity.x / (1 / this.friction - 1);
|
1074 | this.transform.y = this.content.y + this.velocity.y / (1 / this.friction - 1);
|
1075 | this.transform.scale = this.content.scale + this.velocity.scale / (1 / this.friction - 1);
|
1076 | }
|
1077 |
|
1078 | |
1079 |
|
1080 |
|
1081 |
|
1082 | isAnimating() {
|
1083 | return !!(
|
1084 | this.friction &&
|
1085 | (Math.abs(this.velocity.x) > 0.05 || Math.abs(this.velocity.y) > 0.05 || Math.abs(this.velocity.scale) > 0.05)
|
1086 | );
|
1087 | }
|
1088 |
|
1089 | |
1090 |
|
1091 |
|
1092 | setTransform(final) {
|
1093 | let x, y, scale;
|
1094 |
|
1095 | if (final) {
|
1096 | x = round(this.transform.x);
|
1097 | y = round(this.transform.y);
|
1098 |
|
1099 | scale = this.transform.scale;
|
1100 |
|
1101 | this.content = { ...this.content, x, y, scale };
|
1102 | } else {
|
1103 | x = round(this.content.x);
|
1104 | y = round(this.content.y);
|
1105 |
|
1106 | scale = this.content.scale / (this.content.width / this.content.fitWidth);
|
1107 |
|
1108 | this.content = { ...this.content, x, y };
|
1109 | }
|
1110 |
|
1111 | this.trigger("beforeTransform");
|
1112 |
|
1113 | x = round(this.content.x);
|
1114 | y = round(this.content.y);
|
1115 |
|
1116 | if (final && this.option("zoom")) {
|
1117 | let width;
|
1118 | let height;
|
1119 |
|
1120 | width = round(this.content.fitWidth * scale);
|
1121 | height = round(this.content.fitHeight * scale);
|
1122 |
|
1123 | this.content.width = width;
|
1124 | this.content.height = height;
|
1125 |
|
1126 | this.transform = { ...this.transform, width, height, scale };
|
1127 |
|
1128 | Object.assign(this.$content.style, {
|
1129 | width: `${width}px`,
|
1130 | height: `${height}px`,
|
1131 | maxWidth: "none",
|
1132 | maxHeight: "none",
|
1133 | transform: `translate3d(${x}px, ${y}px, 0) scale(1)`,
|
1134 | });
|
1135 | } else {
|
1136 | this.$content.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
|
1137 | }
|
1138 |
|
1139 | this.trigger("afterTransform");
|
1140 | }
|
1141 |
|
1142 | |
1143 |
|
1144 |
|
1145 | endAnimation(silently) {
|
1146 | cancelAnimationFrame(this.rAF);
|
1147 | this.rAF = null;
|
1148 |
|
1149 | this.velocity = {
|
1150 | x: 0,
|
1151 | y: 0,
|
1152 | scale: 0,
|
1153 | };
|
1154 |
|
1155 | this.setTransform(true);
|
1156 |
|
1157 | this.state = "ready";
|
1158 |
|
1159 | this.handleCursor();
|
1160 |
|
1161 | if (silently !== true) {
|
1162 | this.trigger("endAnimation");
|
1163 | }
|
1164 | }
|
1165 |
|
1166 | |
1167 |
|
1168 |
|
1169 | handleCursor() {
|
1170 | const draggableClass = this.option("draggableClass");
|
1171 |
|
1172 | if (!draggableClass || !this.option("touch")) {
|
1173 | return;
|
1174 | }
|
1175 |
|
1176 | if (
|
1177 | this.option("panOnlyZoomed") == true &&
|
1178 | this.content.width <= this.viewport.width &&
|
1179 | this.content.height <= this.viewport.height &&
|
1180 | this.transform.scale <= this.option("baseScale")
|
1181 | ) {
|
1182 | this.$container.classList.remove(draggableClass);
|
1183 | } else {
|
1184 | this.$container.classList.add(draggableClass);
|
1185 | }
|
1186 | }
|
1187 |
|
1188 | |
1189 |
|
1190 |
|
1191 | detachEvents() {
|
1192 | this.$content.removeEventListener("load", this.onLoad);
|
1193 |
|
1194 | this.$container.removeEventListener("wheel", this.onWheel, { passive: false });
|
1195 | this.$container.removeEventListener("click", this.onClick, { passive: false });
|
1196 |
|
1197 | if (this.pointerTracker) {
|
1198 | this.pointerTracker.stop();
|
1199 | this.pointerTracker = null;
|
1200 | }
|
1201 |
|
1202 | if (this.resizeObserver) {
|
1203 | this.resizeObserver.disconnect();
|
1204 | this.resizeObserver = null;
|
1205 | }
|
1206 | }
|
1207 |
|
1208 | |
1209 |
|
1210 |
|
1211 | destroy() {
|
1212 | if (this.state === "destroy") {
|
1213 | return;
|
1214 | }
|
1215 |
|
1216 | this.state = "destroy";
|
1217 |
|
1218 | clearTimeout(this.updateTimer);
|
1219 | this.updateTimer = null;
|
1220 |
|
1221 | cancelAnimationFrame(this.rAF);
|
1222 | this.rAF = null;
|
1223 |
|
1224 | this.detachEvents();
|
1225 |
|
1226 | this.detachPlugins();
|
1227 |
|
1228 | this.resetDragPosition();
|
1229 | }
|
1230 | }
|
1231 |
|
1232 |
|
1233 | Panzoom.version = "__VERSION__";
|
1234 |
|
1235 |
|
1236 | Panzoom.Plugins = Plugins;
|