Skip to content

Commit e822ab1

Browse files
committed
neat fractal
1 parent 8086edb commit e822ab1

7 files changed

Lines changed: 413 additions & 24 deletions

File tree

experiments/irrational_lattice/colormap.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,62 @@
3838
[60, 78, 122], [97, 104, 165], [149, 137, 191],
3939
[196, 175, 208], [226, 217, 226],
4040
];
41+
const FIRE = [
42+
[0, 0, 0], [60, 0, 0], [120, 10, 0],
43+
[200, 40, 0], [240, 120, 0], [255, 200, 40],
44+
[255, 255, 180], [255, 255, 255],
45+
];
46+
const ICE = [
47+
[0, 0, 20], [0, 20, 60], [0, 60, 120],
48+
[20, 120, 200], [80, 180, 240], [160, 220, 250],
49+
[220, 245, 255], [255, 255, 255],
50+
];
51+
const PLASMA = [
52+
[13, 8, 135], [75, 3, 161], [125, 3, 168],
53+
[168, 34, 150], [203, 70, 121], [229, 107, 93],
54+
[248, 148, 65], [253, 195, 40], [240, 249, 33],
55+
];
56+
// HSV to RGB; h,s,v in [0,1], returns 0..255 triple.
57+
function hsv2rgb(h, s, v) {
58+
h = (h - Math.floor(h)) * 6;
59+
const i = Math.floor(h);
60+
const f = h - i;
61+
const p = v * (1 - s);
62+
const q = v * (1 - s * f);
63+
const t = v * (1 - s * (1 - f));
64+
let r, g, b;
65+
switch (i % 6) {
66+
case 0: r = v; g = t; b = p; break;
67+
case 1: r = q; g = v; b = p; break;
68+
case 2: r = p; g = v; b = t; break;
69+
case 3: r = p; g = q; b = v; break;
70+
case 4: r = t; g = p; b = v; break;
71+
default: r = v; g = p; b = q; break;
72+
}
73+
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
74+
}
75+
4176

4277
export const colormaps = {
4378
viridis: (t) => sampleStops(VIRIDIS, t),
4479
magma: (t) => sampleStops(MAGMA, t),
80+
fire: (t) => sampleStops(FIRE, t),
81+
ice: (t) => sampleStops(ICE, t),
82+
plasma: (t) => sampleStops(PLASMA, t),
4583
twilight: (t) => sampleStops(TWILIGHT, t),
4684
grayscale: (t) => {
4785
const v = Math.round(clamp01(t) * 255);
4886
return [v, v, v];
4987
},
50-
};
88+
// Cycling / disco maps take an optional phase argument.
89+
rainbow: (t, phase = 0) => hsv2rgb(clamp01(t) + phase, 0.9, 1.0),
90+
disco: (t, phase = 0) => hsv2rgb(clamp01(t) * 3 + phase, 1.0, 0.5 + 0.5 * Math.abs(Math.sin((t + phase) * Math.PI * 4))),
91+
neon: (t, phase = 0) => hsv2rgb(clamp01(t) * 0.8 + 0.5 + phase, 1.0, clamp01(0.3 + 0.7 * t)),
92+
};
93+
// 2D colormaps: map (u, v) in [0,1]^2 -> [r,g,b]. phase shifts hue.
94+
export const colormaps2d = {
95+
// u drives hue, v drives brightness.
96+
snap: (u, v, phase = 0) => hsv2rgb(u + phase, 0.85, clamp01(0.15 + 0.85 * v)),
97+
rational_irrational: (u, v, phase = 0) => hsv2rgb(0.6 * u + 0.15 + phase, clamp01(0.4 + 0.6 * v), clamp01(0.25 + 0.75 * v)),
98+
xy_phase: (u, v, phase = 0) => hsv2rgb(u + phase, 0.7, clamp01(0.2 + 0.8 * v)),
99+
};

