Skip to content

Commit 336990e

Browse files
committed
neat fractal
1 parent 0cc7634 commit 336990e

3 files changed

Lines changed: 101 additions & 5 deletions

File tree

experiments/irrational_lattice/index.html

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,31 @@ <h1>Algebraic Colored Lattice Field</h1>
8787

8888
<div class="control">
8989
<label>Grid size: <output id="sizeOut">256</output></label>
90-
<input type="range" id="size" min="64" max="1024" value="256" step="32">
90+
<input type="hidden" id="size" value="256">
91+
<div class="int-stepper">
92+
<button id="sizeMinus" title="Decrease grid size"></button>
93+
<input type="range" id="sizeInput" min="1" max="1024" value="256" step="1">
94+
<button id="sizePlus" title="Increase grid size">+</button>
95+
</div>
9196
</div>
9297
<div class="control">
9398
<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">
99+
<input type="range" id="zoomStep" min="1.001" max="4.0" value="1.1" step="0.001">
100+
<div class="rational-stepper">
101+
<div class="rs-frac">
102+
<div class="int-stepper">
103+
<button id="zoomNumMinus" title="Decrease numerator"></button>
104+
<input type="number" id="zoomNum" min="1" max="100000" value="11" step="1">
105+
<button id="zoomNumPlus" title="Increase numerator">+</button>
106+
</div>
107+
<span class="rs-bar">/</span>
108+
<div class="int-stepper">
109+
<button id="zoomDenMinus" title="Decrease denominator"></button>
110+
<input type="number" id="zoomDen" min="1" max="100000" value="10" step="1">
111+
<button id="zoomDenPlus" title="Increase denominator">+</button>
112+
</div>
113+
</div>
114+
</div>
95115
<button class="sweep-btn" data-target="zoom" data-dir="in" title="Sweep zoom in">⟲ in</button>
96116
<button class="sweep-btn" data-target="zoom" data-dir="out" title="Sweep zoom out">⟲ out</button>
97117
</div>

experiments/irrational_lattice/main.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { computeField } from "./field.js";
44
import { renderField } from "./render.js";
55
import { computeFFT2D, renderFFT3D } from "./fft.js";
66
import { topAutocorrVectors } from "./autocorr.js";
7+
import { wireRationalControls } from "./rational.js";
78

89
const canvas = document.getElementById("field");
910
const statsEl = document.getElementById("stats");
@@ -46,6 +47,18 @@ const REF_SIZE = 512;
4647
function effectiveZoom(size) {
4748
return (view.zoom * REF_SIZE) / size;
4849
}
50+
// The active zoom granularity is a rational p/q controlled by the
51+
// numerator/denominator steppers (the slider only seeds an approximation).
52+
const zoomNumEl = document.getElementById("zoomNum");
53+
const zoomDenEl = document.getElementById("zoomDen");
54+
function zoomGranularity() {
55+
const num = parseFloat(zoomNumEl.value) || 1;
56+
const den = parseFloat(zoomDenEl.value) || 1;
57+
const v = num / den;
58+
// Accept any finite ratio strictly greater than 1. Do not pin to a
59+
// fixed fallback range; the user may enter arbitrary num/den.
60+
return isFinite(v) && v > 1 ? v : 1.0001;
61+
}
4962

5063
// Integer offset state (paged through with buttons).
5164
const offset = { x: 0, y: 0 };
@@ -118,7 +131,6 @@ function updateOutputs(o) {
118131
outputs.seed.textContent = o.seed;
119132
outputs.cycle.textContent = parseFloat(controls.cycle.value).toFixed(2);
120133
outputs.offset.textContent = `${offset.x}, ${offset.y}`;
121-
outputs.zoomStep.textContent = parseFloat(controls.zoomStep.value).toFixed(3);
122134
}
123135

