@@ -4,6 +4,7 @@ import { computeField } from "./field.js";
44import { renderField } from "./render.js" ;
55import { computeFFT2D , renderFFT3D } from "./fft.js" ;
66import { topAutocorrVectors } from "./autocorr.js" ;
7+ import { wireRationalControls } from "./rational.js" ;
78
89const canvas = document . getElementById ( "field" ) ;
910const statsEl = document . getElementById ( "stats" ) ;
@@ -46,6 +47,18 @@ const REF_SIZE = 512;
4647function 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).
5164const 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
124136function updateStats ( o , result , elapsed ) {
@@ -175,10 +187,25 @@ function rerenderColor() {
175187
176188// Wire up listeners.
177189for ( 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}
181196document . 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+ } ) ;
182209document . 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.
659686fitCanvas ( ) ;
660687hashToState ( ) ;
688+ // Reflect any hash-restored size into the rational stepper display.
689+ if ( rationalControls && rationalControls . refresh ) rationalControls . refresh ( ) ;
661690regenerate ( ) ;
662691// Respond to external hash changes (shared link pasted, back/forward nav).
663692window . addEventListener ( "hashchange" , ( ) => {
0 commit comments