From 4cc85e59d7b3f53f4eec897f022bc4bcb643f185 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 15 Oct 2026 11:03:48 +0200 Subject: feat(window-switcher): gravity pulls the scatter toward the centre On screen the spiral alone read as loose: separation keeps the spiral's gaps, and the previews filled 24 to 30% of the screen. Thirty rounds now pull every window 15% closer to the centre and push them apart again, which fills 35 to 55% and makes each preview up to 1.5x larger. Twenty windows lay out in about 4ms. Co-Authored-By: Claude Opus 5.5 --- window-switcher/Expose.js | 59 ++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 21 deletions(-) (limited to 'window-switcher/Expose.js') diff --git a/window-switcher/Expose.js b/window-switcher/Expose.js index 2b14e4e..4120907 100644 --- a/window-switcher/Expose.js +++ b/window-switcher/Expose.js @@ -16,35 +16,24 @@ // [{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. +// 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 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. +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++) { @@ -67,6 +56,34 @@ function layout(rects, W, H) { 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); -- cgit v1.2.3