aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher/Expose.js
blob: 412090716d79c10c82060379976d086877da06c7 (plain)
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 }));
}