Skip to content

Commit 4d01eed

Browse files
committed
wip
1 parent 5033f31 commit 4d01eed

2 files changed

Lines changed: 205 additions & 31 deletions

File tree

experiments/symmetry_simple/app.js

Lines changed: 185 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
// the smaller grid dimension.
4343
latticeRadius: 0, // 0 = auto (min(W,H)/2 - 1)
4444
latticeCurvature: 1, // scale factor on the metric (k); larger = more curved
45+
// Global signed color permutation applied to symmetry-induced linkages.
46+
// Each entry is { src: 0|1|2, sign: +1|-1 } meaning the output channel
47+
// takes ±(value of source channel). The default identity is r,g,b.
48+
// A negated channel means "flipped around 0.5": -c -> 1 - c.
49+
colorPerm: [
50+
{ src: 0, sign: 1 },
51+
{ src: 1, sign: 1 },
52+
{ src: 2, sign: 1 },
53+
],
4554
symmetry: {
4655
translationX: false,
4756
translationY: false,
@@ -57,6 +66,65 @@
5766
},
5867
};
5968
for (const a of ROT_ANGLES) state.symmetry.rotations[a] = false;
69+
// ── Color-permutation helpers ──────────────
70+
// The permutation P is a signed 3x3 monomial matrix acting on RGB values.
71+
// Negation is interpreted as flip-around-0.5, so the affine action is:
72+
// out[i] = sign_i * (in[src_i] - 0.5) + 0.5
73+
// For composition we work in centered space (value - 0.5).
74+
const IDENTITY_PERM = [
75+
{ src: 0, sign: 1 },
76+
{ src: 1, sign: 1 },
77+
{ src: 2, sign: 1 },
78+
];
79+
function applyPerm(perm, rgb) {
80+
const r = rgb[0] - 0.5, g = rgb[1] - 0.5, b = rgb[2] - 0.5;
81+
const c = [r, g, b];
82+
return [
83+
perm[0].sign * c[perm[0].src] + 0.5,
84+
perm[1].sign * c[perm[1].src] + 0.5,
85+
perm[2].sign * c[perm[2].src] + 0.5,
86+
];
87+
}
88+
// Compose perms: (a ∘ b)(x) = a(b(x))
89+
function composePerm(a, b) {
90+
const out = new Array(3);
91+
for (let i = 0; i < 3; i++) {
92+
const ai = a[i];
93+
const bi = b[ai.src];
94+
out[i] = { src: bi.src, sign: ai.sign * bi.sign };
95+
}
96+
return out;
97+
}
98+
function permPow(p, k) {
99+
let out = IDENTITY_PERM.map(e => ({ ...e }));
100+
if (k === 0) return out;
101+
const sign = k > 0 ? 1 : -1;
102+
const n = Math.abs(k);
103+
let base = sign === 1 ? p : invertPerm(p);
104+
for (let i = 0; i < n; i++) out = composePerm(out, base);
105+
return out;
106+
}
107+
function invertPerm(p) {
108+
const out = new Array(3);
109+
for (let i = 0; i < 3; i++) {
110+
// p sends src -> i with sign s; inverse sends i -> src with sign s
111+
out[p[i].src] = { src: i, sign: p[i].sign };
112+
}
113+
return out;
114+
}
115+
function isIdentityPerm(p) {
116+
for (let i = 0; i < 3; i++) {
117+
if (p[i].src !== i || p[i].sign !== 1) return false;
118+
}
119+
return true;
120+
}
121+
// Channel-by-channel application: returns sign and source channel for output `c`.
122+
// Used inside hot diffusion loops to avoid array allocation.
123+
function permEntry(perm, c) { return perm[c]; }
124+
function permSignature(p) {
125+
return p.map(e => `${e.sign > 0 ? '+' : '-'}${e.src}`).join(',');
126+
}
127+
60128

61129
let animHandle = null;
62130
let lastFrameTime = 0;
@@ -257,7 +325,9 @@
257325