124136
function updateStats(o, result, elapsed) {
@@ -175,10 +187,25 @@ function rerenderColor() {
175187

176188
// Wire up listeners.
177189
for (const key of Object.keys(controls)) {
190+
// `size` and `zoomStep` are managed by the rational view controls, which
191+
// update the hidden inputs and emit their own change events.
192+
if (key === "size" || key === "zoomStep") continue;
178193
controls[key].addEventListener("input", regenerate);
179194
controls[key].addEventListener("change", regenerate);
180195
}
181196
document.getElementById("regen").addEventListener("click", regenerate);
197+
// Wire the rational view controls (integer grid size + rational zoom
198+
// granularity p/q seeded from the slider via continued fractions).
199+
const rationalControls = wireRationalControls({
200+
maxDen: 100,
201+
onChange: () => {
202+
// Keep the legacy zoomStep output (if present) in sync, then redraw.
203+
if (outputs.zoomStep) {
204+
outputs.zoomStep.textContent = zoomGranularity().toFixed(3);
205+
}
206+
regenerate();
207+
},
208+
});
182209
document.getElementById("resetView").addEventListener("click", () => {
183210
view.panX = 0;
184211
view.panY = 0;
@@ -222,7 +249,7 @@ function stepSweeps() {
222249
// Use the zoom granularity slider as a per-frame rate multiplier so the
223250
// sweep speed respects the same control as manual wheel zooming.
224251
if (target === "zoom") {
225-
const zoomStep = parseFloat(controls.zoomStep.value) || 1.1;
252+
const zoomStep = zoomGranularity();
226253
// Per-frame multiplier derived from the granularity (gentler than a
227254
// full wheel notch so the animation stays smooth).
228255
const rate = 1 + (zoomStep - 1) * 0.25;
@@ -421,7 +448,7 @@ canvas.addEventListener("wheel", (ev) => {
421448
const before = pixelToLattice(fx, fy);
422449
// Configurable zoom granularity: each wheel notch multiplies/divides
423450
// the zoom by zoomStep. Values close to 1.0 give finer control.
424-
const zoomStep = parseFloat(controls.zoomStep.value) || 1.1;
451+
const zoomStep = zoomGranularity();
425452
const factor = ev.deltaY < 0 ? 1 / zoomStep : zoomStep;
426453
view.zoom *= factor;
427454
// Clamp zoom to a reasonable range.
@@ -658,6 +685,8 @@ acShow.addEventListener("click", () => {
658685
// Initial render.
659686
fitCanvas();
660687
hashToState();
688+
// Reflect any hash-restored size into the rational stepper display.
689+
if (rationalControls && rationalControls.refresh) rationalControls.refresh();
661690
regenerate();
662691
// Respond to external hash changes (shared link pasted, back/forward nav).
663692
window.addEventListener("hashchange", () => {

experiments/irrational_lattice/styles.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,53 @@
9797
padding: 0.35rem;
9898
font-size: 0.8rem;
9999
}
100+
/* --- Integer stepper (numerator-style control) --- */
101+
.int-stepper {
102+
display: flex;
103+
align-items: stretch;
104+
gap: 0.25rem;
105+
}
106+
.int-stepper button {
107+
flex: 0 0 auto;
108+
width: 2rem;
109+
padding: 0.25rem 0;
110+
font-size: 1rem;
111+
line-height: 1;
112+
background: var(--panel-2);
113+
color: var(--accent);
114+
border: 1px solid var(--border);
115+
}
116+
.int-stepper button:hover { filter: brightness(1.2); }
117+
.int-stepper input[type="number"] {
118+
flex: 1 1 0;
119+
width: 100%;
120+
min-width: 0;
121+
max-width: 100%;
122+
background: var(--panel-2);
123+
color: var(--fg);
124+
border: 1px solid var(--border);
125+
border-radius: 4px;
126+
padding: 0.35rem 0.5rem;
127+
font-size: 0.85rem;
128+
font-variant-numeric: tabular-nums;
129+
text-align: center;
130+
}
131+
/* --- Rational stepper (p / q control) --- */
132+
.rational-stepper {
133+
margin-top: 0.4rem;
134+
}
135+
.rs-frac {
136+
display: flex;
137+
align-items: center;
138+
gap: 0.4rem;
139+
}
140+
.rs-frac .int-stepper { flex: 1 1 0; }
141+
.rs-bar {
142+
color: var(--muted);
143+
font-size: 1.2rem;
144+
font-weight: 600;
145+
padding: 0 0.1rem;
146+
}
100147

101148
.legend {
102149
display: flex;

0 commit comments

Comments
 (0)