Skip to content

Commit 8d782ef

Browse files
committed
neat fractal
1 parent 087668d commit 8d782ef

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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

Comments
 (0)