From 750e0e4e38640143701ed1632fa13f110678bc2b Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 22 Apr 2025 00:20:43 -0400 Subject: [PATCH] Pan support, mobile support for zoom/pan --- app/static/app/js/components/ImagePopup.jsx | 110 ++++++++++++++++++-- app/static/app/js/css/ImagePopup.scss | 8 +- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/app/static/app/js/components/ImagePopup.jsx b/app/static/app/js/components/ImagePopup.jsx index 75be03bb..2df98bde 100644 --- a/app/static/app/js/components/ImagePopup.jsx +++ b/app/static/app/js/components/ImagePopup.jsx @@ -20,7 +20,8 @@ class ImagePopup extends React.Component { translateX: 0, translateY: 0, - scale: 1 + scale: 1, + dragging: false } } @@ -37,6 +38,13 @@ class ImagePopup extends React.Component { if (this.image){ this.image.addEventListener("fullscreenchange", this.onFullscreenChange); this.image.addEventListener("mousewheel", this.onMouseWheel); + this.image.addEventListener("mousedown", this.onMouseDown); + this.image.addEventListener("mousemove", this.onMouseMove); + this.image.addEventListener("mouseup", this.onMouseUp); + this.image.addEventListener("touchstart", this.onTouchStart); + this.image.addEventListener("touchmove", this.onTouchMove); + this.image.addEventListener("touchend", this.onTouchEnd); + } } @@ -44,6 +52,12 @@ class ImagePopup extends React.Component { if (this.image){ this.image.removeEventListener("fullscreenchange", this.onFullscreenChange); this.image.removeEventListener("mousewheel", this.onMouseWheel); + this.image.removeEventListener("mousedown", this.onMouseDown); + this.image.removeEventListener("mousemove", this.onMouseMove); + this.image.removeEventListener("mouseup", this.onMouseUp); + this.image.removeEventListener("touchstart", this.onTouchStart); + this.image.removeEventListener("touchmove", this.onTouchMove); + this.image.removeEventListener("touchend", this.onTouchEnd); } } @@ -61,6 +75,88 @@ class ImagePopup extends React.Component { this.setState({loading: false}); } + onMouseDown = (e) => { + const { translateX, translateY } = this.state; + this.dragging = true; + this.dragged = false; + this.startMouseX = e.clientX; + this.startTranslateX = translateX; + this.startMouseY = e.clientY; + this.startTranslateY = translateY; + } + + onMouseUp = () => { + if (this.dragging){ + this.startMouseX = this.startMouseY = 0; + this.setState({dragging: false}); + } + this.dragging = false; + } + + onMouseMove = (e) => { + if (this.dragging){ + const dx = e.clientX - this.startMouseX; + const dy = e.clientY - this.startMouseY; + + if (Math.abs(dx) > 5 || Math.abs(dy) > 5){ + this.dragged = true; + this.setState({ + dragging: true, + translateX: dx + this.startTranslateX, + translateY: dy + this.startTranslateY + }); + } + } + } + + touchDistance = e => { + if (e.touches && e.touches.length === 2){ + const [t1, t2] = e.touches; + const dx = t1.clientX - t2.clientX; + const dy = t1.clientY - t2.clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + return 0; + } + + onTouchStart = e => { + if (e.touches.length === 2){ + this.lastTouchDist = this.touchDistance(e); + }else if (e.touches.length === 1){ + this.lastTouchDist = 0; + this.onMouseDown({ + clientX: e.touches[0].clientX, + clientY: e.touches[0].clientY + }); + } + } + + onTouchMove = e => { + if (e.touches.length === 2 && this.lastTouchDist > 0){ + const [t1, t2] = e.touches; + const curDist = this.touchDistance(e); + const delta = 1.5 * (curDist - this.lastTouchDist); + if (Math.abs(delta) > 0.05){ + this.lastTouchDist = curDist; + this.onMouseWheel({ + clientX: (t1.clientX + t2.clientX) / 2, + clientY: (t1.clientY + t2.clientY) / 2, + deltaY: -delta + }); + } + }else if (e.touches.length === 1){ + this.onMouseMove({ + clientX: e.touches[0].clientX, + clientY: e.touches[0].clientY + }); + } + } + + onTouchEnd = () => { + this.lastTouchDist = 0; + this.onMouseUp(); + } onMouseWheel = e => { if (!this.image || !this.state.expandThumb) return; @@ -74,8 +170,8 @@ class ImagePopup extends React.Component { const mouseY = e.clientY; const delta = -e.deltaY || e.wheelDelta || -e.detail; - const zoomFactor = 1.2; - const newScale = Math.max(1, delta > 0 ? scale * zoomFactor : scale / zoomFactor); + const zoomFactor = 1.0 + (2.0 * delta / Math.max(window.innerHeight, window.innerWidth)); + const newScale = Math.max(1, scale * zoomFactor); if (newScale > maxScale) return; @@ -101,14 +197,14 @@ class ImagePopup extends React.Component { if (!expandThumb){ this.image.requestFullscreen(); this.setState({ loading: true, expandThumb: true, translateX: 0, translateY: 0, scale: 1}); - }else{ + }else if (!this.dragged){ document.exitFullscreen(); this.setState({ expandThumb: false, translateX: 0, translateY: 0, scale: 1 }); } } render(){ - const { error, loading, expandThumb, translateX, translateY, scale } = this.state; + const { error, loading, expandThumb, dragging, translateX, translateY, scale } = this.state; const { feature, task } = this.props; const downloadImageLink = `/api/projects/${task.project}/tasks/${task.id}/images/download/${feature.properties.filename}`; @@ -122,11 +218,11 @@ class ImagePopup extends React.Component { : ""} {error !== "" ?
{error}
: [ -
{ this.image = domNode;}}> {loading && expandThumb ?
: ""} - +
,
{_("Download Image")} diff --git a/app/static/app/js/css/ImagePopup.scss b/app/static/app/js/css/ImagePopup.scss index 8a80d95c..c440ffe8 100644 --- a/app/static/app/js/css/ImagePopup.scss +++ b/app/static/app/js/css/ImagePopup.scss @@ -29,7 +29,7 @@ top: 45%; } a{ - cursor: zoom-in; + cursor: default; } img{ image-rendering: pixelated; @@ -38,5 +38,11 @@ transform-origin: 0 0; } } + + &.dragging{ + a{ + cursor: move; + } + } } } \ No newline at end of file