1+ // Autocorrelation analysis for the lattice field.
2+ //
3+ // We compute the 2D autocorrelation of the (downsampled) field via FFT,
4+ // then locate the strongest off-center peaks. The displacement vectors to
5+ // those peaks (in lattice units) make natural "step" directions for a
6+ // random walk that tends to land the viewport on self-similar features.
7+
8+ import { computeFFT2D } from "./fft.js" ;
9+
10+ // Radix-2 in-place FFT (re/im arrays). Mirrors the one in fft.js but kept
11+ // local so this module is self-contained for inverse transforms.
12+ function fft1d ( re , im , inverse ) {
13+ const n = re . length ;
14+ for ( let i = 1 , j = 0 ; i < n ; i ++ ) {
15+ let bit = n >> 1 ;
16+ for ( ; j & bit ; bit >>= 1 ) j ^= bit ;
17+ j ^= bit ;
18+ if ( i < j ) {
19+ const tr = re [ i ] ; re [ i ] = re [ j ] ; re [ j ] = tr ;
20+ const ti = im [ i ] ; im [ i ] = im [ j ] ; im [ j ] = ti ;
21+ }
22+ }
23+ for ( let len = 2 ; len <= n ; len <<= 1 ) {
24+ const ang = ( inverse ? 2 : - 2 ) * Math . PI / len ;
25+ const wpr = Math . cos ( ang ) ;
26+ const wpi = Math . sin ( ang ) ;
27+ for ( let i = 0 ; i < n ; i += len ) {
28+ let wr = 1 , wi = 0 ;
29+ for ( let k = 0 ; k < len / 2 ; k ++ ) {
30+ const a = i + k ;
31+ const b = i + k + len / 2 ;
32+ const tr = wr * re [ b ] - wi * im [ b ] ;
33+ const ti = wr * im [ b ] + wi * re [ b ] ;
34+ re [ b ] = re [ a ] - tr ;
35+ im [ b ] = im [ a ] - ti ;
36+ re [ a ] += tr ;
37+ im [ a ] += ti ;
38+ const nwr = wr * wpr - wi * wpi ;
39+ wi = wr * wpi + wi * wpr ;
40+ wr = nwr ;
41+ }
42+ }
43+ }
44+ }
45+
46+ function largestPow2LE ( n ) {
47+ let p = 1 ;
48+ while ( p * 2 <= n ) p *= 2 ;
49+ return p ;
50+ }
51+
52+ function fft2d ( re , im , N , inverse ) {
53+ const row = new Float64Array ( N ) ;
54+ const rowI = new Float64Array ( N ) ;
55+ for ( let y = 0 ; y < N ; y ++ ) {
56+ for ( let x = 0 ; x < N ; x ++ ) { row [ x ] = re [ y * N + x ] ; rowI [ x ] = im [ y * N + x ] ; }
57+ fft1d ( row , rowI , inverse ) ;
58+ for ( let x = 0 ; x < N ; x ++ ) { re [ y * N + x ] = row [ x ] ; im [ y * N + x ] = rowI [ x ] ; }
59+ }
60+ const col = new Float64Array ( N ) ;
61+ const colI = new Float64Array ( N ) ;
62+ for ( let x = 0 ; x < N ; x ++ ) {
63+ for ( let y = 0 ; y < N ; y ++ ) { col [ y ] = re [ y * N + x ] ; colI [ y ] = im [ y * N + x ] ; }
64+ fft1d ( col , colI , inverse ) ;
65+ for ( let y = 0 ; y < N ; y ++ ) { re [ y * N + x ] = col [ y ] ; im [ y * N + x ] = colI [ y ] ; }
66+ }
67+ }
68+
69+ // Compute the top-2 autocorrelation displacement vectors (in lattice units).
70+ // `data` is a Float32Array (size*size). `zoom` converts pixel lags into
71+ // lattice units. Returns an array of up to 2 {dx, dy, strength} entries.
72+ export function topAutocorrVectors ( data , size , zoom , maxN = 64 ) {
73+ const N = largestPow2LE ( Math . min ( size , maxN ) ) ;
74+ const re = new Float64Array ( N * N ) ;
75+ const im = new Float64Array ( N * N ) ;
76+
77+ // Downsample and remove mean (so DC doesn't dominate autocorrelation).
78+ const scale = size / N ;
79+ let mean = 0 ;
80+ for ( let y = 0 ; y < N ; y ++ ) {
81+ for ( let x = 0 ; x < N ; x ++ ) {
82+ const sx = Math . floor ( x * scale ) ;
83+ const sy = Math . floor ( y * scale ) ;
84+ const v = data [ sy * size + sx ] ;
85+ re [ y * N + x ] = v ;
86+ mean += v ;
87+ }
88+ }
89+ mean /= N * N ;
90+ for ( let i = 0 ; i < N * N ; i ++ ) re [ i ] -= mean ;
91+
92+ // Autocorrelation = IFFT( |FFT(x)|^2 ).
93+ fft2d ( re , im , N , false ) ;
94+ for ( let i = 0 ; i < N * N ; i ++ ) {
95+ const p = re [ i ] * re [ i ] + im [ i ] * im [ i ] ;
96+ re [ i ] = p ;
97+ im [ i ] = 0 ;
98+ }
99+ fft2d ( re , im , N , true ) ;
100+ // Normalize inverse transform.
101+ for ( let i = 0 ; i < N * N ; i ++ ) re [ i ] /= N * N ;
102+
103+ // fftshift so zero-lag is centered.
104+ const half = N / 2 ;
105+ const ac = new Float64Array ( N * N ) ;
106+ for ( let y = 0 ; y < N ; y ++ ) {
107+ for ( let x = 0 ; x < N ; x ++ ) {
108+ const sx = ( x + half ) % N ;
109+ const sy = ( y + half ) % N ;
110+ ac [ sy * N + sx ] = re [ y * N + x ] ;
111+ }
112+ }
113+
114+ // Find local maxima away from the central (zero-lag) peak.
115+ const center = half ;
116+ const minR = 2 ; // ignore tiny lags near the center
117+ const candidates = [ ] ;
118+ for ( let y = 1 ; y < N - 1 ; y ++ ) {
119+ for ( let x = 1 ; x < N - 1 ; x ++ ) {
120+ const dx = x - center ;
121+ const dy = y - center ;
122+ if ( Math . hypot ( dx , dy ) < minR ) continue ;
123+ const v = ac [ y * N + x ] ;
124+ // Local maximum test (3x3 neighborhood).
125+ let isMax = true ;
126+ for ( let oy = - 1 ; oy <= 1 && isMax ; oy ++ ) {
127+ for ( let ox = - 1 ; ox <= 1 ; ox ++ ) {
128+ if ( ox === 0 && oy === 0 ) continue ;
129+ if ( ac [ ( y + oy ) * N + ( x + ox ) ] > v ) { isMax = false ; break ; }
130+ }
131+ }
132+ if ( isMax && v > 0 ) candidates . push ( { x, y, v, dx, dy } ) ;
133+ }
134+ }
135+ candidates . sort ( ( a , b ) => b . v - a . v ) ;
136+
137+ // Pick the top 2 that aren't near-duplicates / mirror images.
138+ const picked = [ ] ;
139+ for ( const c of candidates ) {
140+ let dup = false ;
141+ for ( const p of picked ) {
142+ if ( Math . hypot ( c . dx - p . rdx , c . dy - p . rdy ) < minR ||
143+ Math . hypot ( c . dx + p . rdx , c . dy + p . rdy ) < minR ) { dup = true ; break ; }
144+ }
145+ if ( dup ) continue ;
146+ picked . push ( { rdx : c . dx , rdy : c . dy , strength : c . v } ) ;
147+ if ( picked . length >= 2 ) break ;
148+ }
149+
150+ // Convert pixel lags into lattice units (each pixel lag in the downsampled
151+ // grid corresponds to `scale` source pixels, each `zoom` lattice units).
152+ return picked . map ( ( p ) => ( {
153+ dx : p . rdx * scale * zoom ,
154+ dy : p . rdy * scale * zoom ,
155+ strength : p . strength ,
156+ } ) ) ;
157+ }
0 commit comments