Fix expanded image pan and zoom
Browse files- frontend/app.js +41 -12
- frontend/index.html +3 -3
- frontend/style.css +6 -6
frontend/app.js
CHANGED
|
@@ -15,7 +15,14 @@ const POST_ID_RE = /\d+/g
|
|
| 15 |
const PAGE_SIZE = 60
|
| 16 |
const SEARCH_PAGE_SIZE = 10
|
| 17 |
const IMAGE_URL_VERSION = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
let viewerScale = 1
|
|
|
|
|
|
|
| 19 |
let viewerDrag = null
|
| 20 |
let viewerPostId = ""
|
| 21 |
let viewerPages = []
|
|
@@ -696,6 +703,8 @@ function showViewerPage() {
|
|
| 696 |
$("#viewer-prev").classList.toggle("hidden", viewerPages.length <= 1)
|
| 697 |
$("#viewer-next").classList.toggle("hidden", viewerPages.length <= 1)
|
| 698 |
viewerScale = 1
|
|
|
|
|
|
|
| 699 |
applyViewerZoom()
|
| 700 |
}
|
| 701 |
|
|
@@ -728,18 +737,38 @@ function closeViewer() {
|
|
| 728 |
}
|
| 729 |
|
| 730 |
function applyViewerZoom() {
|
| 731 |
-
$("#viewer-img").style.transform = `scale(${viewerScale})`
|
| 732 |
$("#viewer-zoom").textContent = `${Math.round(viewerScale * 100)}%`
|
| 733 |
}
|
| 734 |
|
| 735 |
-
|
| 736 |
-
|
|
|
|
|
|
|
|
|
|
| 737 |
applyViewerZoom()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
})
|
| 739 |
|
| 740 |
$("#viewer-zoom-out").addEventListener("click", () => {
|
| 741 |
-
|
| 742 |
-
applyViewerZoom()
|
| 743 |
})
|
| 744 |
|
| 745 |
$("#viewer-close").addEventListener("click", closeViewer)
|
|
@@ -748,26 +777,26 @@ $("#viewer-next").addEventListener("click", () => moveViewerPage(1))
|
|
| 748 |
$("#viewer").addEventListener("click", e => { if (e.target.id === "viewer") closeViewer() })
|
| 749 |
$("#viewer-stage").addEventListener("wheel", e => {
|
| 750 |
e.preventDefault()
|
| 751 |
-
|
| 752 |
-
applyViewerZoom()
|
| 753 |
})
|
| 754 |
$("#viewer-stage").addEventListener("pointerdown", e => {
|
| 755 |
if (e.button !== 0 || e.target.closest(".viewer-nav")) return
|
|
|
|
| 756 |
const stage = $("#viewer-stage")
|
| 757 |
viewerDrag = {
|
| 758 |
x: e.clientX,
|
| 759 |
y: e.clientY,
|
| 760 |
-
left:
|
| 761 |
-
top:
|
| 762 |
}
|
| 763 |
stage.classList.add("dragging")
|
| 764 |
stage.setPointerCapture(e.pointerId)
|
| 765 |
})
|
| 766 |
$("#viewer-stage").addEventListener("pointermove", e => {
|
| 767 |
if (!viewerDrag) return
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
})
|
| 772 |
$("#viewer-stage").addEventListener("pointerup", e => {
|
| 773 |
viewerDrag = null
|
|
|
|
| 15 |
const PAGE_SIZE = 60
|
| 16 |
const SEARCH_PAGE_SIZE = 10
|
| 17 |
const IMAGE_URL_VERSION = 2
|
| 18 |
+
const VIEWER_GAP = 16
|
| 19 |
+
const VIEWER_MIN_SCALE = .25
|
| 20 |
+
const VIEWER_MAX_SCALE = 6
|
| 21 |
+
const VIEWER_BUTTON_STEP = .25
|
| 22 |
+
const VIEWER_WHEEL_STEP = .15
|
| 23 |
let viewerScale = 1
|
| 24 |
+
let viewerX = 0
|
| 25 |
+
let viewerY = VIEWER_GAP
|
| 26 |
let viewerDrag = null
|
| 27 |
let viewerPostId = ""
|
| 28 |
let viewerPages = []
|
|
|
|
| 703 |
$("#viewer-prev").classList.toggle("hidden", viewerPages.length <= 1)
|
| 704 |
$("#viewer-next").classList.toggle("hidden", viewerPages.length <= 1)
|
| 705 |
viewerScale = 1
|
| 706 |
+
viewerX = 0
|
| 707 |
+
viewerY = VIEWER_GAP
|
| 708 |
applyViewerZoom()
|
| 709 |
}
|
| 710 |
|
|
|
|
| 737 |
}
|
| 738 |
|
| 739 |
function applyViewerZoom() {
|
| 740 |
+
$("#viewer-img").style.transform = `translate3d(${viewerX}px, ${viewerY}px, 0) scale(${viewerScale})`
|
| 741 |
$("#viewer-zoom").textContent = `${Math.round(viewerScale * 100)}%`
|
| 742 |
}
|
| 743 |
|
| 744 |
+
function resetViewerZoom() {
|
| 745 |
+
const stage = $("#viewer-stage")
|
| 746 |
+
viewerScale = 1
|
| 747 |
+
viewerX = (stage.clientWidth - $("#viewer-img").naturalWidth) / 2
|
| 748 |
+
viewerY = VIEWER_GAP
|
| 749 |
applyViewerZoom()
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
function zoomViewer(scale, x, y) {
|
| 753 |
+
const next = Math.min(Math.max(scale, VIEWER_MIN_SCALE), VIEWER_MAX_SCALE)
|
| 754 |
+
if (next === viewerScale) return
|
| 755 |
+
const rect = $("#viewer-stage").getBoundingClientRect()
|
| 756 |
+
const anchorX = (x ?? rect.left + rect.width / 2) - rect.left
|
| 757 |
+
const anchorY = (y ?? rect.top + rect.height / 2) - rect.top
|
| 758 |
+
const ratio = next / viewerScale
|
| 759 |
+
viewerX = anchorX - (anchorX - viewerX) * ratio
|
| 760 |
+
viewerY = anchorY - (anchorY - viewerY) * ratio
|
| 761 |
+
viewerScale = next
|
| 762 |
+
applyViewerZoom()
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
$("#viewer-img").addEventListener("load", resetViewerZoom)
|
| 766 |
+
$("#viewer-zoom-in").addEventListener("click", () => {
|
| 767 |
+
zoomViewer(viewerScale + VIEWER_BUTTON_STEP)
|
| 768 |
})
|
| 769 |
|
| 770 |
$("#viewer-zoom-out").addEventListener("click", () => {
|
| 771 |
+
zoomViewer(viewerScale - VIEWER_BUTTON_STEP)
|
|
|
|
| 772 |
})
|
| 773 |
|
| 774 |
$("#viewer-close").addEventListener("click", closeViewer)
|
|
|
|
| 777 |
$("#viewer").addEventListener("click", e => { if (e.target.id === "viewer") closeViewer() })
|
| 778 |
$("#viewer-stage").addEventListener("wheel", e => {
|
| 779 |
e.preventDefault()
|
| 780 |
+
zoomViewer(viewerScale + (e.deltaY < 0 ? VIEWER_WHEEL_STEP : -VIEWER_WHEEL_STEP), e.clientX, e.clientY)
|
|
|
|
| 781 |
})
|
| 782 |
$("#viewer-stage").addEventListener("pointerdown", e => {
|
| 783 |
if (e.button !== 0 || e.target.closest(".viewer-nav")) return
|
| 784 |
+
e.preventDefault()
|
| 785 |
const stage = $("#viewer-stage")
|
| 786 |
viewerDrag = {
|
| 787 |
x: e.clientX,
|
| 788 |
y: e.clientY,
|
| 789 |
+
left: viewerX,
|
| 790 |
+
top: viewerY,
|
| 791 |
}
|
| 792 |
stage.classList.add("dragging")
|
| 793 |
stage.setPointerCapture(e.pointerId)
|
| 794 |
})
|
| 795 |
$("#viewer-stage").addEventListener("pointermove", e => {
|
| 796 |
if (!viewerDrag) return
|
| 797 |
+
viewerX = viewerDrag.left + e.clientX - viewerDrag.x
|
| 798 |
+
viewerY = viewerDrag.top + e.clientY - viewerDrag.y
|
| 799 |
+
applyViewerZoom()
|
| 800 |
})
|
| 801 |
$("#viewer-stage").addEventListener("pointerup", e => {
|
| 802 |
viewerDrag = null
|
frontend/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<link rel="icon" href="data:;base64,=">
|
| 7 |
<title>Pixif2</title>
|
| 8 |
-
<link rel="stylesheet" href="style.css?v=
|
| 9 |
</head>
|
| 10 |
<body>
|
| 11 |
<div class="topbar">
|
|
@@ -115,10 +115,10 @@
|
|
| 115 |
</div>
|
| 116 |
<div id="viewer-stage">
|
| 117 |
<button id="viewer-prev" class="viewer-nav" aria-label="Previous result page">‹</button>
|
| 118 |
-
<img id="viewer-img" alt="">
|
| 119 |
<button id="viewer-next" class="viewer-nav" aria-label="Next result page">›</button>
|
| 120 |
</div>
|
| 121 |
</div>
|
| 122 |
-
<script src="app.js?v=
|
| 123 |
</body>
|
| 124 |
</html>
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<link rel="icon" href="data:;base64,=">
|
| 7 |
<title>Pixif2</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css?v=20260720-1">
|
| 9 |
</head>
|
| 10 |
<body>
|
| 11 |
<div class="topbar">
|
|
|
|
| 115 |
</div>
|
| 116 |
<div id="viewer-stage">
|
| 117 |
<button id="viewer-prev" class="viewer-nav" aria-label="Previous result page">‹</button>
|
| 118 |
+
<img id="viewer-img" draggable="false" alt="">
|
| 119 |
<button id="viewer-next" class="viewer-nav" aria-label="Next result page">›</button>
|
| 120 |
</div>
|
| 121 |
</div>
|
| 122 |
+
<script src="app.js?v=20260720-1"></script>
|
| 123 |
</body>
|
| 124 |
</html>
|
frontend/style.css
CHANGED
|
@@ -550,11 +550,7 @@ input:focus, select:focus, textarea:focus { outline: none; border-color: var(--a
|
|
| 550 |
#viewer-stage {
|
| 551 |
position: relative;
|
| 552 |
flex: 1;
|
| 553 |
-
|
| 554 |
-
align-items: flex-start;
|
| 555 |
-
justify-content: center;
|
| 556 |
-
overflow: auto;
|
| 557 |
-
padding: 1rem;
|
| 558 |
cursor: grab;
|
| 559 |
touch-action: none;
|
| 560 |
}
|
|
@@ -576,10 +572,14 @@ input:focus, select:focus, textarea:focus { outline: none; border-color: var(--a
|
|
| 576 |
#viewer-next { right: 1rem; }
|
| 577 |
#viewer-stage.dragging { cursor: grabbing; }
|
| 578 |
#viewer-img {
|
|
|
|
|
|
|
|
|
|
| 579 |
max-width: none;
|
| 580 |
max-height: none;
|
| 581 |
-
transform-origin: top
|
| 582 |
user-select: none;
|
|
|
|
| 583 |
}
|
| 584 |
@media (max-width: 860px) {
|
| 585 |
.topbar { align-items: stretch; min-height: 64px; padding: .7rem 4vw; flex-direction: column; }
|
|
|
|
| 550 |
#viewer-stage {
|
| 551 |
position: relative;
|
| 552 |
flex: 1;
|
| 553 |
+
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
cursor: grab;
|
| 555 |
touch-action: none;
|
| 556 |
}
|
|
|
|
| 572 |
#viewer-next { right: 1rem; }
|
| 573 |
#viewer-stage.dragging { cursor: grabbing; }
|
| 574 |
#viewer-img {
|
| 575 |
+
position: absolute;
|
| 576 |
+
top: 0;
|
| 577 |
+
left: 0;
|
| 578 |
max-width: none;
|
| 579 |
max-height: none;
|
| 580 |
+
transform-origin: top left;
|
| 581 |
user-select: none;
|
| 582 |
+
-webkit-user-drag: none;
|
| 583 |
}
|
| 584 |
@media (max-width: 860px) {
|
| 585 |
.topbar { align-items: stretch; min-height: 64px; padding: .7rem 4vw; flex-direction: column; }
|