experiments/irrational_lattice/field.js

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,34 @@
7878
// Compute a scalar field over a grid using the chosen mode.
7979
// Returns Float32Array of length size*size and {min, max}.
8080
export function computeField(opts) {
81-
const { D, size, K, alphaScale, seed, mode, epsilon } = opts;
81+
const { D, size, K, alphaScale, seed, mode, epsilon } = opts;
82+
// Viewport: panX/panY in lattice units, zoom > 0 (lattice units per pixel).
83+
const panX = opts.panX || 0;
84+
const panY = opts.panY || 0;
85+
const zoom = opts.zoom || 1;
86+
const offsetX = opts.offsetX || 0;
87+
const offsetY = opts.offsetY || 0;
88+
const cmap2d = opts.cmap2d || "none";
8289
const field = new QuadField(D);
8390
const params = makeFrequencies(D, K, alphaScale, seed);
8491
const out = new Float32Array(size * size);
92+
// Secondary channel for 2D colormaps (e.g. snap distance).
93+
const useChannel2 = cmap2d && cmap2d !== "none";
94+
const chan2 = useChannel2 ? new Float32Array(size * size) : null;
95+
let min2 = Infinity, max2 = -Infinity;
8596

8697
let minV = Infinity, maxV = -Infinity;
8798
let irrSumSq = 0, irrCount = 0;
8899

89100
for (let j = 0; j < size; j++) {
90101
for (let i = 0; i < size; i++) {
91102
// Center the grid for symmetric viewing.
92-
const x = i - size / 2;
93-
const y = j - size / 2;
94-
const e = etaAt(x, y, params, K);
103+
// Apply pan (in lattice units) and zoom (lattice units per pixel).
104+
const x = (i - size / 2) * zoom + panX;
105+
const y = (j - size / 2) * zoom + panY;
106+
const xo = x + offsetX;
107+
const yo = y + offsetY;
108+
const e = etaAt(xo, yo, params, K);
95109

96110
// e = [ [a_x, b_x], [a_y, b_y] ]
97111
const ax = e[0][0], bx = e[0][1];
@@ -116,8 +130,8 @@
116130
break;
117131
case "snapped": {
118132
// Snap T_eps(x) = x + eps * eta(x) to nearest integer lattice.
119-
const tx = x + epsilon * rx;
120-
const ty = y + epsilon * ry;
133+
const tx = xo + epsilon * rx;
134+
const ty = yo + epsilon * ry;
121135
const sx = Math.round(tx);
122136
const sy = Math.round(ty);
123137
v = Math.hypot(tx - sx, ty - sy);
@@ -132,9 +146,32 @@
132146
out[j * size + i] = v;
133147
if (v < minV) minV = v;
134148
if (v > maxV) maxV = v;
149+
if (useChannel2) {
150+
let v2;
151+
switch (cmap2d) {
152+
case "rational_irrational":
153+
v2 = Math.hypot(ax, ay);
154+
break;
155+
case "xy_phase": {
156+
// Phase angle of the displacement, normalized to [0,1].
157+
v2 = (Math.atan2(ry, rx) + Math.PI) / (2 * Math.PI);
158+
break;
159+
}
160+
case "snap":
161+
default: {
162+
const tx = xo + epsilon * rx;
163+
const ty = yo + epsilon * ry;
164+
v2 = Math.hypot(tx - Math.round(tx), ty - Math.round(ty));
165+
break;
166+
}
167+
}
168+
chan2[j * size + i] = v2;
169+
if (v2 < min2) min2 = v2;
170+
if (v2 > max2) max2 = v2;
171+
}
135172
}
136173
}
137174

138175
const irrRMS = Math.sqrt(irrSumSq / irrCount);
139-
return { data: out, min: minV, max: maxV, irrRMS };
176+
return { data: out, min: minV, max: maxV, irrRMS, chan2, min2, max2 };
140177
}

experiments/irrational_lattice/index.html

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,25 @@ <h1>Algebraic Colored Lattice Field</h1>
5353

5454
<div class="control">
5555
<label>K frequencies: <output id="kOut">3</output></label>
56-
<input type="range" id="K" min="1" max="6" value="3" step="1">
56+
<input type="range" id="K" min="1" max="16" value="3" step="1">
57+
<button class="sweep-btn" data-target="K" title="Sweep K">⟲ sweep</button>
5758
</div>
5859

5960
<div class="control">
6061
<label>Frequency scale &alpha;: <output id="alphaOut">0.080</output></label>
61-
<input type="range" id="alpha" min="0.01" max="0.30" value="0.08" step="0.005">
62+
<input type="range" id="alpha" min="0.001" max="2.0" value="0.08" step="0.001">
63+
<button class="sweep-btn" data-target="alpha" title="Sweep alpha">⟲ sweep</button>
6264
</div>
6365

6466
<div class="control">
6567
<label>Amplitude &epsilon;: <output id="epsOut">0.30</output></label>
66-
<input type="range" id="eps" min="0.0" max="1.0" value="0.30" step="0.01">
68+
<input type="range" id="eps" min="0.0" max="5.0" value="0.30" step="0.01">
69+
<button class="sweep-btn" data-target="eps" title="Sweep epsilon">⟲ sweep</button>
6770
</div>
6871

6972
<div class="control">
7073
<label>Grid size: <output id="sizeOut">256</output></label>
71-
<input type="range" id="size" min="64" max="512" value="256" step="32">
74+
<input type="range" id="size" min="64" max="1024" value="256" step="32">
7275
</div>
7376

7477
<div class="control">
@@ -78,15 +81,48 @@ <h1>Algebraic Colored Lattice Field</h1>
7881
<option value="magma">magma</option>
7982
<option value="grayscale">grayscale</option>
8083
<option value="twilight">twilight</option>
84+
<option value="rainbow">rainbow</option>
85+
<option value="disco">disco</option>
86+
<option value="neon">neon</option>
87+
<option value="fire">fire</option>
88+
<option value="ice">ice</option>
89+
<option value="plasma">plasma</option>
8190
</select>
8291
</div>
92+
<div class="control">
93+
<label>2D color map (hue ⊗ value)</label>
94+
<select id="cmap2d">
95+
<option value="none">none (use 1D map)</option>
96+
<option value="snap" selected>snap distance ⊗ field</option>
97+
<option value="rational_irrational">rational ⊗ irrational</option>
98+
<option value="xy_phase">xy phase ⊗ field</option>
99+
</select>
100+
</div>
101+
<div class="control">
102+
<label>Color cycling speed: <output id="cycleOut">0.00</output></label>
103+
<input type="range" id="cycle" min="0.0" max="2.0" value="0.0" step="0.01">
104+
</div>
105+
83106

84107
<div class="control">
85108
<label>Seed (phase offset): <output id="seedOut">0</output></label>
86-
<input type="range" id="seed" min="0" max="100" value="0" step="1">
109+
<input type="range" id="seed" min="0" max="1000" value="0" step="1">
110+
<button class="sweep-btn" data-target="seed" title="Sweep seed">⟲ sweep</button>
87111
</div>
112+
<div class="control">
113+
<label>Integer offset: <output id="offsetOut">0, 0</output></label>
114+
<div class="offset-buttons">
115+
<button id="offsetXMinus">x-</button>
116+
<button id="offsetXPlus">x+</button>
117+
<button id="offsetYMinus">y-</button>
118+
<button id="offsetYPlus">y+</button>
119+
</div>
120+
</div>
121+
88122

89123
<button id="regen">Regenerate</button>
124+
<button id="resetView">Reset view</button>
125+
<button id="exportPng">Export PNG</button>
90126

91127
<div class="info">
92128
<h3>About</h3>
@@ -101,6 +137,10 @@ <h3>About</h3>
101137
Lower D values and small &alpha; yield long-range moir&eacute;-like
102138
interference reminiscent of Penrose patterns, constrained to the square lattice.
103139
</p>
140+
<p>
141+
<strong>Drag</strong> to pan and <strong>scroll</strong> to zoom into the
142+
rational lattice. Use <em>Reset view</em> to return to the origin.
143+
</p>
104144
<div class="stats" id="stats"></div>
105145
</div>
106146
</aside>

0 commit comments

Comments
 (0)