Skip to content

Commit 4f0d5d5

Browse files
committed
neat fractal
1 parent 9b38ad7 commit 4f0d5d5

2 files changed

Lines changed: 57 additions & 8 deletions

File tree

experiments/irrational_lattice/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ <h1>Algebraic Colored Lattice Field</h1>
8989
<label>Grid size: <output id="sizeOut">256</output></label>
9090
<input type="range" id="size" min="64" max="1024" value="256" step="32">
9191
</div>
92+
<div class="control">
93+
<label>Zoom granularity: <output id="zoomStepOut">1.100</output></label>
94+
<input type="range" id="zoomStep" min="1.005" max="1.5" value="1.1" step="0.005">
95+
<button class="sweep-btn" data-target="zoom" data-dir="in" title="Sweep zoom in">⟲ in</button>
96+
<button class="sweep-btn" data-target="zoom" data-dir="out" title="Sweep zoom out">⟲ out</button>
97+
</div>
98+
9299

93100
<div class="control">
94101
<label>Color map</label>

experiments/irrational_lattice/main.js

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
cmap2d: document.getElementById("cmap2d"),
1919
cycle: document.getElementById("cycle"),
2020
seed: document.getElementById("seed"),
21+
zoomStep: document.getElementById("zoomStep"),
2122
};
2223

2324
const outputs = {
@@ -28,6 +29,7 @@
2829
seed: document.getElementById("seedOut"),
2930
cycle: document.getElementById("cycleOut"),
3031
offset: document.getElementById("offsetOut"),
32+
zoomStep: document.getElementById("zoomStepOut"),
3133
};
3234
// Viewport state for pan & zoom (rational lattice coordinates).
3335
const view = {
@@ -106,6 +108,7 @@
106108
outputs.seed.textContent = o.seed;
107109
outputs.cycle.textContent = parseFloat(controls.cycle.value).toFixed(2);
108110
outputs.offset.textContent = `${offset.x}, ${offset.y}`;
111+
outputs.zoomStep.textContent = parseFloat(controls.zoomStep.value).toFixed(3);
109112
}
110113

111114
function updateStats(o, result, elapsed) {
@@ -211,6 +214,24 @@
211214
for (const target of Object.keys(sweeping)) {
212215
if (!sweeping[target]) continue;
213216
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+
}
214235
const input = controls[target];
215236
const min = parseFloat(input.min);
216237
const max = parseFloat(input.max);
@@ -229,13 +250,31 @@
229250
document.querySelectorAll(".sweep-btn").forEach((btn) => {
230251
btn.addEventListener("click", () => {
231252
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+
}
239278
});
240279
});
241280

@@ -322,7 +361,10 @@
322361
ev.preventDefault();
323362
const { fx, fy } = clientToFieldPixel(ev);
324363
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;
326368
view.zoom *= factor;
327369
// Clamp zoom to a reasonable range.
328370
view.zoom = Math.min(Math.max(view.zoom, 0.001), 1000);

0 commit comments

Comments
 (0)