1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
.pragma library
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// 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 pulled toward the centre and pushed apart again for a number of
// rounds, so the scatter is a cluster rather than a loose spiral, 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 ROUNDS = 30;
const PULL = 0.85; // centre distance kept per gravity round
const GOLDEN = 137.5 * Math.PI / 180;
function separate(r, W, H) {
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;
}
}
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 };
});
// Gravity: everything is pulled toward the centre, then pushed apart
// again, until the cluster stops shrinking. Separation alone leaves
// the spiral's gaps in place and the scatter reads as loose.
// 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 round = 0; round < ROUNDS; round++) {
for (const o of r) {
o.x = (o.x + o.w / 2) * PULL - o.w / 2;
o.y = (o.y + o.h / 2) * PULL - o.h / 2;
}
separate(r, W, H);
}
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 }));
}
|