|
16 | 16 | scale: 1, |
17 | 17 | panX: 0, |
18 | 18 | panY: 0, |
| 19 | + fittedScale: 1, |
| 20 | + fittedPanX: 0, |
| 21 | + fittedPanY: 0, |
19 | 22 | originX: 0, |
20 | 23 | originY: 0, |
21 | 24 | isPanning: false, |
|
64 | 67 |
|
65 | 68 | const scaleX = containerWidth / imageWidth; |
66 | 69 | const scaleY = containerHeight / imageHeight; |
67 | | - this.scale = Math.min(scaleX, scaleY, 1); |
| 70 | + const newScale = Math.min(scaleX, scaleY, 1); |
68 | 71 |
|
69 | | - this.panX = 0; |
70 | | - this.panY = 0; |
| 72 | + // Center the image within the container using top-left origin math |
| 73 | + const centeredPanX = (containerWidth - imageWidth * newScale) / 2; |
| 74 | + const centeredPanY = (containerHeight - imageHeight * newScale) / 2; |
| 75 | + |
| 76 | + this.scale = newScale; |
| 77 | + this.panX = centeredPanX; |
| 78 | + this.panY = centeredPanY; |
| 79 | + |
| 80 | + // Store fitted state for precise toggling |
| 81 | + this.fittedScale = newScale; |
| 82 | + this.fittedPanX = centeredPanX; |
| 83 | + this.fittedPanY = centeredPanY; |
71 | 84 |
|
72 | 85 | this.$nextTick(() => { |
73 | 86 | this.updateBounds(); |
|
119 | 132 | const clickX = e.clientX - rect.left; |
120 | 133 | const clickY = e.clientY - rect.top; |
121 | 134 |
|
122 | | - // Toggle between fit-to-container and configurable zoom at click position |
123 | | - if (this.scale <= 1) { |
124 | | - // Zoom to configurable level at the exact click position |
| 135 | + // Toggle: if we're approximately at fitted state, zoom in, else return to fitted state |
| 136 | + const near = (a, b, eps = 0.01) => Math.abs(a - b) <= eps; |
| 137 | + const isNearFitted = near(this.scale, this.fittedScale) && near(this.panX, this.fittedPanX, 1) && near(this.panY, this.fittedPanY, 1); |
| 138 | + |
| 139 | + if (isNearFitted) { |
| 140 | + // Zoom to configurable level at the exact click position using top-left origin math |
125 | 141 | const newScale = Math.min(this.maxScale, this.doubleClickZoomLevel); |
126 | | - |
127 | | - // Calculate the position on the image where user clicked |
128 | | - // First, get the current image position relative to container |
129 | | - const imageX = clickX - this.panX; |
130 | | - const imageY = clickY - this.panY; |
131 | | - |
132 | | - // Calculate the ratio of click position relative to current image size |
133 | | - const imageWidth = this.$refs.image.naturalWidth * this.scale; |
134 | | - const imageHeight = this.$refs.image.naturalHeight * this.scale; |
135 | | - const clickRatioX = imageX / imageWidth; |
136 | | - const clickRatioY = imageY / imageHeight; |
137 | | - |
138 | | - // Calculate new image dimensions |
139 | | - const newImageWidth = this.$refs.image.naturalWidth * newScale; |
140 | | - const newImageHeight = this.$refs.image.naturalHeight * newScale; |
141 | | - |
142 | | - // Calculate new pan to keep the clicked point in the same position |
143 | | - const newPanX = clickX - (newImageWidth * clickRatioX); |
144 | | - const newPanY = clickY - (newImageHeight * clickRatioY); |
145 | | - |
| 142 | + |
| 143 | + // Image coordinates of the click before scaling |
| 144 | + const u = (clickX - this.panX) / this.scale; |
| 145 | + const v = (clickY - this.panY) / this.scale; |
| 146 | + |
| 147 | + // New pan to keep the clicked point stationary |
| 148 | + const newPanX = clickX - (u * newScale); |
| 149 | + const newPanY = clickY - (v * newScale); |
| 150 | + |
| 151 | + this.scale = newScale; |
146 | 152 | this.panX = newPanX; |
147 | 153 | this.panY = newPanY; |
148 | | - this.scale = newScale; |
149 | 154 | } else { |
150 | | - // Reset to fit container |
151 | | - this.fitToContainer(); |
| 155 | + // Return to fitted state |
| 156 | + this.scale = this.fittedScale; |
| 157 | + this.panX = this.fittedPanX; |
| 158 | + this.panY = this.fittedPanY; |
152 | 159 | } |
153 | | - |
| 160 | + |
154 | 161 | this.constrainPan(); |
155 | 162 | }, |
156 | 163 |
|
|
165 | 172 | const newScale = Math.max(this.minScale, Math.min(this.maxScale, this.scale + delta)); |
166 | 173 |
|
167 | 174 | if (newScale !== this.scale) { |
168 | | - const scaleDiff = newScale - this.scale; |
169 | | - const centerX = rect.width / 2; |
170 | | - const centerY = rect.height / 2; |
171 | | - this.panX -= (x - centerX) * scaleDiff; |
172 | | - this.panY -= (y - centerY) * scaleDiff; |
| 175 | + // Keep cursor point stationary (top-left origin math) |
| 176 | + const u = (x - this.panX) / this.scale; |
| 177 | + const v = (y - this.panY) / this.scale; |
173 | 178 |
|
174 | 179 | this.scale = newScale; |
| 180 | + this.panX = x - (u * newScale); |
| 181 | + this.panY = y - (v * newScale); |
| 182 | + |
175 | 183 | this.constrainPan(); |
176 | 184 | } |
177 | 185 | }, |
178 | 186 |
|
179 | 187 | zoomIn() { |
| 188 | + const rect = this.$refs.container.getBoundingClientRect(); |
| 189 | + const x = rect.width / 2; |
| 190 | + const y = rect.height / 2; |
180 | 191 | const newScale = Math.min(this.maxScale, this.scale + 0.2); |
181 | | - this.scale = newScale; |
182 | | - this.constrainPan(); |
| 192 | + if (newScale !== this.scale) { |
| 193 | + const u = (x - this.panX) / this.scale; |
| 194 | + const v = (y - this.panY) / this.scale; |
| 195 | + this.scale = newScale; |
| 196 | + this.panX = x - (u * newScale); |
| 197 | + this.panY = y - (v * newScale); |
| 198 | + this.constrainPan(); |
| 199 | + } |
183 | 200 | }, |
184 | 201 |
|
185 | 202 | zoomOut() { |
| 203 | + const rect = this.$refs.container.getBoundingClientRect(); |
| 204 | + const x = rect.width / 2; |
| 205 | + const y = rect.height / 2; |
186 | 206 | const newScale = Math.max(this.minScale, this.scale - 0.2); |
187 | | - this.scale = newScale; |
188 | | - this.constrainPan(); |
| 207 | + if (newScale !== this.scale) { |
| 208 | + const u = (x - this.panX) / this.scale; |
| 209 | + const v = (y - this.panY) / this.scale; |
| 210 | + this.scale = newScale; |
| 211 | + this.panX = x - (u * newScale); |
| 212 | + this.panY = y - (v * newScale); |
| 213 | + this.constrainPan(); |
| 214 | + } |
189 | 215 | }, |
190 | 216 |
|
191 | 217 | reset() { |
|
201 | 227 | const containerWidth = container.offsetWidth; |
202 | 228 | const containerHeight = container.offsetHeight; |
203 | 229 |
|
204 | | - const maxPanX = Math.max(0, (imageWidth - containerWidth) / 2); |
205 | | - const maxPanY = Math.max(0, (imageHeight - containerHeight) / 2); |
| 230 | + // If the image is smaller than the container, keep it centered |
| 231 | + if (imageWidth <= containerWidth) { |
| 232 | + this.panX = (containerWidth - imageWidth) / 2; |
| 233 | + } else { |
| 234 | + const minPanX = containerWidth - imageWidth; |
| 235 | + const maxPanX = 0; |
| 236 | + this.panX = Math.max(minPanX, Math.min(maxPanX, this.panX)); |
| 237 | + } |
206 | 238 |
|
207 | | - this.panX = Math.max(-maxPanX, Math.min(maxPanX, this.panX)); |
208 | | - this.panY = Math.max(-maxPanY, Math.min(maxPanY, this.panY)); |
| 239 | + if (imageHeight <= containerHeight) { |
| 240 | + this.panY = (containerHeight - imageHeight) / 2; |
| 241 | + } else { |
| 242 | + const minPanY = containerHeight - imageHeight; |
| 243 | + const maxPanY = 0; |
| 244 | + this.panY = Math.max(minPanY, Math.min(maxPanY, this.panY)); |
| 245 | + } |
209 | 246 | }, |
210 | 247 |
|
211 | 248 | startTouch(e) { |
|
0 commit comments