Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinch to zoom #1952

Merged
merged 7 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading