atagen

monolith

public
Star0
Watch0

Commit aa790e7e33a0

tarball

atagen <boss@atagen.co>2026-10-16 08:04 UTC

2 files changed+47-19
design: label interrupt targets and compare floating choreographies

The interrupt demo silently included a target that closes B, which read as windows vanishing mid-storm. Floating-to-tiled now offers slot-opens-last and direct-glide alongside the old slot-opens-first so the three can be compared side by side.
Modified file: design/decisions.md+3 additions−2 deletions
@@ -86,8 +86,9 @@ Running log of settled decisions and open questions. Research backing lives in `
8686 - Shared-separator rule, plans of 1 to 4 phases, the 4-phase grid guarantees a plan for any input. Pinch and lerp are safety nets only.
8787 - Window moves and block moves both exist as commands. Tag reordering plans with rigid blocks.
8888 - One curve per motion group.
89-- Floating to tiled is phased. The slot opens concurrently, the floater travels one axis per phase, then lands.
90-- Open. Seed shape, whether landing may resize both axes at once, lane extent, and the layout model for interspersed tags.
89+- Floating windows travel above the tiled layer, exempt from the overlap rule.
90+- Seeds start as a line on the old boundary between neighbours.
91+- Open. Floating choreography (slot opens last, direct glide, slot opens first), whether landing may resize both axes at once, lane extent, and the layout model for interspersed tags.
9192 - Input hit-tests against final layout geometry, never animated geometry.
9293 - smithay is pinned by git rev. The repo uses jj.
9394
Modified file: design/planner.html+44 additions−17 deletions
@@ -141,7 +141,12 @@
141141 <div class="demo" id="d7"></div>
142142
143143 <h2>12. Floating to tiled</h2>
144-<p>A floating window joining the tiled layout is phased, unlike a spawn. The tiled windows open a slot for it, concurrently and following the same rules as an insertion. Meanwhile the floating window travels above them, one axis per phase, until it sits over the slot. In the last phase it falls into place. Lifting a tiled window back out runs the same steps in reverse. The floating window is drawn above the tiled layer and is exempt from the overlap rule while it travels.</p>
144+<p>A floating window joining the tiled layout travels above the tiled layer and is exempt from the overlap rule while it does. The mode button cycles three choreographies. Lifting out plays each one in reverse.</p>
145+<ul>
146+ <li><strong>Slot opens last</strong> (default). F travels one axis per phase until it hovers over where its slot will be. Only then does the gap open, and F lands into it as it widens, in the same final phase.</li>
147+ <li><strong>Direct glide.</strong> F lerps straight into the slot while the gap opens, in one phase.</li>
148+ <li><strong>Slot opens first.</strong> The gap opens while F is still travelling, then F lands. This was draft 2 behaviour.</li>
149+</ul>
145150 <div class="demo" id="d10"></div>
146151
147152 <h2>13. Fallbacks</h2>
@@ -211,11 +216,11 @@
211216 <li>Orthogonal is the default and the flagship look. Direct is a toggle.</li>
212217 <li>Window moves and block moves both exist as commands. Tag reordering moves rigid blocks.</li>
213218 <li>One curve per motion group, bounded by phase time rather than amount of motion.</li>
214- <li>Floating to tiled is phased, travel one axis per phase, then land.</li>
219+ <li>Seeds start as a line on the old boundary.</li>
215220 <li>The planner must never need a fallback. Pinch and lerp stay as safety nets to try.</li>
216221 </ul>
217222 <h3>Open</h3>
218-<div class="q"><strong>Seed shape.</strong> Line on the old boundary, point on the old boundary, or target centre (section 4). Line is always one phase. Point looks more like your "grow from centre" but costs a phase in orthogonal style.</div>
223+<div class="q"><strong>Floating choreography.</strong> Slot opens last, direct glide, or slot opens first (section 12).</div>
219224 <div class="q"><strong>Landing in orthogonal style.</strong> The final "falls into place" step resizes both width and height at once. Is that an accepted exception, or must it also split into two phases?</div>
220225 <div class="q"><strong>Lane extent.</strong> Strips span only the band of the involved windows. Spanning the whole output gives thicker lanes for small windows, but disturbs more of the screen.</div>
221226 <div class="q"><strong>The layout model (section 16).</strong> One tree per output with tags as a filter, or something else. You raised the case of windows being closed and moved while others are hidden. In the one-tree model, hidden windows keep their slot, closing removes only that leaf, and moves are made relative to visible neighbours, so hidden siblings stay attached to whichever visible neighbour they were next to. That rule is what needs discussing.</div>
@@ -773,6 +778,7 @@ function tagLayout(I, view2, order) {
773778
774779 { // d6 interrupts
775780 const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
781+ const names = ["A B C", "C A B", "A wide", "B C A", "close B"];
776782 const targets = [
777783 () => splitX(I, ['A', 'B', 'C']),
778784 () => splitX(I, ['C', 'A', 'B']),
@@ -783,7 +789,7 @@ function tagLayout(I, view2, order) {
783789 const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
784790 let idx = 0;
785791 d.set(withB(targets[0]()));
786- const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); };
792+ const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); d.lastKind += ` · target "${names[idx]}"`; };
787793 d.button('Next target', next);
788794 d.button('Storm (8 rapid interrupts)', () => {
789795 let n = 0;
@@ -889,7 +895,8 @@ for (const [id, names] of [
889895 { // d10 floating to tiled
890896 const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H);
891897 const FL = R(220, 40, 220, 130);
892- let tiled = false;
898+ const modes = ['slot opens last', 'direct glide', 'slot opens first'];
899+ let tiled = false, mode = 0;
893900 const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']);
894901 const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL });
895902 const ortho = () => STYLE === 'orthogonal';
@@ -898,28 +905,48 @@ for (const [id, names] of [
898905 for (let i = 0; i <= n; i++) { const t = tiledAt(i); keys.push({ A: t.A, B: t.B, C: t.C, F: fAt(i) }); }
899906 return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) };
900907 };
908+ const slotPlan = (from, to, opening) => {
909+ const fromBx = (from.B.x + from.B.w + from.C.x) / 2, toBx = (to.B.x + to.B.w + to.C.x) / 2;
910+ return opening
911+ ? plan({ A: from.A, B: from.B, C: from.C, P: R(fromBx, I.y, 0, I.h) }, { A: to.A, B: to.B, C: to.C, P: to.F })
912+ : plan({ A: from.A, B: from.B, C: from.C, P: from.F }, { A: to.A, B: to.B, C: to.C, P: R(toBx, I.y, 0, I.h) });
913+ };
914+ const at = (tp, i) => tp.keys[Math.max(0, Math.min(i, tp.durs.length))];
901915 const floatIn = from => {
902- const slot = tiledL().F, to = tiledL();
903- const bx = (from.B.x + from.B.w + from.C.x) / 2;
904- const tp = plan({ A: from.A, B: from.B, C: from.C, P: R(bx, I.y, 0, I.h) }, { A: to.A, B: to.B, C: to.C, P: slot });
905- const k = tp.durs.length, f = from.F;
916+ const to = tiledL(), slot = to.F, tp = slotPlan(from, to, true), k = tp.durs.length, f = from.F;
917+ if (modes[mode] === 'direct glide') {
918+ const n = Math.max(1, k);
919+ return assemble(n, i => at(tp, i), i => i === 0 ? f : slot, `glide in, slot ${tp.kind}`);
920+ }
906921 const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2;
907922 const path = ortho() ? [f, R(cx, f.y, f.w, f.h), R(cx, cy, f.w, f.h)] : [f, R(cx, cy, f.w, f.h)];
908- const n = Math.max(path.length - 1, k) + 1;
909- return assemble(n, i => tp.keys[Math.min(i, k)], i => i === n ? slot : path[Math.min(i, path.length - 1)], `float in, slot ${tp.kind}`);
923+ const travel = path.length - 1;
924+ if (modes[mode] === 'slot opens last') {
925+ const n = travel + Math.max(1, k);
926+ return assemble(n, i => at(tp, i - (n - k)), i => i === n ? slot : path[Math.min(i, travel)], `float in, slot ${tp.kind}`);
927+ }
928+ const n = Math.max(travel, k) + 1;
929+ return assemble(n, i => at(tp, i), i => i === n ? slot : path[Math.min(i, travel)], `float in, slot ${tp.kind}`);
910930 };
911931 const floatOut = from => {
912- const slot = from.F, to = floatL();
913- const bx = (to.B.x + to.B.w + to.C.x) / 2;
914- const tp = plan({ A: from.A, B: from.B, C: from.C, P: slot }, { A: to.A, B: to.B, C: to.C, P: R(bx, I.y, 0, I.h) });
915- const k = tp.durs.length;
932+ const to = floatL(), slot = from.F, tp = slotPlan(from, to, false), k = tp.durs.length;
933+ if (modes[mode] === 'direct glide') {
934+ const n = Math.max(1, k);
935+ return assemble(n, i => at(tp, i), i => i === 0 ? slot : FL, `glide out, slot ${tp.kind}`);
936+ }
916937 const lift = R(slot.x + slot.w / 2 - FL.w / 2, slot.y + slot.h / 2 - FL.h / 2, FL.w, FL.h);
917938 const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL];
918- const n = Math.max(path.length - 1, k + 1);
919- return assemble(n, i => tp.keys[Math.max(0, Math.min(i - 1, k))], i => path[Math.min(i, path.length - 1)], `lift out, slot ${tp.kind}`);
939+ const travel = path.length - 1;
940+ if (modes[mode] === 'slot opens last') {
941+ const n = Math.max(travel, k);
942+ return assemble(n, i => at(tp, i), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
943+ }
944+ const n = Math.max(travel, k + 1);
945+ return assemble(n, i => at(tp, i - 1), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
920946 };
921947 d.set(floatL());
922948 d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); });
949+ const mb = d.button('mode: ' + modes[0], () => { mode = (mode + 1) % modes.length; mb.textContent = 'mode: ' + modes[mode]; });
923950 }
924951
925952 document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };