From b86d96f2ed5dd4f17b047e8aba22512400484bb3 Mon Sep 17 00:00:00 2001 From: pacien Date: Sun, 26 Apr 2020 06:02:33 +0200 Subject: viewer/LdPicture: implement mousewheel zoom GitHub: closes #153 --- viewer/src/services/ldzoom.ts | 92 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 viewer/src/services/ldzoom.ts (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts new file mode 100644 index 0000000..f001805 --- /dev/null +++ b/viewer/src/services/ldzoom.ts @@ -0,0 +1,92 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2020 Pacien TRAN-GIRARD +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +*/ + +// polyfill still required for IE and Safari, see https://caniuse.com/#feat=resizeobserver +import ResizeObserver from 'resize-observer-polyfill'; + +/** + * Mousewheel picture zoom helper. + */ +export default class LdZoom { + readonly containerElement: HTMLDivElement; + readonly imageElement: HTMLImageElement; + readonly maxScaleFactor: number; + readonly zoomSpeed: number; + scaleFactor: number; + + constructor( + containerElement: HTMLDivElement, imageElement: HTMLImageElement, + maxScaleFactor: number, zoomSpeed: number + ) { + this.containerElement = containerElement; + this.imageElement = imageElement; + this.maxScaleFactor = maxScaleFactor; + this.zoomSpeed = zoomSpeed; + this.scaleFactor = imageElement.clientWidth / imageElement.naturalWidth; + } + + public install() { + new ResizeObserver(() => { + this.setImageScale(this.scaleFactor); + this.recenterImageElement(); + }).observe(this.containerElement); + + this.containerElement.addEventListener('wheel', wheelEvent => { + wheelEvent.preventDefault(); + this.zoom(wheelEvent); + }); + + // TODO: handle pinch-to-zoom. + + this.recenterImageElement(); + } + + /** + * Centers the image element inside its container if it fits, or stick to the top and left borders otherwise. + * It's depressingly hard to do in pure CSS… + */ + private recenterImageElement() { + const marginLeft = Math.max((this.containerElement.clientWidth - this.imageElement.clientWidth) / 2, 0); + const marginTop = Math.max((this.containerElement.clientHeight - this.imageElement.clientHeight) / 2, 0); + this.imageElement.style.marginLeft = `${marginLeft}px`; + this.imageElement.style.marginTop = `${marginTop}px`; + } + + private zoom(wheelEvent: WheelEvent) { + const ratioX = wheelEvent.offsetX / this.imageElement.clientWidth; + const ratioY = wheelEvent.offsetY / this.imageElement.clientHeight; + + const zoomDelta = -Math.sign(wheelEvent.deltaY) * this.zoomSpeed; + this.setImageScale(Math.min(this.scaleFactor + zoomDelta, this.maxScaleFactor)); + + this.containerElement.scrollLeft -= wheelEvent.offsetX - ratioX * this.imageElement.clientWidth; + this.containerElement.scrollTop -= wheelEvent.offsetY - ratioY * this.imageElement.clientHeight; + } + + private setImageScale(newScaleFactor: number) { + const horizontalFillRatio = this.containerElement.clientWidth / this.imageElement.naturalWidth; + const verticalFillRatio = this.containerElement.clientHeight / this.imageElement.naturalHeight; + const minScaleFactor = Math.min(horizontalFillRatio, verticalFillRatio, 1.0); + this.scaleFactor = Math.max(newScaleFactor, minScaleFactor); + + this.imageElement.width = this.scaleFactor * this.imageElement.naturalWidth; + this.imageElement.height = this.scaleFactor * this.imageElement.naturalHeight; + this.recenterImageElement(); + } +} -- cgit v1.2.3 From f8a1763c3bee0e236c86ba9f6b46aceb212dea10 Mon Sep 17 00:00:00 2001 From: pacien Date: Sun, 26 Apr 2020 20:01:46 +0200 Subject: viewer/LdPicture: initially fit image in viewport --- viewer/src/services/ldzoom.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index f001805..61b5dc6 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -28,7 +28,7 @@ export default class LdZoom { readonly imageElement: HTMLImageElement; readonly maxScaleFactor: number; readonly zoomSpeed: number; - scaleFactor: number; + scaleFactor: number = 0.0; constructor( containerElement: HTMLDivElement, imageElement: HTMLImageElement, @@ -38,9 +38,12 @@ export default class LdZoom { this.imageElement = imageElement; this.maxScaleFactor = maxScaleFactor; this.zoomSpeed = zoomSpeed; - this.scaleFactor = imageElement.clientWidth / imageElement.naturalWidth; } + /** + * Register event listeners. + * The dimension of the image should be known before calling this method. + */ public install() { new ResizeObserver(() => { this.setImageScale(this.scaleFactor); @@ -53,8 +56,6 @@ export default class LdZoom { }); // TODO: handle pinch-to-zoom. - - this.recenterImageElement(); } /** -- cgit v1.2.3 From 29d432e64e0482935ef91dbfed37d4d4cf26c42f Mon Sep 17 00:00:00 2001 From: pacien Date: Sun, 26 Apr 2020 21:51:37 +0200 Subject: viewer/LdZoom: add support for pinch-to-zoom GitHub: closes #106 --- viewer/src/services/ldzoom.ts | 46 ++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 14 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index 61b5dc6..27debb5 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -19,25 +19,26 @@ // polyfill still required for IE and Safari, see https://caniuse.com/#feat=resizeobserver import ResizeObserver from 'resize-observer-polyfill'; +import "hammerjs"; /** - * Mousewheel picture zoom helper. + * Mousewheel and pinch zoom handler. */ export default class LdZoom { readonly containerElement: HTMLDivElement; readonly imageElement: HTMLImageElement; readonly maxScaleFactor: number; - readonly zoomSpeed: number; + readonly scrollZoomSpeed: number; scaleFactor: number = 0.0; constructor( containerElement: HTMLDivElement, imageElement: HTMLImageElement, - maxScaleFactor: number, zoomSpeed: number + maxScaleFactor: number, scrollZoomSpeed: number ) { this.containerElement = containerElement; this.imageElement = imageElement; this.maxScaleFactor = maxScaleFactor; - this.zoomSpeed = zoomSpeed; + this.scrollZoomSpeed = scrollZoomSpeed; } /** @@ -52,10 +53,30 @@ export default class LdZoom { this.containerElement.addEventListener('wheel', wheelEvent => { wheelEvent.preventDefault(); - this.zoom(wheelEvent); + const zoomDelta = -Math.sign(wheelEvent.deltaY) * this.scrollZoomSpeed; + this.zoom(wheelEvent.offsetX, wheelEvent.offsetY, zoomDelta); }); - // TODO: handle pinch-to-zoom. + const pinchListener = new Hammer(this.containerElement); + pinchListener.get('pinch').set({enable: true}); + this.installPinchHandler(pinchListener); + } + + private installPinchHandler(pinchListener: HammerManager) { + let lastScaleFactor = 0.0; + + pinchListener.on('pinchstart', (pinchEvent: HammerInput) => { + lastScaleFactor = pinchEvent.scale; + }); + + pinchListener.on('pinchmove', (pinchEvent: HammerInput) => { + // FIXME: pinchEvent.center isn't always well-centered + const focusX = pinchEvent.center.x + this.containerElement.scrollLeft; + const focusY = pinchEvent.center.y + this.containerElement.scrollTop; + const zoomDelta = pinchEvent.scale - lastScaleFactor; + lastScaleFactor = pinchEvent.scale; + this.zoom(focusX, focusY, zoomDelta); + }); } /** @@ -69,15 +90,12 @@ export default class LdZoom { this.imageElement.style.marginTop = `${marginTop}px`; } - private zoom(wheelEvent: WheelEvent) { - const ratioX = wheelEvent.offsetX / this.imageElement.clientWidth; - const ratioY = wheelEvent.offsetY / this.imageElement.clientHeight; - - const zoomDelta = -Math.sign(wheelEvent.deltaY) * this.zoomSpeed; + private zoom(focusX: number, focusY: number, zoomDelta: number) { + const ratioX = focusX / this.imageElement.clientWidth; + const ratioY = focusY / this.imageElement.clientHeight; this.setImageScale(Math.min(this.scaleFactor + zoomDelta, this.maxScaleFactor)); - - this.containerElement.scrollLeft -= wheelEvent.offsetX - ratioX * this.imageElement.clientWidth; - this.containerElement.scrollTop -= wheelEvent.offsetY - ratioY * this.imageElement.clientHeight; + this.containerElement.scrollLeft -= focusX - ratioX * this.imageElement.clientWidth; + this.containerElement.scrollTop -= focusY - ratioY * this.imageElement.clientHeight; } private setImageScale(newScaleFactor: number) { -- cgit v1.2.3 From 058d6b945c06b6daa2824ac837fc6ec003bc9fa6 Mon Sep 17 00:00:00 2001 From: pacien Date: Mon, 27 Apr 2020 22:20:47 +0200 Subject: viewer/LdZoom: properly scale with pinch-to-zoom Previous scaling was too aggressive with large pictures. --- viewer/src/services/ldzoom.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index 27debb5..a494211 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -53,8 +53,8 @@ export default class LdZoom { this.containerElement.addEventListener('wheel', wheelEvent => { wheelEvent.preventDefault(); - const zoomDelta = -Math.sign(wheelEvent.deltaY) * this.scrollZoomSpeed; - this.zoom(wheelEvent.offsetX, wheelEvent.offsetY, zoomDelta); + const scaleFactor = this.scaleFactor - Math.sign(wheelEvent.deltaY) * this.scrollZoomSpeed; + this.zoom(wheelEvent.offsetX, wheelEvent.offsetY, scaleFactor); }); const pinchListener = new Hammer(this.containerElement); @@ -63,19 +63,18 @@ export default class LdZoom { } private installPinchHandler(pinchListener: HammerManager) { - let lastScaleFactor = 0.0; + let startScaleFactor = 0.0; pinchListener.on('pinchstart', (pinchEvent: HammerInput) => { - lastScaleFactor = pinchEvent.scale; + startScaleFactor = this.scaleFactor; }); pinchListener.on('pinchmove', (pinchEvent: HammerInput) => { - // FIXME: pinchEvent.center isn't always well-centered + // FIXME: v-dragscroll interferes with our focus point scroll adjustment const focusX = pinchEvent.center.x + this.containerElement.scrollLeft; const focusY = pinchEvent.center.y + this.containerElement.scrollTop; - const zoomDelta = pinchEvent.scale - lastScaleFactor; - lastScaleFactor = pinchEvent.scale; - this.zoom(focusX, focusY, zoomDelta); + const scaleFactor = pinchEvent.scale * startScaleFactor; + this.zoom(focusX, focusY, scaleFactor); }); } @@ -90,10 +89,10 @@ export default class LdZoom { this.imageElement.style.marginTop = `${marginTop}px`; } - private zoom(focusX: number, focusY: number, zoomDelta: number) { + private zoom(focusX: number, focusY: number, scaleFactor: number) { const ratioX = focusX / this.imageElement.clientWidth; const ratioY = focusY / this.imageElement.clientHeight; - this.setImageScale(Math.min(this.scaleFactor + zoomDelta, this.maxScaleFactor)); + this.setImageScale(Math.min(scaleFactor, this.maxScaleFactor)); this.containerElement.scrollLeft -= focusX - ratioX * this.imageElement.clientWidth; this.containerElement.scrollTop -= focusY - ratioY * this.imageElement.clientHeight; } -- cgit v1.2.3 From bda84785d3d04c0d1d471a1cf9c38363541b64a8 Mon Sep 17 00:00:00 2001 From: pacien Date: Mon, 27 Apr 2020 22:26:56 +0200 Subject: viewer/LdZoom: set image scale on init To prevent some init glitch from happening. --- viewer/src/services/ldzoom.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index a494211..50f006e 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -60,6 +60,8 @@ export default class LdZoom { const pinchListener = new Hammer(this.containerElement); pinchListener.get('pinch').set({enable: true}); this.installPinchHandler(pinchListener); + + this.setImageScale(this.scaleFactor); } private installPinchHandler(pinchListener: HammerManager) { -- cgit v1.2.3 From b4b698ccbdec98dd902b6290f12207bf5547b140 Mon Sep 17 00:00:00 2001 From: pacien Date: Tue, 28 Apr 2020 01:51:08 +0200 Subject: viewer/LdPicture: fix centering in loading phase --- viewer/src/services/ldzoom.ts | 60 +++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 19 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index 50f006e..c28c2c8 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -27,28 +27,31 @@ import "hammerjs"; export default class LdZoom { readonly containerElement: HTMLDivElement; readonly imageElement: HTMLImageElement; + readonly pictureProperties: Gallery.PictureProperties; readonly maxScaleFactor: number; readonly scrollZoomSpeed: number; scaleFactor: number = 0.0; constructor( containerElement: HTMLDivElement, imageElement: HTMLImageElement, + pictureProperties: Gallery.PictureProperties, maxScaleFactor: number, scrollZoomSpeed: number ) { this.containerElement = containerElement; this.imageElement = imageElement; + this.pictureProperties = pictureProperties; this.maxScaleFactor = maxScaleFactor; this.scrollZoomSpeed = scrollZoomSpeed; } /** * Register event listeners. - * The dimension of the image should be known before calling this method. */ public install() { + this.updateImageScale(this.scaleFactor); + new ResizeObserver(() => { - this.setImageScale(this.scaleFactor); - this.recenterImageElement(); + this.updateImageScale(this.scaleFactor); }).observe(this.containerElement); this.containerElement.addEventListener('wheel', wheelEvent => { @@ -60,8 +63,6 @@ export default class LdZoom { const pinchListener = new Hammer(this.containerElement); pinchListener.get('pinch').set({enable: true}); this.installPinchHandler(pinchListener); - - this.setImageScale(this.scaleFactor); } private installPinchHandler(pinchListener: HammerManager) { @@ -72,7 +73,6 @@ export default class LdZoom { }); pinchListener.on('pinchmove', (pinchEvent: HammerInput) => { - // FIXME: v-dragscroll interferes with our focus point scroll adjustment const focusX = pinchEvent.center.x + this.containerElement.scrollLeft; const focusY = pinchEvent.center.y + this.containerElement.scrollTop; const scaleFactor = pinchEvent.scale * startScaleFactor; @@ -80,33 +80,55 @@ export default class LdZoom { }); } + /** + * Returns the picture resolution as it should be displayed. + */ + private getDisplayResolution(): Gallery.Resolution { + return { + width: this.pictureProperties.resolution.width * this.scaleFactor, + height: this.pictureProperties.resolution.height * this.scaleFactor, + }; + } + + /** + * Applies scaling to the DOM image element. + * To call after internal intermediate computations because DOM properties aren't stable. + */ + private resizeImageElement() { + const imageDim = this.getDisplayResolution(); + this.imageElement.width = imageDim.width; + this.imageElement.height = imageDim.height; + } + /** * Centers the image element inside its container if it fits, or stick to the top and left borders otherwise. * It's depressingly hard to do in pure CSS… */ private recenterImageElement() { - const marginLeft = Math.max((this.containerElement.clientWidth - this.imageElement.clientWidth) / 2, 0); - const marginTop = Math.max((this.containerElement.clientHeight - this.imageElement.clientHeight) / 2, 0); + const imageDim = this.getDisplayResolution(); + const marginLeft = Math.max((this.containerElement.clientWidth - imageDim.width) / 2, 0); + const marginTop = Math.max((this.containerElement.clientHeight - imageDim.height) / 2, 0); this.imageElement.style.marginLeft = `${marginLeft}px`; this.imageElement.style.marginTop = `${marginTop}px`; } private zoom(focusX: number, focusY: number, scaleFactor: number) { - const ratioX = focusX / this.imageElement.clientWidth; - const ratioY = focusY / this.imageElement.clientHeight; - this.setImageScale(Math.min(scaleFactor, this.maxScaleFactor)); - this.containerElement.scrollLeft -= focusX - ratioX * this.imageElement.clientWidth; - this.containerElement.scrollTop -= focusY - ratioY * this.imageElement.clientHeight; + const imageDim = this.getDisplayResolution(); + const ratioX = focusX / imageDim.width; + const ratioY = focusY / imageDim.height; + this.updateImageScale(Math.min(scaleFactor, this.maxScaleFactor)); + + const newImageDim = this.getDisplayResolution(); + this.containerElement.scrollLeft -= focusX - ratioX * newImageDim.width; + this.containerElement.scrollTop -= focusY - ratioY * newImageDim.height; } - private setImageScale(newScaleFactor: number) { - const horizontalFillRatio = this.containerElement.clientWidth / this.imageElement.naturalWidth; - const verticalFillRatio = this.containerElement.clientHeight / this.imageElement.naturalHeight; + private updateImageScale(newScaleFactor: number) { + const horizontalFillRatio = this.containerElement.clientWidth / this.pictureProperties.resolution.width; + const verticalFillRatio = this.containerElement.clientHeight / this.pictureProperties.resolution.height; const minScaleFactor = Math.min(horizontalFillRatio, verticalFillRatio, 1.0); this.scaleFactor = Math.max(newScaleFactor, minScaleFactor); - - this.imageElement.width = this.scaleFactor * this.imageElement.naturalWidth; - this.imageElement.height = this.scaleFactor * this.imageElement.naturalHeight; + this.resizeImageElement(); this.recenterImageElement(); } } -- cgit v1.2.3