|
18 | 18 | cmap2d: document.getElementById("cmap2d"), |
19 | 19 | cycle: document.getElementById("cycle"), |
20 | 20 | seed: document.getElementById("seed"), |
| 21 | + zoomStep: document.getElementById("zoomStep"), |
21 | 22 | }; |
22 | 23 |
|
23 | 24 | const outputs = { |
|
28 | 29 | seed: document.getElementById("seedOut"), |
29 | 30 | cycle: document.getElementById("cycleOut"), |
30 | 31 | offset: document.getElementById("offsetOut"), |
| 32 | + zoomStep: document.getElementById("zoomStepOut"), |
31 | 33 | }; |
32 | 34 | // Viewport state for pan & zoom (rational lattice coordinates). |
33 | 35 | const view = { |
|
106 | 108 | outputs.seed.textContent = o.seed; |
107 | 109 | outputs.cycle.textContent = parseFloat(controls.cycle.value).toFixed(2); |
108 | 110 | outputs.offset.textContent = `${offset.x}, ${offset.y}`; |
| 111 | + outputs.zoomStep.textContent = parseFloat(controls.zoomStep.value).toFixed(3); |
109 | 112 | } |
110 | 113 |
|
111 | 114 | function updateStats(o, result, elapsed) { |
|
211 | 214 | for (const target of Object.keys(sweeping)) { |
212 | 215 | if (!sweeping[target]) continue; |
213 | 216 | any = true; |
| 217 | + // Zoom sweep is special: zoom lives in `view`, not in a range control. |
| 218 | + // Use the zoom granularity slider as a per-frame rate multiplier so the |
| 219 | + // sweep speed respects the same control as manual wheel zooming. |
| 220 | + if (target === "zoom") { |
| 221 | + const zoomStep = parseFloat(controls.zoomStep.value) || 1.1; |
| 222 | + // Per-frame multiplier derived from the granularity (gentler than a |
| 223 | + // full wheel notch so the animation stays smooth). |
| 224 | + const rate = 1 + (zoomStep - 1) * 0.25; |
| 225 | + const zoomMin = 0.001; |
| 226 | + const zoomMax = 1000; |
| 227 | + // dir === 1 => zoom in (decrease lattice-units-per-pixel), |
| 228 | + // dir === -1 => zoom out. Unlike other sweeps, zoom does not bounce; |
| 229 | + // it holds the chosen direction and just clamps at the limits. |
| 230 | + view.zoom *= sweeping[target].dir > 0 ? 1 / rate : rate; |
| 231 | + if (view.zoom <= zoomMin) view.zoom = zoomMin; |
| 232 | + if (view.zoom >= zoomMax) view.zoom = zoomMax; |
| 233 | + continue; |
| 234 | + } |
214 | 235 | const input = controls[target]; |
215 | 236 | const min = parseFloat(input.min); |
216 | 237 | const max = parseFloat(input.max); |
|
229 | 250 | document.querySelectorAll(".sweep-btn").forEach((btn) => { |
230 | 251 | btn.addEventListener("click", () => { |
231 | 252 | const target = btn.dataset.target; |
232 | | - if (sweeping[target]) { |
233 | | - delete sweeping[target]; |
234 | | - btn.classList.remove("active"); |
235 | | - } else { |
236 | | - sweeping[target] = { dir: 1 }; |
237 | | - btn.classList.add("active"); |
238 | | - } |
| 253 | + // Zoom has two buttons (in/out) sharing the "zoom" target. Each button |
| 254 | + // selects a fixed direction; clicking the active one stops the sweep. |
| 255 | + if (target === "zoom") { |
| 256 | + const dir = btn.dataset.dir === "out" ? -1 : 1; |
| 257 | + const alreadyActive = |
| 258 | + sweeping[target] && sweeping[target].dir === dir; |
| 259 | + // Clear any active zoom button state first. |
| 260 | + document |
| 261 | + .querySelectorAll('.sweep-btn[data-target="zoom"]') |
| 262 | + .forEach((b) => b.classList.remove("active")); |
| 263 | + if (alreadyActive) { |
| 264 | + delete sweeping[target]; |
| 265 | + } else { |
| 266 | + sweeping[target] = { dir }; |
| 267 | + btn.classList.add("active"); |
| 268 | + } |
| 269 | + return; |
| 270 | + } |
| 271 | + if (sweeping[target]) { |
| 272 | + delete sweeping[target]; |
| 273 | + btn.classList.remove("active"); |
| 274 | + } else { |
| 275 | + sweeping[target] = { dir: 1 }; |
| 276 | + btn.classList.add("active"); |
| 277 | + } |
239 | 278 | }); |
240 | 279 | }); |
241 | 280 |
|
|
322 | 361 | ev.preventDefault(); |
323 | 362 | const { fx, fy } = clientToFieldPixel(ev); |
324 | 363 | const before = pixelToLattice(fx, fy); |
325 | | - const factor = ev.deltaY < 0 ? 1 / 1.1 : 1.1; |
| 364 | + // Configurable zoom granularity: each wheel notch multiplies/divides |
| 365 | + // the zoom by zoomStep. Values close to 1.0 give finer control. |
| 366 | + const zoomStep = parseFloat(controls.zoomStep.value) || 1.1; |
| 367 | + const factor = ev.deltaY < 0 ? 1 / zoomStep : zoomStep; |
326 | 368 | view.zoom *= factor; |
327 | 369 | // Clamp zoom to a reasonable range. |
328 | 370 | view.zoom = Math.min(Math.max(view.zoom, 0.001), 1000); |
|
0 commit comments