Skip to content

Commit

Permalink
Merge pull request #1952 from nextcloud/enh/pinch-to-zoom
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv authored Aug 22, 2024
2 parents eaaa9b2 + 77c3fae commit 5655193
Show file tree
Hide file tree
Showing 14 changed files with 74,933 additions and 140 deletions.
9,330 changes: 9,330 additions & 0 deletions css/main-B8JMBDQI.chunk.css

Large diffs are not rendered by default.

9,334 changes: 9,334 additions & 0 deletions css/main-BQpYvOBl.chunk.css

Large diffs are not rendered by default.

9,330 changes: 9,330 additions & 0 deletions css/main-Bib0W53f.chunk.css

Large diffs are not rendered by default.

9,330 changes: 9,330 additions & 0 deletions css/main-Bq-oOeTE.chunk.css

Large diffs are not rendered by default.

9,330 changes: 9,330 additions & 0 deletions css/main-BqxICS9X.chunk.css

Large diffs are not rendered by default.

9,330 changes: 9,330 additions & 0 deletions css/main-CTwq7dfv.chunk.css

Large diffs are not rendered by default.

9,330 changes: 9,330 additions & 0 deletions css/main-CWJ2NyUp.chunk.css

Large diffs are not rendered by default.

9,330 changes: 9,330 additions & 0 deletions css/main-JMTTP9er.chunk.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion css/viewer-main.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* extracted by css-entry-points-plugin */
@import './main-dRuNgPbA.chunk.css';
@import './main-BQpYvOBl.chunk.css';
200 changes: 136 additions & 64 deletions js/viewer-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/viewer-main.mjs.map

Large diffs are not rendered by default.

210 changes: 141 additions & 69 deletions src/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,25 @@
:class="{
dragging,
loaded,
zoomed: zoomRatio !== 1
zoomed: zoomRatio > 1
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@load="updateImgSize"
@wheel="updateZoom"
@wheel.stop.prevent="updateZoom"
@dblclick.prevent="onDblclick"
@mousedown.prevent="dragStart">
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove">

<template v-if="livePhoto">
<video v-show="livePhotoCanBePlayed"
ref="video"
:class="{
dragging,
loaded,
zoomed: zoomRatio !== 1
zoomed: zoomRatio > 1
}"
:style="imgStyle"
:playsinline="true"
Expand All @@ -60,10 +62,12 @@
preload="metadata"
@canplaythrough="doneLoadingLivePhoto"
@loadedmetadata="updateImgSize"
@wheel="updateZoom"
@wheel.stop.prevent="updateZoom"
@error.capture.prevent.stop.once="onFail"
@dblclick.prevent="onDblclick"
@mousedown.prevent="dragStart"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove"
@ended="stopLivePhoto" />
<button v-if="width !== 0"
class="live-photo_play_button"
Expand Down Expand Up @@ -111,10 +115,6 @@ export default {
},