258326
// Returns list of [x,y] mirror positions for a given (x,y)
259327
function symmetryPositions(x, y) {
260-
const positions = new Set();
328+
// Returns array of { x, y, perm } where perm is the color permutation to
329+
// apply to the source colour before depositing at (x,y).
330+
const seen = new Map(); // key "x,y" -> perm (first one wins)
261331

262332
const sym = state.symmetry;
263333
const W = GRID_W, H = GRID_H;
@@ -268,11 +338,16 @@
268338
const cx = (W - 1) / 2;
269339
const cy = (H - 1) / 2;
270340

271-
const seeds = [[x, y]];
341+
// seeds: [px, py, perm] where perm is the permutation applied to the
342+
// *source* colour to obtain the colour deposited at (px, py). The
343+
// identity seed is (x, y, identity).
344+
const P = state.colorPerm;
345+
const ID = IDENTITY_PERM;
346+
const seeds = [[x, y, ID]];
272347

273-
if (sym.mirrorX) seeds.push([mx, y]);
274-
if (sym.mirrorY) seeds.push([x, my]);
275-
if (sym.mirrorX && sym.mirrorY) seeds.push([mx, my]);
348+
if (sym.mirrorX) seeds.push([mx, y, P]);
349+
if (sym.mirrorY) seeds.push([x, my, P]);
350+
if (sym.mirrorX && sym.mirrorY) seeds.push([mx, my, composePerm(P, P)]);
276351

277352

278353

@@ -286,14 +361,15 @@
286361
seeds.push([
287362
Math.round(cx + dx * cos - dy * sin),
288363
Math.round(cy + dx * sin + dy * cos),
364+
P,
289365
]);
290366
}
291367
}
292368

293369
if (sym.diagonal) {
294-
seeds.push([y, x]);
370+
seeds.push([y, x, P]);
295371
// anti-diagonal reflection: (x,y) -> (H-1-y, W-1-x) — only add when both mirrors active
296-
if (sym.mirrorX && sym.mirrorY) seeds.push([my, mx]);
372+
if (sym.mirrorX && sym.mirrorY) seeds.push([my, mx, composePerm(P, P)]);
297373
}
298374
// Lattice translation symmetries (fractional vectors in [0,1]).
299375
// For each (tx,ty) we add seeds at integer multiples ±k of the vector,
@@ -307,29 +383,43 @@
307383
if (Math.abs(dxCells) < 1e-6 && Math.abs(dyCells) < 1e-6) continue;
308384
// Replicate up to a reasonable number of multiples
309385
const maxK = 8;
310-
for (const [bx, by] of baseSeeds) {
386+
for (const [bx, by, bp] of baseSeeds) {
311387
for (let k = 1; k <= maxK; k++) {
312-
seeds.push([bx + dxCells * k, by + dyCells * k]);
313-
seeds.push([bx - dxCells * k, by - dyCells * k]);
388+
seeds.push([bx + dxCells * k, by + dyCells * k, composePerm(bp, permPow(P, k))]);
389+
seeds.push([bx - dxCells * k, by - dyCells * k, composePerm(bp, permPow(P, -k))]);
314390
}
315391
}
316392
}
317393
}
318394

319395

320-
for (const [sx, sy] of seeds) {
321-
addPos(positions, sx, sy);
396+
const tryAdd = (sx, sy, perm) => {
397+
const ix = Math.round(sx), iy = Math.round(sy);
398+
const cxw = state.symmetry.translationX ? wrapX(ix) : ix;
399+
const cyw = state.symmetry.translationY ? wrapY(iy) : iy;
400+
if (cxw < 0 || cxw >= GRID_W || cyw < 0 || cyw >= GRID_H) return;
401+
if (activeMask && !activeMask[cyw * GRID_W + cxw]) return;
402+
const key = cxw + ',' + cyw;
403+
if (!seen.has(key)) seen.set(key, perm);
404+
};
405+
for (const [sx, sy, perm] of seeds) {
406+
tryAdd(sx, sy, perm);
322407
if (sym.translationX) {
323-
addPos(positions, sx + Math.floor(W / 2), sy);
324-
addPos(positions, sx - Math.floor(W / 2), sy);
408+
tryAdd(sx + Math.floor(W / 2), sy, perm);
409+
tryAdd(sx - Math.floor(W / 2), sy, perm);
325410
}
326411
if (sym.translationY) {
327-
addPos(positions, sx, sy + Math.floor(H / 2));
328-
addPos(positions, sx, sy - Math.floor(H / 2));
412+
tryAdd(sx, sy + Math.floor(H / 2), perm);
413+
tryAdd(sx, sy - Math.floor(H / 2), perm);
329414
}
330415
}
331416

332-
return [...positions].map(s => s.split(',').map(Number));
417+
const result = [];
418+
for (const [key, perm] of seen) {
419+
const [xs, ys] = key.split(',').map(Number);
420+
result.push({ x: xs, y: ys, perm });
421+
}
422+
return result;
333423
}
334424

335425
// ── Drawing ────────────────────────────────
@@ -401,7 +491,7 @@
401491
const dist = latticeDist(x, y, wx, wy);
402492
if (!isFinite(dist) || dist <= 0) continue;
403493
const weight = 1 / dist;
404-
neighbors.push({ x: wx, y: wy, w: weight });
494+
neighbors.push({ x: wx, y: wy, w: weight, perm: IDENTITY_PERM });
405495
}
406496

