aboutsummaryrefslogtreecommitdiff
path: root/viewer/src
diff options
context:
space:
mode:
authorpacien2020-04-26 06:02:33 +0200
committerpacien2020-04-26 17:49:05 +0200
commitb86d96f2ed5dd4f17b047e8aba22512400484bb3 (patch)
tree57f64f8d296e1c9f8697a6e4af8d7669287d714c /viewer/src
parentadd23cc6e83a677f8e747348a2429050efe8c515 (diff)
downloadldgallery-b86d96f2ed5dd4f17b047e8aba22512400484bb3.tar.gz
viewer/LdPicture: implement mousewheel zoom
GitHub: closes #153
Diffstat (limited to 'viewer/src')
-rw-r--r--viewer/src/components/LdPicture.vue61
-rw-r--r--viewer/src/services/ldzoom.ts92
2 files changed, 123 insertions, 30 deletions
diff --git a/viewer/src/components/LdPicture.vue b/viewer/src/components/LdPicture.vue
index 3170c81..622165d 100644
--- a/viewer/src/components/LdPicture.vue
+++ b/viewer/src/components/LdPicture.vue
@@ -19,31 +19,37 @@
19 19
20<template> 20<template>
21 <div 21 <div
22 ref="containerElement"
22 v-dragscroll 23 v-dragscroll
23 class="scrollbar" 24 class="scrollbar ld-picture-container"
24 :class="{'fit-to-screen': !$uiStore.fullscreen, 'original-size': $uiStore.fullscreen}"
25 @click.capture="e => dragScrollClickFix.onClickCapture(e)" 25 @click.capture="e => dragScrollClickFix.onClickCapture(e)"
26 @click="$uiStore.toggleFullscreen()" 26 @dblclick="$uiStore.toggleFullscreen()"
27 @dragstart.prevent
27 @dragscrollstart="dragScrollClickFix.onDragScrollStart()" 28 @dragscrollstart="dragScrollClickFix.onDragScrollStart()"
28 @dragscrollend="dragScrollClickFix.onDragScrollEnd()" 29 @dragscrollend="dragScrollClickFix.onDragScrollEnd()"
29 > 30 >
30 <v-lazy-image 31 <v-lazy-image
32 ref="imageElement"
31 :src="pictureSrc(picture.properties.resource)" 33 :src="pictureSrc(picture.properties.resource)"
34 class="ld-picture-element"
32 :class="{'slow-loading': Boolean(slowLoadingStyle)}" 35 :class="{'slow-loading': Boolean(slowLoadingStyle)}"
33 :style="slowLoadingStyle" 36 :style="slowLoadingStyle"
34 @load="clearSlowLoading" 37 @load="lazyImageLoaded"
35 /> 38 />
36 <b-loading :active="loader" :is-full-page="false" class="ld-picture-loader" /> 39 <b-loading :active="loader" :is-full-page="false" class="ld-picture-loader" />
37 </div> 40 </div>
38</template> 41</template>
39 42
40<script lang="ts"> 43<script lang="ts">
41import { Component, Vue, Prop } from "vue-property-decorator"; 44import { Component, Vue, Prop, Ref } from "vue-property-decorator";
45import LdZoom from "@/services/ldzoom";
42import DragScrollClickFix from "@/services/dragscrollclickfix"; 46import DragScrollClickFix from "@/services/dragscrollclickfix";
43 47
44@Component 48@Component
45export default class LdPicture extends Vue { 49export default class LdPicture extends Vue {
46 @Prop({ required: true }) readonly picture!: Gallery.Picture; 50 @Prop({ required: true }) readonly picture!: Gallery.Picture;
51 @Ref() readonly containerElement!: HTMLDivElement;
52 @Ref() readonly imageElement!: any; // FIXME: no typedef for v-lazy-image
47 53
48 readonly SLOW_LOADING_TIMEOUT_MS: number = 1500; 54 readonly SLOW_LOADING_TIMEOUT_MS: number = 1500;
49 readonly dragScrollClickFix = new DragScrollClickFix(); 55 readonly dragScrollClickFix = new DragScrollClickFix();
@@ -60,6 +66,11 @@ export default class LdPicture extends Vue {
60 this.clearSlowLoading(); 66 this.clearSlowLoading();
61 } 67 }
62 68
69 lazyImageLoaded() {
70 this.clearSlowLoading();
71 new LdZoom(this.containerElement, this.imageElement.$el, 10, 1 / 5).install();
72 }
73
63 clearSlowLoading() { 74 clearSlowLoading() {
64 if (this.timer) clearTimeout(this.timer); 75 if (this.timer) clearTimeout(this.timer);
65 this.timer = null; 76 this.timer = null;
@@ -85,13 +96,17 @@ export default class LdPicture extends Vue {
85<style lang="scss"> 96<style lang="scss">
86@import "~@/assets/scss/theme.scss"; 97@import "~@/assets/scss/theme.scss";
87 98
88.ld-picture-loader { 99.ld-picture-container {
89 position: relative; 100 height: 100%;
90 & .loading-background { 101}
91 background: none !important; 102
92 } 103.ld-picture-element {
104 max-width: unset;
105 max-height: unset;
106 cursor: grab;
93} 107}
94img.slow-loading { 108
109.slow-loading {
95 background-repeat: no-repeat; 110 background-repeat: no-repeat;
96 background-position: center; 111 background-position: center;
97 background-size: contain; 112 background-size: contain;
@@ -99,25 +114,11 @@ img.slow-loading {
99 background-blend-mode: soft-light; 114 background-blend-mode: soft-light;
100 opacity: 1 !important; 115 opacity: 1 !important;
101} 116}
102.fit-to-screen { 117
103 display: flex; 118.ld-picture-loader {
104 justify-content: space-around; 119 position: relative;
105 height: 100%; 120 & .loading-background {
106 & > img { 121 background: none !important;
107 object-fit: contain;
108 cursor: zoom-in;
109 }
110}
111.original-size {
112 display: block;
113 text-align: center;
114 cursor: grab;
115 height: 100%;
116 & > img {
117 max-width: unset;
118 max-height: unset;
119 object-fit: none;
120 cursor: zoom-out;
121 } 122 }
122} 123}
123</style> 124</style>
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 @@
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';
22
23/**
24 * Mousewheel picture zoom helper.
25 */
26export default class LdZoom {
27 readonly containerElement: HTMLDivElement;
28 readonly imageElement: HTMLImageElement;
29 readonly maxScaleFactor: number;
30 readonly zoomSpeed: number;
31 scaleFactor: number;
32
33 constructor(
34 containerElement: HTMLDivElement, imageElement: HTMLImageElement,
35 maxScaleFactor: number, zoomSpeed: number
36 ) {
37 this.containerElement = containerElement;
38 this.imageElement = imageElement;
39 this.maxScaleFactor = maxScaleFactor;
40 this.zoomSpeed = zoomSpeed;
41 this.scaleFactor = imageElement.clientWidth / imageElement.naturalWidth;
42 }
43
44 public install() {
45 new ResizeObserver(() => {
46 this.setImageScale(this.scaleFactor);
47 this.recenterImageElement();
48 }).observe(this.containerElement);
49
50 this.containerElement.addEventListener('wheel', wheelEvent => {
51 wheelEvent.preventDefault();
52 this.zoom(wheelEvent);
53 });
54
55 // TODO: handle pinch-to-zoom.
56
57 this.recenterImageElement();
58 }
59
60 /**
61 * Centers the image element inside its container if it fits, or stick to the top and left borders otherwise.
62 * It's depressingly hard to do in pure CSS…
63 */
64 private recenterImageElement() {
65 const marginLeft = Math.max((this.containerElement.clientWidth - this.imageElement.clientWidth) / 2, 0);
66 const marginTop = Math.max((this.containerElement.clientHeight - this.imageElement.clientHeight) / 2, 0);
67 this.imageElement.style.marginLeft = `${marginLeft}px`;
68 this.imageElement.style.marginTop = `${marginTop}px`;
69 }
70
71 private zoom(wheelEvent: WheelEvent) {
72 const ratioX = wheelEvent.offsetX / this.imageElement.clientWidth;
73 const ratioY = wheelEvent.offsetY / this.imageElement.clientHeight;
74
75 const zoomDelta = -Math.sign(wheelEvent.deltaY) * this.zoomSpeed;
76 this.setImageScale(Math.min(this.scaleFactor + zoomDelta, this.maxScaleFactor));
77
78 this.containerElement.scrollLeft -= wheelEvent.offsetX - ratioX * this.imageElement.clientWidth;
79 this.containerElement.scrollTop -= wheelEvent.offsetY - ratioY * this.imageElement.clientHeight;
80 }
81
82 private setImageScale(newScaleFactor: number) {
83 const horizontalFillRatio = this.containerElement.clientWidth / this.imageElement.naturalWidth;
84 const verticalFillRatio = this.containerElement.clientHeight / this.imageElement.naturalHeight;
85 const minScaleFactor = Math.min(horizontalFillRatio, verticalFillRatio, 1.0);
86 this.scaleFactor = Math.max(newScaleFactor, minScaleFactor);
87
88 this.imageElement.width = this.scaleFactor * this.imageElement.naturalWidth;
89 this.imageElement.height = this.scaleFactor * this.imageElement.naturalHeight;
90 this.recenterImageElement();
91 }
92}