aboutsummaryrefslogtreecommitdiff
path: root/viewer/src/services/ldzoom.ts
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/src/services/ldzoom.ts')
-rw-r--r--viewer/src/services/ldzoom.ts134
1 files changed, 134 insertions, 0 deletions
diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts
new file mode 100644
index 0000000..ddf57c0
--- /dev/null
+++ b/viewer/src/services/ldzoom.ts
@@ -0,0 +1,134 @@
1/* ldgallery - A static generator which turns a collection of tagged
2-- pictures into a searchable web gallery.
3--
4-- Copyright (C) 2020 Pacien TRAN-GIRARD
5--
6-- This program is free software: you can redistribute it and/or modify
7-- it under the terms of the GNU Affero General Public License as
8-- published by the Free Software Foundation, either version 3 of the
9-- License, or (at your option) any later version.
10--
11-- This program is distributed in the hope that it will be useful,
12-- but WITHOUT ANY WARRANTY; without even the implied warranty of
13-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14-- GNU Affero General Public License for more details.
15--
16-- You should have received a copy of the GNU Affero General Public License
17-- along with this program. If not, see <https://www.gnu.org/licenses/>.
18*/
19
20// polyfill still required for IE and Safari, see https://caniuse.com/#feat=resizeobserver
21import ResizeObserver from "resize-observer-polyfill";
22import "hammerjs";
23
24/**
25 * Mousewheel and pinch zoom handler.
26 */
27export default class LdZoom {
28 readonly containerElement: HTMLDivElement;
29 readonly imageElement: HTMLImageElement;
30 readonly pictureProperties: Gallery.PictureProperties;
31 readonly maxScaleFactor: number;
32 readonly scrollZoomSpeed: number;
33 scaleFactor: number = 0.0;
34
35 constructor(
36 containerElement: HTMLDivElement, imageElement: HTMLImageElement,
37 pictureProperties: Gallery.PictureProperties,
38 maxScaleFactor: number, scrollZoomSpeed: number
39 ) {
40 this.containerElement = containerElement;
41 this.imageElement = imageElement;
42 this.pictureProperties = pictureProperties;
43 this.maxScaleFactor = maxScaleFactor;
44 this.scrollZoomSpeed = scrollZoomSpeed;
45 }
46
47 /**
48 * Register event listeners.
49 */
50 public install() {
51 this.updateImageScale(this.scaleFactor);
52
53 new ResizeObserver(() => {
54 this.updateImageScale(this.scaleFactor);
55 }).observe(this.containerElement);
56
57 this.containerElement.addEventListener("wheel", wheelEvent => {
58 wheelEvent.preventDefault();
59 const scaleFactor = this.scaleFactor - Math.sign(wheelEvent.deltaY) * this.scrollZoomSpeed;
60 this.zoom(wheelEvent.offsetX, wheelEvent.offsetY, scaleFactor);
61 });
62
63 const pinchListener = new Hammer(this.containerElement);
64 pinchListener.get("pinch").set({ enable: true });
65 this.installPinchHandler(pinchListener);
66 }
67
68 private installPinchHandler(pinchListener: HammerManager) {
69 let startScaleFactor = 0.0;
70
71 pinchListener.on("pinchstart", (pinchEvent: HammerInput) => {
72 startScaleFactor = this.scaleFactor;
73 });
74
75 pinchListener.on("pinchmove", (pinchEvent: HammerInput) => {
76 const focusX = pinchEvent.center.x + this.containerElement.scrollLeft;
77 const focusY = pinchEvent.center.y + this.containerElement.scrollTop;
78 const scaleFactor = pinchEvent.scale * startScaleFactor;
79 this.zoom(focusX, focusY, scaleFactor);
80 });
81 }
82
83 /**
84 * Returns the picture resolution as it should be displayed.
85 */
86 private getDisplayResolution(): Gallery.Resolution {
87 return {
88 width: this.pictureProperties.resolution.width * this.scaleFactor,
89 height: this.pictureProperties.resolution.height * this.scaleFactor,
90 };
91 }
92
93 /**
94 * Applies scaling to the DOM image element.
95 * To call after internal intermediate computations because DOM properties aren't stable.
96 */
97 private resizeImageElement() {
98 const imageDim = this.getDisplayResolution();
99 this.imageElement.width = imageDim.width;
100 this.imageElement.height = imageDim.height;
101 }
102
103 /**
104 * Centers the image element inside its container if it fits, or stick to the top and left borders otherwise.
105 * It's depressingly hard to do in pure CSS…
106 */
107 private recenterImageElement() {
108 const imageDim = this.getDisplayResolution();
109 const marginLeft = Math.max((this.containerElement.clientWidth - imageDim.width) / 2, 0);
110 const marginTop = Math.max((this.containerElement.clientHeight - imageDim.height) / 2, 0);
111 this.imageElement.style.marginLeft = `${marginLeft}px`;
112 this.imageElement.style.marginTop = `${marginTop}px`;
113 }
114
115 private zoom(focusX: number, focusY: number, scaleFactor: number) {
116 const imageDim = this.getDisplayResolution();
117 const ratioX = focusX / imageDim.width;
118 const ratioY = focusY / imageDim.height;
119 this.updateImageScale(Math.min(scaleFactor, this.maxScaleFactor));
120
121 const newImageDim = this.getDisplayResolution();
122 this.containerElement.scrollLeft -= focusX - ratioX * newImageDim.width;
123 this.containerElement.scrollTop -= focusY - ratioY * newImageDim.height;
124 }
125
126 private updateImageScale(newScaleFactor: number) {
127 const horizontalFillRatio = this.containerElement.clientWidth / this.pictureProperties.resolution.width;
128 const verticalFillRatio = this.containerElement.clientHeight / this.pictureProperties.resolution.height;
129 const minScaleFactor = Math.min(horizontalFillRatio, verticalFillRatio, 1.0);
130 this.scaleFactor = Math.max(newScaleFactor, minScaleFactor);
131 this.resizeImageElement();
132 this.recenterImageElement();
133 }
134}