407497
// Symmetry-induced extra connections
@@ -410,9 +500,13 @@
410500
const mx = W - 1 - x, my = H - 1 - y;
411501
const cx = (W - 1) / 2, cy = (H - 1) / 2;
412502
const symPeers = [];
503+
// Each symPeer is [px, py, perm] — perm is the colour permutation to
504+
// apply to the peer's value before it acts on this cell.
505+
const P = state.colorPerm;
506+
const ID = IDENTITY_PERM;
413507

414-
if (sym.mirrorX) symPeers.push([mx, y]);
415-
if (sym.mirrorY) symPeers.push([x, my]);
508+
if (sym.mirrorX) symPeers.push([mx, y, P]);
509+
if (sym.mirrorY) symPeers.push([x, my, P]);
416510
// Per-angle rotational symmetry peers
417511
{
418512
const dx = x - cx, dy = y - cy;
@@ -423,11 +517,12 @@
423517
symPeers.push([
424518
cx + dx * cos - dy * sin,
425519
cy + dx * sin + dy * cos,
520+
P,
426521
]);
427522
}
428523
}
429-
if (sym.diagonal) symPeers.push([y, x]); // already integer
430-
if (sym.diagonal && sym.mirrorX && sym.mirrorY) symPeers.push([my, mx]); // anti-diagonal
524+
if (sym.diagonal) symPeers.push([y, x, P]); // already integer
525+
if (sym.diagonal && sym.mirrorX && sym.mirrorY) symPeers.push([my, mx, composePerm(P, P)]);
431526
// Lattice translation symmetry peers: ±k * (tx*W, ty*H)
432527
{
433528
const latTrans = sym.latticeTranslations || [];
@@ -437,13 +532,13 @@
437532
if (Math.abs(dxCells) < 1e-6 && Math.abs(dyCells) < 1e-6) continue;
438533
const maxK = 4;
439534
for (let k = 1; k <= maxK; k++) {
440-
symPeers.push([x + dxCells * k, y + dyCells * k]);
441-
symPeers.push([x - dxCells * k, y - dyCells * k]);
535+
symPeers.push([x + dxCells * k, y + dyCells * k, permPow(P, k)]);
536+
symPeers.push([x - dxCells * k, y - dyCells * k, permPow(P, -k)]);
442537
}
443538
}
444539
}
445540
// Helper: add bilinear (nearest-4) weighted connections for a fractional peer position
446-
function addBilinearNeighbors(fx, fy, baseWeight) {
541+
function addBilinearNeighbors(fx, fy, baseWeight, perm) {
447542
const x0 = Math.floor(fx), y0 = Math.floor(fy);
448543
const x1 = x0 + 1, y1 = y0 + 1;
449544
const tx = fx - x0, ty = fy - y0;
@@ -457,13 +552,13 @@
457552
if (w < 1e-6) continue;
458553
if (cx < 0 || cx >= GRID_W || cy < 0 || cy >= GRID_H) continue;
459554
if (cx === x && cy === y) continue;
460-
neighbors.push({ x: cx, y: cy, w: baseWeight * w });
555+
neighbors.push({ x: cx, y: cy, w: baseWeight * w, perm });
461556
}
462557
}
463558

464559