props: {
canZoom: {
type: Boolean,
default: false,
},
editing: {
type: Boolean,
default: false,
Expand All @@ -128,6 +128,10 @@ export default {
zoomRatio: 1,
fallback: false,
livePhotoCanBePlayed: false,
zooming: false,
pinchDistance: 0,
pinchStartZoomRatio: 1,
pointerCache: [],
}
},

Expand All @@ -146,7 +150,10 @@ export default {
},
imgStyle() {
if (this.zoomRatio === 1) {
return {}
return {
height: this.zoomHeight + 'px',
width: this.zoomWidth + 'px',
}
}
return {
marginTop: Math.round(this.shiftY * 2) + 'px',
Expand Down Expand Up @@ -203,11 +210,13 @@ export default {
// the item was hidden before and is now the current view
if (val === true && old === false) {
this.resetZoom()
// end the dragging if your mouse go out of the content
window.addEventListener('mouseout', this.dragEnd)
// end the dragging if your pointer (mouse or touch) go out of the content
// Not sure why ???
window.addEventListener('pointerout', this.pointerUp)
// the item is not displayed
} else if (val === false) {
window.removeEventListener('mouseout', this.dragEnd)
// Not sure why ???
window.removeEventListener('pointerout', this.pointerUp)
}
},
},
Expand Down Expand Up @@ -236,6 +245,49 @@ export default {
return `data:${this.mime};base64,${btoa(unescape(encodeURIComponent(file.data)))}`
},

// Helper methods for zoom/pan operations
updateShift(newShiftX, newShiftY, newZoomRatio) {
const maxShiftX = this.width * newZoomRatio - this.width
const maxShiftY = this.height * newZoomRatio - this.height
this.shiftX = Math.min(Math.max(newShiftX, -maxShiftX / 2), maxShiftX / 2)
this.shiftY = Math.min(Math.max(newShiftY, -maxShiftY / 2), maxShiftY / 2)
},

// Change zoom ratio of the image to newZoomRatio.
// Try to make sure that image position at stableX, stableY
// in client coordinates stays in the same place on the screen.
updateZoomAndShift(stableX, stableY, newZoomRatio) {
if (!this.canZoom) {
return
}

// scrolling position relative to the image
const element = this.$refs.image ?? this.$refs.video
const scrollX = stableX - element.getBoundingClientRect().x - (this.width * this.zoomRatio / 2)
const scrollY = stableY - element.getBoundingClientRect().y - (this.height * this.zoomRatio / 2)
const scrollPercX = scrollX / (this.width * this.zoomRatio)
const scrollPercY = scrollY / (this.height * this.zoomRatio)

// calc how much the img grow from its current size
// and adjust the margin accordingly
const growX = this.width * newZoomRatio - this.width * this.zoomRatio
const growY = this.height * newZoomRatio - this.height * this.zoomRatio

// compensate for existing margins
const newShiftX = this.shiftX - scrollPercX * growX
const newShiftY = this.shiftY - scrollPercY * growY
this.updateShift(newShiftX, newShiftY, newZoomRatio)
this.zoomRatio = newZoomRatio
},

distanceBetweenTouches() {
const t0 = this.pointerCache[0]
const t1 = this.pointerCache[1]
const diffX = (t1.x - t0.x)
const diffY = (t1.y - t0.y)
return Math.sqrt(diffX * diffX + diffY * diffY)
},

/**
* Handle zooming
*
Expand All @@ -247,17 +299,7 @@ export default {
return
}

event.stopPropagation()
event.preventDefault()

// scrolling position relative to the image
const element = this.$refs.image ?? this.$refs.video
const scrollX = event.clientX - element.x - (this.width * this.zoomRatio / 2)
const scrollY = event.clientY - element.y - (this.height * this.zoomRatio / 2)
const scrollPercX = scrollX / (this.width * this.zoomRatio)
const scrollPercY = scrollY / (this.height * this.zoomRatio)
const isZoomIn = event.deltaY < 0

const newZoomRatio = isZoomIn
? Math.min(this.zoomRatio * 1.1, 5) // prevent too big zoom
: Math.max(this.zoomRatio / 1.1, 1) // prevent too small zoom
Expand All @@ -267,16 +309,8 @@ export default {
return this.resetZoom()
}

// calc how much the img grow from its current size
// and adjust the margin accordingly
const growX = this.width * newZoomRatio - this.width * this.zoomRatio
const growY = this.height * newZoomRatio - this.height * this.zoomRatio

// compensate for existing margins
this.disableSwipe()
this.shiftX = this.shiftX - scrollPercX * growX
this.shiftY = this.shiftY - scrollPercY * growY
this.zoomRatio = newZoomRatio
this.updateZoomAndShift(event.clientX, event.clientY, newZoomRatio)
},

resetZoom() {
Expand All @@ -286,52 +320,94 @@ export default {
this.shiftY = 0
},

// Pinch-zoom implementation based on:
// https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures

/**
* Dragging handlers
* Dragging and (pinch) zooming handlers
*
* @param {DragEvent} event the event
*/
dragStart(event) {
const { pageX, pageY } = event
pointerDown(event) {
// New pointer - mouse down or additional touch --> store client coordinates in the pointer cache
this.pointerCache.push({ pointerId: event.pointerId, x: event.clientX, y: event.clientY })

// Single touch or mouse down --> start dragging
if (this.pointerCache.length === 1) {
this.dragX = event.clientX
this.dragY = event.clientY
this.dragging = true
}

this.dragX = pageX
this.dragY = pageY
this.dragging = true
const element = this.$refs.image ?? this.$refs.video
element.onmouseup = this.dragEnd
element.onmousemove = this.dragHandler
// Two touches --> start (pinch) zooming
if (this.pointerCache.length === 2) {
// Calculate base (reference) distance between touches
this.pinchDistance = this.distanceBetweenTouches()
this.pinchStartZoomRatio = this.zoomRatio
this.zooming = true
this.disableSwipe()
}
},
/**
* @param {DragEvent} event the event
*/
dragEnd(event) {
event.preventDefault()

pointerUp(event) {
// Remove pointer from the pointer cache
const index = this.pointerCache.findIndex(
(cachedEv) => cachedEv.pointerId === event.pointerId,
)
this.pointerCache.splice(index, 1)
this.dragging = false
const element = this.$refs.image ?? this.$refs.video
if (element) {
element.onmouseup = null
element.onmousemove = null
}
this.zooming = false
},
/**
* @param {DragEvent} event the event
*/
dragHandler(event) {
event.preventDefault()
const { pageX, pageY } = event

if (this.dragging && this.zoomRatio > 1 && pageX > 0 && pageY > 0) {
const moveX = this.shiftX + (pageX - this.dragX)
const moveY = this.shiftY + (pageY - this.dragY)
const growX = this.zoomWidth - this.width
const growY = this.zoomHeight - this.height

this.shiftX = Math.min(Math.max(moveX, -growX / 2), growX / 2)
this.shiftY = Math.min(Math.max(moveY, -growY / 2), growY / 2)
this.dragX = pageX
this.dragY = pageY
pointerMove(event) {
if (!this.canZoom) {
return
}

if (this.pointerCache.length > 0) {
// Update pointer position in the pointer cache
const index = this.pointerCache.findIndex(
(cachedEv) => cachedEv.pointerId === event.pointerId,
)
if (index >= 0) {
this.pointerCache[index].x = event.clientX
this.pointerCache[index].y = event.clientY
}
}

// Single touch or mouse down --> dragging
if (this.pointerCache.length === 1 && this.dragging && !this.zooming && this.zoomRatio > 1) {
const { clientX, clientY } = event
const newShiftX = this.shiftX + (clientX - this.dragX)
const newShiftY = this.shiftY + (clientY - this.dragY)

this.updateShift(newShiftX, newShiftY, this.zoomRatio)

this.dragX = clientX
this.dragY = clientY
}

// Two touches --> (pinch) zooming
if (this.pointerCache.length === 2 && this.zooming) {
// Calculate current distance between touches
const newDistance = this.distanceBetweenTouches()

// Calculate new zoom ratio - keep it between 1 and 5
const newZoomRatio = Math.min(Math.max(this.pinchStartZoomRatio * (newDistance / this.pinchDistance), 1), 5)

// Calculate "stable" point - in the middle between touches
const t0 = this.pointerCache[0]
const t1 = this.pointerCache[1]
const stableX = (t0.x + t1.x) / 2
const stableY = (t0.y + t1.y) / 2

this.updateZoomAndShift(stableX, stableY, newZoomRatio)
}

},
onDblclick() {
if (!this.canZoom) {
Expand Down Expand Up @@ -392,14 +468,13 @@ $checkered-color: #efefef;
}

img, video {
max-width: 100%;
max-height: 100%;
align-self: center;
justify-self: center;
// black while loading
background-color: #000;
// disable animations during zooming/resize
transition: none !important;
touch-action: none;
// show checkered bg on hover if not currently zooming (but ok if zoomed)
&:hover {
background-image: linear-gradient(45deg, #{$checkered-color} 25%, transparent 25%),
Expand All @@ -414,9 +489,6 @@ img, video {
background-color: #fff;
}
&.zoomed {
position: absolute;
max-height: none;
max-width: none;
z-index: 10010;
cursor: move;
}
Expand Down
7 changes: 7 additions & 0 deletions src/mixins/Mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export default {
type: Boolean,
default: true,
},
canZoom: {
type: Boolean,
default: false,
},
// is the content loaded?
// synced with parent
loaded: {
Expand Down Expand Up @@ -197,6 +201,9 @@ export default {
this.height = this.naturalHeight
this.width = this.naturalWidth
}
} else {
this.height = this.naturalHeight
this.width = this.naturalWidth
}
},

Expand Down
8 changes: 3 additions & 5 deletions src/views/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
ref="content"
:active="true"
:can-swipe="false"
:can-zoom="false"
v-bind="currentFile"
:file-list="[currentFile]"
:is-full-screen="false"
Expand Down Expand Up @@ -155,7 +156,7 @@
v-bind="currentFile"
:active="true"
:can-swipe.sync="canSwipe"
:can-zoom="canZoom"
:can-zoom="true"
:editing.sync="editing"
:file-list="fileList"
:is-full-screen="isFullscreen"
Expand Down Expand Up @@ -332,9 +333,6 @@ export default {
canLoop() {
return this.Viewer.canLoop
},
canZoom() {
return !this.Viewer.el
},
isStartOfList() {
return this.currentIndex === 0
},
Expand Down Expand Up @@ -505,7 +503,7 @@ export default {

// user reached the end of list
async isEndOfList(isEndOfList) {
if (!isEndOfList) {
if (!isEndOfList || this.el) {
return
}

Expand Down

0 comments on commit 5655193

Please sign in to comment.