From 61509d1b1e2e276364600f668a584aa9516573ea Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 13:39:56 -0400 Subject: [PATCH 01/18] Make media modal be aware of multi-touch actions Originally the media viewer would think every touch is a swipe (one-finger touch event), so we would encounter the case where a two-finger scale event would incorrectly change the current media. This is now fixed. --- src/components/media_modal/media_modal.js | 25 +++--- src/components/media_modal/media_modal.vue | 1 + .../gesture_service/gesture_service.js | 83 ++++++++++++++++++- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index e7384c93..795da733 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -41,25 +41,22 @@ const MediaModal = { } }, created () { - this.mediaSwipeGestureRight = GestureService.swipeGesture( - GestureService.DIRECTION_RIGHT, - this.goPrev, - 50 - ) - this.mediaSwipeGestureLeft = GestureService.swipeGesture( - GestureService.DIRECTION_LEFT, - this.goNext, - 50 - ) + this.mediaGesture = new GestureService.SwipeAndScaleGesture({ + direction: GestureService.DIRECTION_LEFT, + callbackPositive: this.goNext, + callbackNegative: this.goPrev, + threshold: 50 + }) }, methods: { mediaTouchStart (e) { - GestureService.beginSwipe(e, this.mediaSwipeGestureRight) - GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) + this.mediaGesture.start(e) }, mediaTouchMove (e) { - GestureService.updateSwipe(e, this.mediaSwipeGestureRight) - GestureService.updateSwipe(e, this.mediaSwipeGestureLeft) + this.mediaGesture.move(e) + }, + mediaTouchEnd (e) { + this.mediaGesture.end(e) }, hide () { this.$store.dispatch('closeMediaViewer') diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 54bc5335..b1922ffb 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -12,6 +12,7 @@ :title="currentMedia.description" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" + @touchend.stop="mediaTouchEnd" @click="hide" > (e.touches.length === 1) +const isSwipeEventEnd = e => (e.changedTouches.length === 1) + +const isScaleEvent = e => (e.targetTouches.length === 2) +// const isScaleEventEnd = e => (e.changedTouches.length === 2) + const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY]) +const touchCoord = touch => [touch.screenX, touch.screenY] + +const touchEventCoord = e => touchCoord(e.touches[0]) const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) @@ -61,6 +69,76 @@ const updateSwipe = (event, gesture) => { gesture._swiping = false } +class SwipeAndScaleGesture { + constructor ({ + direction, callbackPositive, callbackNegative, + previewCallback, threshold = 30, perpendicularTolerance = 1.0 + }) { + this.direction = direction + this.previewCallback = previewCallback + this.callbackPositive = callbackPositive + this.callbackNegative = callbackNegative + this.threshold = threshold + this.perpendicularTolerance = perpendicularTolerance + this._startPos = [0, 0] + this._swiping = false + } + + start (event) { + console.log('start() called', event) + if (isSwipeEvent(event)) { + this._startPos = touchEventCoord(event) + console.log('start pos:', this._startPos) + this._swiping = true + } else if (isScaleEvent(event)) { + this._scalePoints = [...event.targetTouches] + this._swiping = false + } + } + + move (event) { + if (isScaleEvent(event)) { + } + } + + end (event) { + console.log('end() called', event) + if (!isSwipeEventEnd(event)) { + console.log('not swipe event') + return + } + if (!this._swiping) { + console.log('not swiping') + return + } + this.swiping = false + + console.log('is swipe event') + + // movement too small + const touch = event.changedTouches[0] + const delta = deltaCoord(this._startPos, touchCoord(touch)) + if (vectorLength(delta) < this.threshold) return + // movement is opposite from direction + const isPositive = dotProduct(delta, this.direction) > 0 + + // movement perpendicular to direction is too much + const towardsDir = project(delta, this.direction) + const perpendicularDir = perpendicular(this.direction) + const towardsPerpendicular = project(delta, perpendicularDir) + if ( + vectorLength(towardsDir) * this.perpendicularTolerance < + vectorLength(towardsPerpendicular) + ) return + + if (isPositive) { + this.callbackPositive() + } else { + this.callbackNegative() + } + } +} + const GestureService = { DIRECTION_LEFT, DIRECTION_RIGHT, @@ -68,7 +146,8 @@ const GestureService = { DIRECTION_DOWN, swipeGesture, beginSwipe, - updateSwipe + updateSwipe, + SwipeAndScaleGesture } export default GestureService From aa70c319505ba46678c0f2b12bac5864b7a62d0f Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 19:46:27 -0400 Subject: [PATCH 02/18] Preview swipe action --- src/components/media_modal/media_modal.js | 26 +++++++ src/components/media_modal/media_modal.vue | 1 + src/modules/media_viewer.js | 66 +++++++++++++++++- .../gesture_service/gesture_service.js | 69 +++++++++++++------ 4 files changed, 137 insertions(+), 25 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 795da733..11b368f7 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -3,6 +3,7 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' +import Vuex from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, @@ -14,6 +15,8 @@ library.add( faChevronRight ) +const onlyXAxis = ([x, y]) => [x, 0] + const MediaModal = { components: { StillImage, @@ -38,6 +41,15 @@ const MediaModal = { }, type () { return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null + }, + scaling () { + return this.$store.state.mediaViewer.swipeScaler.scaling + }, + offsets () { + return this.$store.state.mediaViewer.swipeScaler.offsets + }, + transform () { + return `scale(${this.scaling}, ${this.scaling}) translate(${this.offsets[0]}px, ${this.offsets[1]}px)` } }, created () { @@ -45,6 +57,8 @@ const MediaModal = { direction: GestureService.DIRECTION_LEFT, callbackPositive: this.goNext, callbackNegative: this.goPrev, + swipePreviewCallback: this.handleSwipePreview, + swipeEndCallback: this.handleSwipeEnd, threshold: 50 }) }, @@ -73,6 +87,18 @@ const MediaModal = { this.$store.dispatch('setCurrent', this.media[nextIndex]) } }, + handleSwipePreview (offsets) { + this.$store.dispatch('swipeScaler/apply', { offsets: onlyXAxis(offsets) }) + }, + handleSwipeEnd (sign) { + if (sign === 0) { + this.$store.dispatch('swipeScaler/revert') + } else if (sign > 0) { + this.goNext() + } else { + this.goPrev() + } + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index b1922ffb..6b2e4abc 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -10,6 +10,7 @@ :src="currentMedia.url" :alt="currentMedia.description" :title="currentMedia.description" + :style="{ transform }" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" @touchend.stop="mediaTouchEnd" diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js index 721c25e6..7aa479c8 100644 --- a/src/modules/media_viewer.js +++ b/src/modules/media_viewer.js @@ -19,19 +19,79 @@ const mediaViewer = { } }, actions: { - setMedia ({ commit }, attachments) { + setMedia ({ commit, dispatch }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) return type === 'image' || type === 'video' || type === 'audio' }) commit('setMedia', media) + dispatch('swipeScaler/reset') }, - setCurrent ({ commit, state }, current) { + setCurrent ({ commit, state, dispatch }, current) { const index = state.media.indexOf(current) commit('setCurrent', index || 0) + dispatch('swipeScaler/reset') }, - closeMediaViewer ({ commit }) { + closeMediaViewer ({ commit, dispatch }) { commit('close') + dispatch('swipeScaler/reset') + } + }, + modules: { + swipeScaler: { + namespaced: true, + + state: { + origOffsets: [0, 0], + offsets: [0, 0], + origScaling: 1, + scaling: 1 + }, + + mutations: { + reset (state) { + state.origOffsets = [0, 0] + state.offsets = [0, 0] + state.origScaling = 1 + state.scaling = 1 + }, + applyOffsets (state, { offsets }) { + state.offsets = state.origOffsets.map((k, n) => k + offsets[n]) + }, + applyScaling (state, { scaling }) { + state.scaling = state.origScaling * scaling + }, + finishOffsets (state) { + state.origOffsets = [...state.offsets] + }, + finishScaling (state) { + state.origScaling = state.scaling + }, + revertOffsets (state) { + state.offsets = [...state.origOffsets] + }, + revertScaling (state) { + state.scaling = state.origScaling + } + }, + + actions: { + reset ({ commit }) { + commit('reset') + }, + apply ({ commit }, { offsets, scaling = 1 }) { + commit('applyOffsets', { offsets }) + commit('applyScaling', { scaling }) + }, + finish ({ commit }) { + commit('finishOffsets') + commit('finishScaling') + }, + revert ({ commit }) { + commit('revertOffsets') + commit('revertScaling') + } + } } } } diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 8df4f03d..8f406762 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -70,14 +70,28 @@ const updateSwipe = (event, gesture) => { } class SwipeAndScaleGesture { + // swipePreviewCallback(offsets: Array[Number]) + // offsets: the offset vector which the underlying component should move, from the starting position + // pinchPreviewCallback(offsets: Array[Number], scaling: Number) + // offsets: the offset vector which the underlying component should move, from the starting position + // scaling: the scaling factor we should apply to the underlying component, from the starting position + // swipeEndcallback(sign: 0|-1|1) + // sign: if the swipe does not meet the threshold, 0 + // if the swipe meets the threshold in the positive direction, 1 + // if the swipe meets the threshold in the negative direction, -1 constructor ({ - direction, callbackPositive, callbackNegative, - previewCallback, threshold = 30, perpendicularTolerance = 1.0 + direction, + // swipeStartCallback, pinchStartCallback, + swipePreviewCallback, pinchPreviewCallback, + swipeEndCallback, pinchEndCallback, + threshold = 30, perpendicularTolerance = 1.0 }) { + const nop = () => {} this.direction = direction - this.previewCallback = previewCallback - this.callbackPositive = callbackPositive - this.callbackNegative = callbackNegative + this.swipePreviewCallback = swipePreviewCallback || nop + this.pinchPreviewCallback = pinchPreviewCallback || nop + this.swipeEndCallback = swipeEndCallback || nop + this.pinchEndCallback = pinchEndCallback || nop this.threshold = threshold this.perpendicularTolerance = perpendicularTolerance this._startPos = [0, 0] @@ -97,7 +111,12 @@ class SwipeAndScaleGesture { } move (event) { - if (isScaleEvent(event)) { + if (isSwipeEvent(event)) { + const touch = event.changedTouches[0] + const delta = deltaCoord(this._startPos, touchCoord(touch)) + + this.swipePreviewCallback(delta) + } else if (isScaleEvent(event)) { } } @@ -118,24 +137,30 @@ class SwipeAndScaleGesture { // movement too small const touch = event.changedTouches[0] const delta = deltaCoord(this._startPos, touchCoord(touch)) - if (vectorLength(delta) < this.threshold) return - // movement is opposite from direction - const isPositive = dotProduct(delta, this.direction) > 0 + this.swipePreviewCallback(delta) - // movement perpendicular to direction is too much - const towardsDir = project(delta, this.direction) - const perpendicularDir = perpendicular(this.direction) - const towardsPerpendicular = project(delta, perpendicularDir) - if ( - vectorLength(towardsDir) * this.perpendicularTolerance < - vectorLength(towardsPerpendicular) - ) return + const sign = (() => { + if (vectorLength(delta) < this.threshold) { + return 0 + } + // movement is opposite from direction + const isPositive = dotProduct(delta, this.direction) > 0 - if (isPositive) { - this.callbackPositive() - } else { - this.callbackNegative() - } + // movement perpendicular to direction is too much + const towardsDir = project(delta, this.direction) + const perpendicularDir = perpendicular(this.direction) + const towardsPerpendicular = project(delta, perpendicularDir) + if ( + vectorLength(towardsDir) * this.perpendicularTolerance < + vectorLength(towardsPerpendicular) + ) { + return 0 + } + + return isPositive ? 1 : -1 + })() + + this.swipeEndCallback(sign) } } From 7d767f840b47c7544167f90f4f23bc39b67cdfd4 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 21:39:07 -0400 Subject: [PATCH 03/18] Handle pinch action --- src/components/media_modal/media_modal.js | 11 ++++- src/components/media_modal/media_modal.vue | 6 +-- src/modules/media_viewer.js | 14 ++----- .../gesture_service/gesture_service.js | 42 ++++++++++++++++++- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 11b368f7..0e9f6b11 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -49,7 +49,7 @@ const MediaModal = { return this.$store.state.mediaViewer.swipeScaler.offsets }, transform () { - return `scale(${this.scaling}, ${this.scaling}) translate(${this.offsets[0]}px, ${this.offsets[1]}px)` + return `translate(${this.offsets[0]}px, ${this.offsets[1]}px) scale(${this.scaling}, ${this.scaling})` } }, created () { @@ -59,6 +59,8 @@ const MediaModal = { callbackNegative: this.goPrev, swipePreviewCallback: this.handleSwipePreview, swipeEndCallback: this.handleSwipeEnd, + pinchPreviewCallback: this.handlePinchPreview, + pinchEndCallback: this.handlePinchEnd, threshold: 50 }) }, @@ -99,6 +101,13 @@ const MediaModal = { this.goPrev() } }, + handlePinchPreview (offsets, scaling) { + console.log('handle pinch preview:', offsets, scaling) + this.$store.dispatch('swipeScaler/apply', { offsets, scaling }) + }, + handlePinchEnd () { + this.$store.dispatch('swipeScaler/finish') + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 6b2e4abc..c115fa63 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -11,9 +11,9 @@ :alt="currentMedia.description" :title="currentMedia.description" :style="{ transform }" - @touchstart.stop="mediaTouchStart" - @touchmove.stop="mediaTouchMove" - @touchend.stop="mediaTouchEnd" + @touchstart.stop.prevent="mediaTouchStart" + @touchmove.stop.prevent="mediaTouchMove" + @touchend.stop.prevent="mediaTouchEnd" @click="hide" > (e.touches.length === 1) const isSwipeEventEnd = e => (e.changedTouches.length === 1) const isScaleEvent = e => (e.targetTouches.length === 2) -// const isScaleEventEnd = e => (e.changedTouches.length === 2) +const isScaleEventEnd = e => (e.targetTouches.length === 1) const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] +const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) +const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) + +const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) + const touchCoord = touch => [touch.screenX, touch.screenY] const touchEventCoord = e => touchCoord(e.touches[0]) @@ -22,6 +29,8 @@ const perpendicular = v => [v[1], -v[0]] const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1] +// const numProduct = (num, v) => v.map(k => num * k) + const project = (v1, v2) => { const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2)) return [scalar * v2[0], scalar * v2[1]] @@ -86,7 +95,7 @@ class SwipeAndScaleGesture { swipeEndCallback, pinchEndCallback, threshold = 30, perpendicularTolerance = 1.0 }) { - const nop = () => {} + const nop = () => { console.log('Warning: Not implemented') } this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop this.pinchPreviewCallback = pinchPreviewCallback || nop @@ -95,6 +104,7 @@ class SwipeAndScaleGesture { this.threshold = threshold this.perpendicularTolerance = perpendicularTolerance this._startPos = [0, 0] + this._startDistance = DISTANCE_MIN this._swiping = false } @@ -105,23 +115,51 @@ class SwipeAndScaleGesture { console.log('start pos:', this._startPos) this._swiping = true } else if (isScaleEvent(event)) { + const coords = [...event.targetTouches].map(touchCoord) + this._startPos = avgCoord(coords) + this._startDistance = vectorLength(deltaCoord(coords[0], coords[1])) + if (this._startDistance < DISTANCE_MIN) { + this._startDistance = DISTANCE_MIN + } this._scalePoints = [...event.targetTouches] this._swiping = false + console.log( + 'is scale event, start =', this._startPos, + 'dist =', this._startDistance) } } move (event) { + // console.log('move called', event) if (isSwipeEvent(event)) { const touch = event.changedTouches[0] const delta = deltaCoord(this._startPos, touchCoord(touch)) this.swipePreviewCallback(delta) } else if (isScaleEvent(event)) { + console.log('is scale event') + const coords = [...event.targetTouches].map(touchCoord) + const curPos = avgCoord(coords) + const curDistance = vectorLength(deltaCoord(coords[0], coords[1])) + const scaling = curDistance / this._startDistance + const posDiff = vectorMinus(curPos, this._startPos) + // const delta = vectorAdd(numProduct((1 - scaling), this._startPos), posDiff) + const delta = posDiff + // console.log( + // 'is scale event, cur =', curPos, + // 'dist =', curDistance, + // 'scale =', scaling, + // 'delta =', delta) + this.pinchPreviewCallback(delta, scaling) } } end (event) { console.log('end() called', event) + if (isScaleEventEnd(event)) { + this.pinchEndCallback() + } + if (!isSwipeEventEnd(event)) { console.log('not swipe event') return From 0421c06d47e08e829a8b8f3c9013ff0fcaacce7b Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 23:42:34 -0400 Subject: [PATCH 04/18] Add pan threshold --- src/components/media_modal/media_modal.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 0e9f6b11..c53eeccd 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -16,6 +16,8 @@ library.add( ) const onlyXAxis = ([x, y]) => [x, 0] +const SCALING_RESET_MIN = 1.1 +const SCALING_ENABLE_MOVE_THRESHOLD = 1 const MediaModal = { components: { @@ -90,11 +92,17 @@ const MediaModal = { } }, handleSwipePreview (offsets) { - this.$store.dispatch('swipeScaler/apply', { offsets: onlyXAxis(offsets) }) + this.$store.dispatch('swipeScaler/apply', { + offsets: this.scaling > SCALING_ENABLE_MOVE_THRESHOLD ? offsets : onlyXAxis(offsets) + }) }, handleSwipeEnd (sign) { + if (this.scaling > SCALING_ENABLE_MOVE_THRESHOLD) { + this.$store.dispatch('swipeScaler/finish') + return + } if (sign === 0) { - this.$store.dispatch('swipeScaler/revert') + this.$store.dispatch('swipeScaler/reset') } else if (sign > 0) { this.goNext() } else { @@ -106,7 +114,11 @@ const MediaModal = { this.$store.dispatch('swipeScaler/apply', { offsets, scaling }) }, handlePinchEnd () { - this.$store.dispatch('swipeScaler/finish') + if (this.scaling > SCALING_RESET_MIN) { + this.$store.dispatch('swipeScaler/finish') + } else { + this.$store.dispatch('swipeScaler/reset') + } }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape From 0a63208da76f96820654ee7238366887feccd5a3 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 17:42:10 -0400 Subject: [PATCH 05/18] Use pinch-zoom-element for pinch zoom functionality --- package.json | 1 + src/components/media_modal/media_modal.js | 9 ++- src/components/media_modal/media_modal.vue | 64 +++++++++++++++++----- src/components/pinch_zoom/pinch_zoom.js | 6 ++ src/components/pinch_zoom/pinch_zoom.vue | 11 ++++ src/main.js | 2 + yarn.lock | 11 ++++ 7 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 src/components/pinch_zoom/pinch_zoom.js create mode 100644 src/components/pinch_zoom/pinch_zoom.vue diff --git a/package.json b/package.json index 99301266..d4d52023 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/vue-fontawesome": "^2.0.0", + "@kazvmoe-infra/pinch-zoom-element": "https://lily.kazv.moe/infra/pinch-zoom-element.git", "body-scroll-lock": "^2.6.4", "chromatism": "^3.0.0", "cropperjs": "^1.4.3", diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index c53eeccd..ca271f32 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,9 +1,9 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' +import PinchZoom from '../pinch_zoom/pinch_zoom.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' -import Vuex from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, @@ -23,6 +23,7 @@ const MediaModal = { components: { StillImage, VideoAttachment, + PinchZoom, Modal }, computed: { @@ -54,6 +55,12 @@ const MediaModal = { return `translate(${this.offsets[0]}px, ${this.offsets[1]}px) scale(${this.scaling}, ${this.scaling})` } }, + data () { + return { + pinchZoomOptions: { + } + } + }, created () { this.mediaGesture = new GestureService.SwipeAndScaleGesture({ direction: GestureService.DIRECTION_LEFT, diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index c115fa63..48087534 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -4,18 +4,27 @@ class="media-modal-view" @backdropClicked="hide" > - + + + + + + + diff --git a/src/main.js b/src/main.js index 3895da89..03493525 100644 --- a/src/main.js +++ b/src/main.js @@ -45,6 +45,8 @@ Vue.use(VueClickOutside) Vue.use(PortalVue) Vue.use(VBodyScrollLock) +Vue.config.ignoredElements = ['pinch-zoom'] + Vue.component('FAIcon', FontAwesomeIcon) Vue.component('FALayers', FontAwesomeLayers) diff --git a/yarn.lock b/yarn.lock index 23cc895b..cecac65f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -915,6 +915,12 @@ resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.0.tgz#63da3e459147cebb0a8d58eed81d6071db9f5973" integrity sha512-N3VKw7KzRfOm8hShUVldpinlm13HpvLBQgT63QS+aCrIRLwjoEUXY5Rcmttbfb6HkzZaeqjLqd/aZCQ53UjQpg== +"@kazvmoe-infra/pinch-zoom-element@https://lily.kazv.moe/infra/pinch-zoom-element.git": + version "1.1.1" + resolved "https://lily.kazv.moe/infra/pinch-zoom-element.git#de150c01057f0de0c3ef669917399fe370e36f40" + dependencies: + pointer-tracker "^2.0.3" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -6941,6 +6947,11 @@ pngjs@^3.3.0: version "3.3.3" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" +pointer-tracker@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/pointer-tracker/-/pointer-tracker-2.4.0.tgz#78721c2d2201486db11ec1094377f03023b621b3" + integrity sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g== + portal-vue@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.4.tgz#1fc679d77e294dc8d026f1eb84aa467de11b392e" From 7469849c39a656ad88ef63a3d7ead8beb67cbced Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:11:59 -0400 Subject: [PATCH 06/18] Add swipe-click handler to media modal Now swiping will correctly change the current media, and with a good preview. Clicking without swiping closes the overlay. --- src/components/media_modal/media_modal.js | 59 +++----- src/components/media_modal/media_modal.vue | 13 +- src/components/pinch_zoom/pinch_zoom.js | 5 + src/modules/media_viewer.js | 54 -------- .../gesture_service/gesture_service.js | 130 ++++++++---------- 5 files changed, 93 insertions(+), 168 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index ca271f32..09be86c2 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -2,8 +2,9 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import PinchZoom from '../pinch_zoom/pinch_zoom.vue' -import fileTypeService from '../../services/file_type/file_type.service.js' +import SwipeClick from '../swipe_click/swipe_click.vue' import GestureService from '../../services/gesture_service/gesture_service' +import fileTypeService from '../../services/file_type/file_type.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, @@ -24,6 +25,7 @@ const MediaModal = { StillImage, VideoAttachment, PinchZoom, + SwipeClick, Modal }, computed: { @@ -57,32 +59,21 @@ const MediaModal = { }, data () { return { - pinchZoomOptions: { - } + swipeDirection: GestureService.DIRECTION_LEFT, + swipeThreshold: 50 } }, created () { - this.mediaGesture = new GestureService.SwipeAndScaleGesture({ - direction: GestureService.DIRECTION_LEFT, - callbackPositive: this.goNext, - callbackNegative: this.goPrev, - swipePreviewCallback: this.handleSwipePreview, - swipeEndCallback: this.handleSwipeEnd, - pinchPreviewCallback: this.handlePinchPreview, - pinchEndCallback: this.handlePinchEnd, - threshold: 50 - }) + // this.mediaGesture = new GestureService.SwipeAndScaleGesture({ + // callbackPositive: this.goNext, + // callbackNegative: this.goPrev, + // swipePreviewCallback: this.handleSwipePreview, + // swipeEndCallback: this.handleSwipeEnd, + // pinchPreviewCallback: this.handlePinchPreview, + // pinchEndCallback: this.handlePinchEnd + // }) }, methods: { - mediaTouchStart (e) { - this.mediaGesture.start(e) - }, - mediaTouchMove (e) { - this.mediaGesture.move(e) - }, - mediaTouchEnd (e) { - this.mediaGesture.end(e) - }, hide () { this.$store.dispatch('closeMediaViewer') }, @@ -90,43 +81,29 @@ const MediaModal = { if (this.canNavigate) { const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) this.$store.dispatch('setCurrent', this.media[prevIndex]) + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } }, goNext () { if (this.canNavigate) { const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) this.$store.dispatch('setCurrent', this.media[nextIndex]) + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } }, handleSwipePreview (offsets) { - this.$store.dispatch('swipeScaler/apply', { - offsets: this.scaling > SCALING_ENABLE_MOVE_THRESHOLD ? offsets : onlyXAxis(offsets) - }) + this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 }) }, handleSwipeEnd (sign) { - if (this.scaling > SCALING_ENABLE_MOVE_THRESHOLD) { - this.$store.dispatch('swipeScaler/finish') - return - } + console.log('handleSwipeEnd:', sign) if (sign === 0) { - this.$store.dispatch('swipeScaler/reset') + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } else if (sign > 0) { this.goNext() } else { this.goPrev() } }, - handlePinchPreview (offsets, scaling) { - console.log('handle pinch preview:', offsets, scaling) - this.$store.dispatch('swipeScaler/apply', { offsets, scaling }) - }, - handlePinchEnd () { - if (this.scaling > SCALING_RESET_MIN) { - this.$store.dispatch('swipeScaler/finish') - } else { - this.$store.dispatch('swipeScaler/reset') - } - }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 48087534..fefbfd27 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -4,9 +4,16 @@ class="media-modal-view" @backdropClicked="hide" > - + k + offsets[n]) - }, - applyScaling (state, { scaling }) { - state.scaling = state.origScaling * scaling - }, - finish (state) { - state.origOffsets = [...state.offsets] - state.origScaling = state.scaling - }, - revert (state) { - state.offsets = [...state.origOffsets] - state.scaling = state.origScaling - } - }, - - actions: { - reset ({ commit }) { - commit('reset') - }, - apply ({ commit }, { offsets, scaling = 1 }) { - commit('applyOffsets', { offsets }) - commit('applyScaling', { scaling }) - }, - finish ({ commit }) { - commit('finish') - }, - revert ({ commit }) { - commit('revert') - } - } } } } diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 82337bc6..f10dec3a 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -4,25 +4,27 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] -const DISTANCE_MIN = 1 +// const DISTANCE_MIN = 1 -const isSwipeEvent = e => (e.touches.length === 1) -const isSwipeEventEnd = e => (e.changedTouches.length === 1) +// const isSwipeEvent = e => (e.touches.length === 1) +// const isSwipeEventEnd = e => (e.changedTouches.length === 1) -const isScaleEvent = e => (e.targetTouches.length === 2) -const isScaleEventEnd = e => (e.targetTouches.length === 1) +// const isScaleEvent = e => (e.targetTouches.length === 2) +// const isScaleEventEnd = e => (e.targetTouches.length === 1) const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) -const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) +// const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) +// const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) -const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) +// const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) const touchCoord = touch => [touch.screenX, touch.screenY] const touchEventCoord = e => touchCoord(e.touches[0]) +const pointerEventCoord = e => [e.clientX, e.clientY] + const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) const perpendicular = v => [v[1], -v[0]] @@ -78,104 +80,87 @@ const updateSwipe = (event, gesture) => { gesture._swiping = false } -class SwipeAndScaleGesture { +class SwipeAndClickGesture { // swipePreviewCallback(offsets: Array[Number]) // offsets: the offset vector which the underlying component should move, from the starting position - // pinchPreviewCallback(offsets: Array[Number], scaling: Number) - // offsets: the offset vector which the underlying component should move, from the starting position - // scaling: the scaling factor we should apply to the underlying component, from the starting position - // swipeEndcallback(sign: 0|-1|1) + // swipeEndCallback(sign: 0|-1|1) // sign: if the swipe does not meet the threshold, 0 // if the swipe meets the threshold in the positive direction, 1 // if the swipe meets the threshold in the negative direction, -1 constructor ({ direction, - // swipeStartCallback, pinchStartCallback, - swipePreviewCallback, pinchPreviewCallback, - swipeEndCallback, pinchEndCallback, + // swipeStartCallback + swipePreviewCallback, + swipeEndCallback, + swipeCancelCallback, + swipelessClickCallback, threshold = 30, perpendicularTolerance = 1.0 }) { const nop = () => { console.log('Warning: Not implemented') } this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop - this.pinchPreviewCallback = pinchPreviewCallback || nop this.swipeEndCallback = swipeEndCallback || nop - this.pinchEndCallback = pinchEndCallback || nop + this.swipeCancelCallback = swipeCancelCallback || nop + this.swipelessClickCallback = swipelessClickCallback || nop this.threshold = threshold this.perpendicularTolerance = perpendicularTolerance + this._reset() + } + + _reset () { this._startPos = [0, 0] - this._startDistance = DISTANCE_MIN + this._pointerId = -1 this._swiping = false + this._swiped = false } start (event) { console.log('start() called', event) - if (isSwipeEvent(event)) { - this._startPos = touchEventCoord(event) - console.log('start pos:', this._startPos) - this._swiping = true - } else if (isScaleEvent(event)) { - const coords = [...event.targetTouches].map(touchCoord) - this._startPos = avgCoord(coords) - this._startDistance = vectorLength(deltaCoord(coords[0], coords[1])) - if (this._startDistance < DISTANCE_MIN) { - this._startDistance = DISTANCE_MIN - } - this._scalePoints = [...event.targetTouches] - this._swiping = false - console.log( - 'is scale event, start =', this._startPos, - 'dist =', this._startDistance) - } + + this._startPos = pointerEventCoord(event) + this._pointerId = event.pointerId + console.log('start pos:', this._startPos) + this._swiping = true + this._swiped = false } move (event) { - // console.log('move called', event) - if (isSwipeEvent(event)) { - const touch = event.changedTouches[0] - const delta = deltaCoord(this._startPos, touchCoord(touch)) + if (this._swiping && this._pointerId === event.pointerId) { + this._swiped = true + + const coord = pointerEventCoord(event) + const delta = deltaCoord(this._startPos, coord) this.swipePreviewCallback(delta) - } else if (isScaleEvent(event)) { - console.log('is scale event') - const coords = [...event.targetTouches].map(touchCoord) - const curPos = avgCoord(coords) - const curDistance = vectorLength(deltaCoord(coords[0], coords[1])) - const scaling = curDistance / this._startDistance - const posDiff = vectorMinus(curPos, this._startPos) - // const delta = vectorAdd(numProduct((1 - scaling), this._startPos), posDiff) - const delta = posDiff - // console.log( - // 'is scale event, cur =', curPos, - // 'dist =', curDistance, - // 'scale =', scaling, - // 'delta =', delta) - this.pinchPreviewCallback(delta, scaling) } } - end (event) { - console.log('end() called', event) - if (isScaleEventEnd(event)) { - this.pinchEndCallback() - } - - if (!isSwipeEventEnd(event)) { - console.log('not swipe event') + cancel (event) { + if (!this._swiping || this._pointerId !== event.pointerId) { return } + + this.swipeCancelCallback() + } + + end (event) { if (!this._swiping) { console.log('not swiping') return } - this.swiping = false - console.log('is swipe event') + if (this._pointerId !== event.pointerId) { + console.log('pointer id does not match') + return + } + + this._swiping = false + + console.log('end: is swipe event') // movement too small - const touch = event.changedTouches[0] - const delta = deltaCoord(this._startPos, touchCoord(touch)) - this.swipePreviewCallback(delta) + const coord = pointerEventCoord(event) + const delta = deltaCoord(this._startPos, coord) const sign = (() => { if (vectorLength(delta) < this.threshold) { @@ -198,7 +183,12 @@ class SwipeAndScaleGesture { return isPositive ? 1 : -1 })() - this.swipeEndCallback(sign) + if (this._swiped) { + this.swipeEndCallback(sign) + } else { + this.swipelessClickCallback() + } + this._reset() } } @@ -210,7 +200,7 @@ const GestureService = { swipeGesture, beginSwipe, updateSwipe, - SwipeAndScaleGesture + SwipeAndClickGesture } export default GestureService From cd9384adaefacb3a373e33b6326f71f8522fa416 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:21:18 -0400 Subject: [PATCH 07/18] Clean up --- src/components/media_modal/media_modal.js | 23 ++------------ src/components/media_modal/media_modal.vue | 7 ++--- .../gesture_service/gesture_service.js | 30 ++++++------------- 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 09be86c2..81404ae4 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -46,33 +46,16 @@ const MediaModal = { }, type () { return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null - }, - scaling () { - return this.$store.state.mediaViewer.swipeScaler.scaling - }, - offsets () { - return this.$store.state.mediaViewer.swipeScaler.offsets - }, - transform () { - return `translate(${this.offsets[0]}px, ${this.offsets[1]}px) scale(${this.scaling}, ${this.scaling})` } }, data () { return { swipeDirection: GestureService.DIRECTION_LEFT, - swipeThreshold: 50 + swipeThreshold: 50, + pinchZoomMinScale: 1, + pinchZoomScaleResetLimit: 1.2 } }, - created () { - // this.mediaGesture = new GestureService.SwipeAndScaleGesture({ - // callbackPositive: this.goNext, - // callbackNegative: this.goPrev, - // swipePreviewCallback: this.handleSwipePreview, - // swipeEndCallback: this.handleSwipeEnd, - // pinchPreviewCallback: this.handlePinchPreview, - // pinchEndCallback: this.handlePinchEnd - // }) - }, methods: { hide () { this.$store.dispatch('closeMediaViewer') diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index fefbfd27..7fe924ef 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -16,12 +16,11 @@ ref="pinchZoom" class="modal-image-container-inner" selector=".modal-image" - allow-pan-min-scale="1" - min-scale="1" - reset-to-min-scale-limit="1.2" reach-min-scale-strategy="reset" stop-propagate-handled="stop-propgate-handled" - :inner-class="'modal-image-container-inner'" + :allow-pan-min-scale="pinchZoomMinScale" + :min-scale="pinchZoomMinScale" + :reset-to-min-scale-limit="pinchZoomScaleResetLimit" > (e.touches.length === 1) -// const isSwipeEventEnd = e => (e.changedTouches.length === 1) - -// const isScaleEvent = e => (e.targetTouches.length === 2) -// const isScaleEventEnd = e => (e.targetTouches.length === 1) - const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -// const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) -// const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) - -// const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) - const touchCoord = touch => [touch.screenX, touch.screenY] const touchEventCoord = e => touchCoord(e.touches[0]) @@ -31,13 +18,14 @@ const perpendicular = v => [v[1], -v[0]] const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1] -// const numProduct = (num, v) => v.map(k => num * k) - const project = (v1, v2) => { const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2)) return [scalar * v2[0], scalar * v2[1]] } +// const debug = console.log +const debug = () => {} + // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the // callback should be called. @@ -96,7 +84,7 @@ class SwipeAndClickGesture { swipelessClickCallback, threshold = 30, perpendicularTolerance = 1.0 }) { - const nop = () => { console.log('Warning: Not implemented') } + const nop = () => { debug('Warning: Not implemented') } this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop this.swipeEndCallback = swipeEndCallback || nop @@ -115,11 +103,11 @@ class SwipeAndClickGesture { } start (event) { - console.log('start() called', event) + debug('start() called', event) this._startPos = pointerEventCoord(event) this._pointerId = event.pointerId - console.log('start pos:', this._startPos) + debug('start pos:', this._startPos) this._swiping = true this._swiped = false } @@ -145,18 +133,18 @@ class SwipeAndClickGesture { end (event) { if (!this._swiping) { - console.log('not swiping') + debug('not swiping') return } if (this._pointerId !== event.pointerId) { - console.log('pointer id does not match') + debug('pointer id does not match') return } this._swiping = false - console.log('end: is swipe event') + debug('end: is swipe event') // movement too small const coord = pointerEventCoord(event) From 214338b9f03f3ddf2940355261cfcf7f6d2ce62e Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:33:33 -0400 Subject: [PATCH 08/18] Add missing swipe click component --- src/components/swipe_click/swipe_click.js | 85 ++++++++++++++++++++++ src/components/swipe_click/swipe_click.vue | 14 ++++ 2 files changed, 99 insertions(+) create mode 100644 src/components/swipe_click/swipe_click.js create mode 100644 src/components/swipe_click/swipe_click.vue diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js new file mode 100644 index 00000000..49b097ce --- /dev/null +++ b/src/components/swipe_click/swipe_click.js @@ -0,0 +1,85 @@ +import GestureService from '../../services/gesture_service/gesture_service' + +/** + * props: + * direction: a vector that indicates the direction of the intended swipe + * threshold: the minimum distance in pixels the swipe has moved on `direction' + * for swipe-finished() to have a non-zero sign + * perpendicularTolerance: see gesture_service + * + * Events: + * preview-requested(offsets) + * Emitted when the pointer has moved. + * offsets: the offsets from the start of the swipe to the current cursor position + * + * swipe-canceled() + * Emitted when the swipe has been canceled due to a pointercancel event. + * + * swipe-finished(sign: 0|-1|1) + * Emitted when the swipe has finished. + * sign: if the swipe does not meet the threshold, 0 + * if the swipe meets the threshold in the positive direction, 1 + * if the swipe meets the threshold in the negative direction, -1 + * + * swipeless-clicked() + * Emitted when there is a click without swipe. + * This and swipe-finished() cannot be emitted for the same pointerup event. + */ +const SwipeClick = { + props: { + direction: { + type: Array + }, + threshold: { + type: Number, + default: 30 + }, + perpendicularTolerance: { + type: Number, + default: 1.0 + } + }, + methods: { + handlePointerDown (event) { + this.$gesture.start(event) + }, + handlePointerMove (event) { + this.$gesture.move(event) + }, + handlePointerUp (event) { + this.$gesture.end(event) + }, + handlePointerCancel (event) { + this.$gesture.cancel(event) + }, + handleNativeClick (event) { + event.stopPropagation() + event.preventDefault() + }, + preview (offsets) { + this.$emit('preview-requested', offsets) + }, + end (sign) { + this.$emit('swipe-finished', sign) + }, + click () { + this.$emit('swipeless-clicked') + }, + cancel () { + this.$emit('swipe-canceled') + } + }, + created () { + this.$gesture = new GestureService.SwipeAndClickGesture({ + direction: this.direction, + threshold: this.threshold, + perpendicularTolerance: this.perpendicularTolerance, + swipePreviewCallback: this.preview, + swipeEndCallback: this.end, + swipeCancelCallback: this.cancel, + swipelessClickCallback: this.click + }); + } +} + +export default SwipeClick diff --git a/src/components/swipe_click/swipe_click.vue b/src/components/swipe_click/swipe_click.vue new file mode 100644 index 00000000..5372071d --- /dev/null +++ b/src/components/swipe_click/swipe_click.vue @@ -0,0 +1,14 @@ + + + From 4552c28e0660040916fa70cb4b875f205a7c9310 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:34:18 -0400 Subject: [PATCH 09/18] Prevent the click event from firing on content below modal --- src/components/media_modal/media_modal.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 81404ae4..f83fe472 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -58,7 +58,12 @@ const MediaModal = { }, methods: { hide () { - this.$store.dispatch('closeMediaViewer') + // HACK: Closing immediately via a touch will cause the click + // to be processed on the content below the overlay + const transitionTime = 100 // ms + setTimeout(() => { + this.$store.dispatch('closeMediaViewer') + }, transitionTime) }, goPrev () { if (this.canNavigate) { From e2f3929872c49fbf9d875c83b798183431ca3c70 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:44:01 -0400 Subject: [PATCH 10/18] Make lint happy --- src/components/media_modal/media_modal.js | 4 ---- src/components/pinch_zoom/pinch_zoom.js | 6 ++++-- src/components/swipe_click/swipe_click.js | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index f83fe472..0bc8a658 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -16,10 +16,6 @@ library.add( faChevronRight ) -const onlyXAxis = ([x, y]) => [x, 0] -const SCALING_RESET_MIN = 1.1 -const SCALING_ENABLE_MOVE_THRESHOLD = 1 - const MediaModal = { components: { StillImage, diff --git a/src/components/pinch_zoom/pinch_zoom.js b/src/components/pinch_zoom/pinch_zoom.js index 36bebbce..82670ddf 100644 --- a/src/components/pinch_zoom/pinch_zoom.js +++ b/src/components/pinch_zoom/pinch_zoom.js @@ -1,11 +1,13 @@ import PinchZoom from '@kazvmoe-infra/pinch-zoom-element' export default { - props: { - }, methods: { setTransform ({ scale, x, y }) { this.$el.setTransform({ scale, x, y }) } + }, + created () { + // Make lint happy + (() => PinchZoom)() } } diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js index 49b097ce..ac77154a 100644 --- a/src/components/swipe_click/swipe_click.js +++ b/src/components/swipe_click/swipe_click.js @@ -78,7 +78,7 @@ const SwipeClick = { swipeEndCallback: this.end, swipeCancelCallback: this.cancel, swipelessClickCallback: this.click - }); + }) } } From 9d4dd64276072381eb5ddd2eccc48dddb21d6195 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 20:03:01 -0400 Subject: [PATCH 11/18] Reset position on swipe end even if we cannot navigate --- src/components/media_modal/media_modal.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 0bc8a658..ffc7c365 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -65,14 +65,12 @@ const MediaModal = { if (this.canNavigate) { const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) this.$store.dispatch('setCurrent', this.media[prevIndex]) - this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } }, goNext () { if (this.canNavigate) { const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) this.$store.dispatch('setCurrent', this.media[nextIndex]) - this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } }, handleSwipePreview (offsets) { @@ -80,9 +78,8 @@ const MediaModal = { }, handleSwipeEnd (sign) { console.log('handleSwipeEnd:', sign) - if (sign === 0) { - this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) - } else if (sign > 0) { + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) + if (sign > 0) { this.goNext() } else { this.goPrev() From 33384af5df77570312c259942011b823cbaf6642 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 20:32:02 -0400 Subject: [PATCH 12/18] Use native click for hiding overlay The pointerup strategy is unsuccessful, as some other overlays (Firefox's Inspect Element) will pass down pointerup events. --- src/components/swipe_click/swipe_click.js | 3 +-- .../gesture_service/gesture_service.js | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js index ac77154a..b979f42a 100644 --- a/src/components/swipe_click/swipe_click.js +++ b/src/components/swipe_click/swipe_click.js @@ -53,8 +53,7 @@ const SwipeClick = { this.$gesture.cancel(event) }, handleNativeClick (event) { - event.stopPropagation() - event.preventDefault() + this.$gesture.click(event) }, preview (offsets) { this.$emit('preview-requested', offsets) diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 238b7875..cd9e3ba2 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -4,6 +4,8 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] +const BUTTON_LEFT = 0 + const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] const touchCoord = touch => [touch.screenX, touch.screenY] @@ -23,8 +25,8 @@ const project = (v1, v2) => { return [scalar * v2[0], scalar * v2[1]] } -// const debug = console.log -const debug = () => {} +const debug = console.log +// const debug = () => {} // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the @@ -100,11 +102,17 @@ class SwipeAndClickGesture { this._pointerId = -1 this._swiping = false this._swiped = false + this._preventNextClick = false } start (event) { debug('start() called', event) + // Only handle left click + if (event.button !== BUTTON_LEFT) { + return + } + this._startPos = pointerEventCoord(event) this._pointerId = event.pointerId debug('start pos:', this._startPos) @@ -124,6 +132,7 @@ class SwipeAndClickGesture { } cancel (event) { + debug('cancel called') if (!this._swiping || this._pointerId !== event.pointerId) { return } @@ -146,6 +155,8 @@ class SwipeAndClickGesture { debug('end: is swipe event') + debug('button = ', event.button) + // movement too small const coord = pointerEventCoord(event) const delta = deltaCoord(this._startPos, coord) @@ -171,9 +182,18 @@ class SwipeAndClickGesture { return isPositive ? 1 : -1 })() + const swiped = this._swiped if (this._swiped) { this.swipeEndCallback(sign) - } else { + } + this._reset() + if (swiped) { + this._preventNextClick = true + } + } + + click (event) { + if (!this._preventNextClick) { this.swipelessClickCallback() } this._reset() From 1478e72fcc8df66e8e6c8a9b518932480af73f03 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 20:46:10 -0400 Subject: [PATCH 13/18] Allow pinch-zoom to fill the whole screen --- src/components/media_modal/media_modal.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 7fe924ef..94143ef4 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -107,7 +107,10 @@ flex-direction: column; max-width: 90%; max-height: 95%; + width: 100%; + height: 100%; flex-grow: 1; + justify-content: center; &-inner { width: 100%; @@ -116,6 +119,7 @@ display: flex; flex-direction: column; align-items: center; + justify-content: center; } } From 7b77c0c884d90fff47e5acb45a460e4d61c038a1 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 21:02:57 -0400 Subject: [PATCH 14/18] Update pinch-zoom-element --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index cecac65f..88641aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,7 +917,7 @@ "@kazvmoe-infra/pinch-zoom-element@https://lily.kazv.moe/infra/pinch-zoom-element.git": version "1.1.1" - resolved "https://lily.kazv.moe/infra/pinch-zoom-element.git#de150c01057f0de0c3ef669917399fe370e36f40" + resolved "https://lily.kazv.moe/infra/pinch-zoom-element.git#fbb4d71b1c09c2e24db054cf15d716e125a53bf9" dependencies: pointer-tracker "^2.0.3" From db71bbf3585544efd768308163dfc77f83bab96d Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 21:19:04 -0400 Subject: [PATCH 15/18] Scale swipe threshold with viewport width --- src/components/media_modal/media_modal.js | 7 +++++-- src/components/swipe_click/swipe_click.js | 4 ++-- src/services/gesture_service/gesture_service.js | 11 +++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index ffc7c365..b44a2b63 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -47,7 +47,10 @@ const MediaModal = { data () { return { swipeDirection: GestureService.DIRECTION_LEFT, - swipeThreshold: 50, + swipeThreshold: () => { + const considerableMoveRatio = 1 / 4 + return window.innerWidth * considerableMoveRatio + }, pinchZoomMinScale: 1, pinchZoomScaleResetLimit: 1.2 } @@ -81,7 +84,7 @@ const MediaModal = { this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) if (sign > 0) { this.goNext() - } else { + } else if (sign < 0) { this.goPrev() } }, diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js index b979f42a..238e6df8 100644 --- a/src/components/swipe_click/swipe_click.js +++ b/src/components/swipe_click/swipe_click.js @@ -31,8 +31,8 @@ const SwipeClick = { type: Array }, threshold: { - type: Number, - default: 30 + type: Function, + default: () => 30 }, perpendicularTolerance: { type: Number, diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index cd9e3ba2..97a26ba7 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -25,8 +25,8 @@ const project = (v1, v2) => { return [scalar * v2[0], scalar * v2[1]] } -const debug = console.log -// const debug = () => {} +// const debug = console.log +const debug = () => {} // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the @@ -92,7 +92,7 @@ class SwipeAndClickGesture { this.swipeEndCallback = swipeEndCallback || nop this.swipeCancelCallback = swipeCancelCallback || nop this.swipelessClickCallback = swipelessClickCallback || nop - this.threshold = threshold + this.threshold = typeof threshold === 'function' ? threshold : () => threshold this.perpendicularTolerance = perpendicularTolerance this._reset() } @@ -162,7 +162,10 @@ class SwipeAndClickGesture { const delta = deltaCoord(this._startPos, coord) const sign = (() => { - if (vectorLength(delta) < this.threshold) { + debug( + 'threshold = ', this.threshold(), + 'vector len =', vectorLength(delta)) + if (vectorLength(delta) < this.threshold()) { return 0 } // movement is opposite from direction From 3366c915e990185d66fa3bdfdb1953db01e21c8b Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 22:08:04 -0400 Subject: [PATCH 16/18] Check whether we swiped only for mouse pointer --- src/services/gesture_service/gesture_service.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 97a26ba7..741d4a12 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -190,7 +190,11 @@ class SwipeAndClickGesture { this.swipeEndCallback(sign) } this._reset() - if (swiped) { + // Only a mouse will fire click event when + // the end point is far from the starting point + // so for other kinds of pointers do not check + // whether we have swiped + if (swiped && event.pointerType === 'mouse') { this._preventNextClick = true } } From 4c2a7aabe5ac67d554580023dadb3979d42003b5 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 22:45:19 -0400 Subject: [PATCH 17/18] Clean up --- src/components/media_modal/media_modal.vue | 5 ----- src/modules/media_viewer.js | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 94143ef4..6a6eff45 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -133,11 +133,6 @@ animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein; } -//.modal-image { -// height: 90vh; -// width: 100%; -//} - .modal-view-button-arrow { position: absolute; display: block; diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js index 2d756289..721c25e6 100644 --- a/src/modules/media_viewer.js +++ b/src/modules/media_viewer.js @@ -19,18 +19,18 @@ const mediaViewer = { } }, actions: { - setMedia ({ commit, dispatch }, attachments) { + setMedia ({ commit }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) return type === 'image' || type === 'video' || type === 'audio' }) commit('setMedia', media) }, - setCurrent ({ commit, state, dispatch }, current) { + setCurrent ({ commit, state }, current) { const index = state.media.indexOf(current) commit('setCurrent', index || 0) }, - closeMediaViewer ({ commit, dispatch }) { + closeMediaViewer ({ commit }) { commit('close') } } From 3b6b8b96c4a69cca0a07806609fc81d1a1e8aee7 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 23:24:39 -0400 Subject: [PATCH 18/18] Bump pinch-zoom-element version --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 88641aef..53be0d74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,7 +917,7 @@ "@kazvmoe-infra/pinch-zoom-element@https://lily.kazv.moe/infra/pinch-zoom-element.git": version "1.1.1" - resolved "https://lily.kazv.moe/infra/pinch-zoom-element.git#fbb4d71b1c09c2e24db054cf15d716e125a53bf9" + resolved "https://lily.kazv.moe/infra/pinch-zoom-element.git#b5d2e9fb41231e1bff12058bbfc55df05cfdc1eb" dependencies: pointer-tracker "^2.0.3"