.pragma library // Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // The expose scatter, kept pure so it runs under node (test_expose.js). // Input is [{w, h}] in real pixels, most recently used first; output is // [{x, y, w, h}] inside a W by H screen. // // Windows are seeded on a golden-angle spiral, the most recent at the centre, // then pushed apart until nothing overlaps, then fitted with one scale, so // their sizes stay relative to each other. Seeding from the windows' real // positions was tried first and degenerates here: every window fills its // monitor, so they all start in one pile, and pushing a pile apart makes a // line that still overlapped after 300 passes and filled under 15% of the // screen. const GAP = 60; // real pixels kept between two windows const PASSES = 300; const FILL = 0.92; // of the screen, each way const MAX_SCALE = 0.5; // a lone window is not a full-screen mirror const SPREAD = 0.45; // spiral step, as a fraction of the tallest window const GOLDEN = 137.5 * Math.PI / 180; function layout(rects, W, H) { if (rects.length === 0) return []; const aspect = W / H; const step = SPREAD * Math.max(...rects.map(o => o.h)); // The spiral is stretched by the screen's aspect, so the cluster comes // out the shape of the screen rather than round. const r = rects.map((o, k) => { const rad = step * Math.sqrt(k), t = k * GOLDEN; return { x: rad * Math.cos(t) * aspect - o.w / 2, y: rad * Math.sin(t) - o.h / 2, w: o.w, h: o.h }; }); // ponytail: all pairs every pass, O(n^2); fine for this desk's ~20 // windows, a spatial grid if it ever is not. for (let pass = 0; pass < PASSES; pass++) { let moved = false; for (let i = 0; i < r.length; i++) for (let j = i + 1; j < r.length; j++) { const a = r[i], b = r[j]; const ox = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) + GAP; const oy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) + GAP; if (ox <= 0 || oy <= 0) continue; moved = true; // Push along whichever axis costs less screen, measured against // the screen's own width and height: compared in raw pixels, wide // windows always separate vertically and stack into a column. if (ox / W < oy / H) { const s = (b.x + b.w / 2) >= (a.x + a.w / 2) ? 1 : -1; a.x -= s * ox / 2; b.x += s * ox / 2; } else { const s = (b.y + b.h / 2) >= (a.y + a.h / 2) ? 1 : -1; a.y -= s * oy / 2; b.y += s * oy / 2; } } if (!moved) break; } let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; for (const o of r) { x0 = Math.min(x0, o.x); y0 = Math.min(y0, o.y); x1 = Math.max(x1, o.x + o.w); y1 = Math.max(y1, o.y + o.h); } const k = Math.min(FILL * W / (x1 - x0), FILL * H / (y1 - y0), MAX_SCALE); const dx = (W - (x1 - x0) * k) / 2, dy = (H - (y1 - y0) * k) / 2; return r.map(o => ({ x: dx + (o.x - x0) * k, y: dy + (o.y - y0) * k, w: o.w * k, h: o.h * k })); }