465-
for (const [sx, sy] of symPeers) {
466-
addBilinearNeighbors(sx, sy, 1.0);
560+
for (const [sx, sy, perm] of symPeers) {
561+
addBilinearNeighbors(sx, sy, 1.0, perm);
467562
}
468563

469564
return neighbors;
@@ -510,9 +605,16 @@
510605
for (const nb of neighbors) {
511606
const j = idx(nb.x, nb.y);
512607
const wn = nb.w / (totalW || 1);
513-
nr += rate * wn * (pixels[j] - pixels[i]);
514-
ng += rate * wn * (pixels[j + 1] - pixels[i + 1]);
515-
nb2 += rate * wn * (pixels[j + 2] - pixels[i + 2]);
608+
const perm = nb.perm || IDENTITY_PERM;
609+
// Permuted peer value (in centered space, then re-shifted):
610+
// pv[c] = sign_c * (pixels[j + src_c] - 0.5) + 0.5
611+
const pe0 = perm[0], pe1 = perm[1], pe2 = perm[2];
612+
const pv0 = pe0.sign * (pixels[j + pe0.src] - 0.5) + 0.5;
613+
const pv1 = pe1.sign * (pixels[j + pe1.src] - 0.5) + 0.5;
614+
const pv2 = pe2.sign * (pixels[j + pe2.src] - 0.5) + 0.5;
615+
nr += rate * wn * (pv0 - pixels[i]);
616+
ng += rate * wn * (pv1 - pixels[i + 1]);
617+
nb2 += rate * wn * (pv2 - pixels[i + 2]);
516618
}
517619

518620
next[i] = nr;
@@ -820,6 +922,7 @@
820922
s.translationX|0, s.translationY|0, s.mirrorX|0, s.mirrorY|0,
821923
s.diagonal|0, rotKey,
822924
latKey,
925+
permSignature(state.colorPerm),
823926
state.spectralK,
824927
].join(':');
825928
}
@@ -1237,6 +1340,57 @@
12371340
[0.25, Math.sqrt(3) / 4],
12381341
]));
12391342
renderLatTransList();
1343+
// ── Color permutation UI ──────────────────
1344+
const PERM_OPTIONS = [
1345+
{ v: '+0', label: '+r' }, { v: '-0', label: '−r' },
1346+
{ v: '+1', label: '+g' }, { v: '-1', label: '−g' },
1347+
{ v: '+2', label: '+b' }, { v: '-2', label: '−b' },
1348+
];
1349+
const permSelects = document.querySelectorAll('.perm-sel');
1350+
permSelects.forEach(sel => {
1351+
for (const opt of PERM_OPTIONS) {
1352+
const o = document.createElement('option');
1353+
o.value = opt.v;
1354+
o.textContent = opt.label;
1355+
sel.appendChild(o);
1356+
}
1357+
sel.addEventListener('change', () => {
1358+
const out = parseInt(sel.dataset.out);
1359+
const v = sel.value;
1360+
const sign = v[0] === '-' ? -1 : 1;
1361+
const src = parseInt(v.slice(1));
1362+
state.colorPerm[out] = { src, sign };
1363+
invalidateSpectral();
1364+
});
1365+
});
1366+
function syncPermUI() {
1367+
permSelects.forEach(sel => {
1368+
const out = parseInt(sel.dataset.out);
1369+
const e = state.colorPerm[out];
1370+
sel.value = (e.sign > 0 ? '+' : '-') + e.src;
1371+
});
1372+
}
1373+
function setPerm(arr) {
1374+
state.colorPerm = arr.map(e => ({ ...e }));
1375+
syncPermUI();
1376+
invalidateSpectral();
1377+
}
1378+
syncPermUI();
1379+
document.getElementById('btn-perm-id').addEventListener('click', () => setPerm([
1380+
{ src: 0, sign: 1 }, { src: 1, sign: 1 }, { src: 2, sign: 1 },
1381+
]));
1382+
document.getElementById('btn-perm-negate').addEventListener('click', () => setPerm([
1383+
{ src: 0, sign: -1 }, { src: 1, sign: -1 }, { src: 2, sign: -1 },
1384+
]));
1385+
document.getElementById('btn-perm-rgb-rot').addEventListener('click', () => setPerm([
1386+
{ src: 1, sign: 1 }, { src: 2, sign: 1 }, { src: 0, sign: 1 },
1387+
]));
1388+
document.getElementById('btn-perm-swap-rg').addEventListener('click', () => setPerm([
1389+
{ src: 1, sign: 1 }, { src: 0, sign: 1 }, { src: 2, sign: 1 },
1390+
]));
1391+
document.getElementById('btn-perm-neg-g').addEventListener('click', () => setPerm([
1392+
{ src: 0, sign: 1 }, { src: 1, sign: -1 }, { src: 2, sign: 1 },
1393+
]));
12401394

12411395

12421396
// Diffusion controls

experiments/symmetry_simple/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,26 @@ <h3>Symmetry</h3>
8080
</div>
8181
</div>
8282
</section>
83+
<section class="panel">
84+
<h3>Color Permutation</h3>
85+
<div style="font-size:11px; opacity:0.7; line-height:1.4; margin-bottom:6px;">
86+
Applied across symmetry linkages (mirror / rotation / translation).
87+
Negation flips the channel around 0.5.
88+
</div>
89+
<div id="color-perm-row" style="display:flex; gap:6px; align-items:center;">
90+
<label>R'=<select class="perm-sel" data-out="0"></select></label>
91+
<label>G'=<select class="perm-sel" data-out="1"></select></label>
92+
<label>B'=<select class="perm-sel" data-out="2"></select></label>
93+
</div>
94+
<div class="rot-actions" style="margin-top:6px;">
95+
<button id="btn-perm-id" class="mini-btn">id</button>
96+
<button id="btn-perm-negate" class="mini-btn">−id</button>
97+
<button id="btn-perm-rgb-rot" class="mini-btn">rgb→gbr</button>
98+
<button id="btn-perm-swap-rg" class="mini-btn">swap rg</button>
99+
<button id="btn-perm-neg-g" class="mini-btn">r,−g,b</button>
100+
</div>
101+
</section>
102+
83103

84104
<section class="panel">
85105
<h3>Diffusion</h3>

0 commit comments

Comments
 (0)