atagen

monolith

public
Star0
Watch0
aa790e7e33a014cb45dd7ccab68027dee63cb974
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git

Blame: design/planner.html

05668b0d4753atagen2026-10-16 05:50 UTC1
<!doctype html>
05668b0d4753atagen2026-10-16 05:50 UTC2
<html lang="en">
05668b0d4753atagen2026-10-16 05:50 UTC3
<head>
05668b0d4753atagen2026-10-16 05:50 UTC4
<meta charset="utf-8">
05668b0d4753atagen2026-10-16 05:50 UTC5
<title>monolith planner design, draft 1</title>
05668b0d4753atagen2026-10-16 05:50 UTC6
<style>
05668b0d4753atagen2026-10-16 05:50 UTC7
  :root { --bg:#111317; --panel:#181b21; --fg:#d8dbe2; --dim:#8a909c; --accent:#e0774a; --bad:#ff4d5e; }
05668b0d4753atagen2026-10-16 05:50 UTC8
  * { box-sizing:border-box; }
05668b0d4753atagen2026-10-16 05:50 UTC9
  body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.6 system-ui, sans-serif; }
05668b0d4753atagen2026-10-16 05:50 UTC10
  main { max-width:920px; margin:0 auto; padding:32px 20px 120px; }
05668b0d4753atagen2026-10-16 05:50 UTC11
  h1 { font-size:28px; margin:0 0 4px; }
05668b0d4753atagen2026-10-16 05:50 UTC12
  h2 { font-size:20px; margin:48px 0 8px; border-bottom:1px solid #2a2e37; padding-bottom:4px; }
05668b0d4753atagen2026-10-16 05:50 UTC13
  h3 { font-size:16px; margin:24px 0 6px; }
05668b0d4753atagen2026-10-16 05:50 UTC14
  p, li { color:var(--fg); }
05668b0d4753atagen2026-10-16 05:50 UTC15
  .sub { color:var(--dim); margin:0 0 24px; }
05668b0d4753atagen2026-10-16 05:50 UTC16
  code { background:#22262e; padding:1px 5px; border-radius:4px; font-size:13px; }
05668b0d4753atagen2026-10-16 05:50 UTC17
  pre { background:#1a1d23; padding:14px; border-radius:8px; overflow-x:auto; font-size:13px; line-height:1.45; }
05668b0d4753atagen2026-10-16 05:50 UTC18
  table { border-collapse:collapse; width:100%; font-size:14px; margin:12px 0; }
05668b0d4753atagen2026-10-16 05:50 UTC19
  th, td { border:1px solid #2a2e37; padding:6px 8px; text-align:left; vertical-align:top; }
05668b0d4753atagen2026-10-16 05:50 UTC20
  th { background:#1d2027; }
05668b0d4753atagen2026-10-16 05:50 UTC21
  .demo { background:var(--panel); border:1px solid #2a2e37; border-radius:10px; padding:12px; margin:16px 0; }
05668b0d4753atagen2026-10-16 05:50 UTC22
  .demo canvas { display:block; width:100%; height:auto; border-radius:6px; background:#0d0f12; }
05668b0d4753atagen2026-10-16 05:50 UTC23
  .controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
05668b0d4753atagen2026-10-16 05:50 UTC24
  button { background:#2a2f39; color:var(--fg); border:1px solid #3a404c; border-radius:6px; padding:5px 12px; cursor:pointer; font:inherit; font-size:14px; }
05668b0d4753atagen2026-10-16 05:50 UTC25
  button:hover { background:#343a46; }
05668b0d4753atagen2026-10-16 05:50 UTC26
  label { color:var(--dim); font-size:14px; display:flex; gap:6px; align-items:center; }
05668b0d4753atagen2026-10-16 05:50 UTC27
  .status { font:13px/1.4 ui-monospace, monospace; color:var(--dim); margin-top:8px; min-height:1.4em; }
05668b0d4753atagen2026-10-16 05:50 UTC28
  .status .bad { color:var(--bad); }
05668b0d4753atagen2026-10-16 05:50 UTC29
  .global { position:sticky; top:0; z-index:5; background:rgba(17,19,23,0.94); backdrop-filter:blur(4px); border-bottom:1px solid #2a2e37; padding:8px 20px; display:flex; gap:20px; justify-content:center; flex-wrap:wrap; }
05668b0d4753atagen2026-10-16 05:50 UTC30
  .q { background:#1d2027; border-left:3px solid var(--accent); padding:8px 12px; margin:10px 0; border-radius:0 6px 6px 0; }
05668b0d4753atagen2026-10-16 05:50 UTC31
  .note { color:var(--dim); font-size:14px; }
05668b0d4753atagen2026-10-16 05:50 UTC32
</style>
05668b0d4753atagen2026-10-16 05:50 UTC33
</head>
05668b0d4753atagen2026-10-16 05:50 UTC34
<body>
05668b0d4753atagen2026-10-16 05:50 UTC35
<div class="global">
05668b0d4753atagen2026-10-16 05:50 UTC36
  <label>phase duration <input id="phaseMs" type="range" min="150" max="1500" step="50" value="450"> <span id="phaseMsOut">450 ms</span></label>
cd7085e54c88atagen2026-10-16 06:58 UTC37
  <label>style <select id="style"><option value="orthogonal">orthogonal (default, one axis per window per phase)</option><option value="direct">direct (diagonal when safe)</option></select></label>
05668b0d4753atagen2026-10-16 05:50 UTC38
</div>
05668b0d4753atagen2026-10-16 05:50 UTC39
<main>
cd7085e54c88atagen2026-10-16 06:58 UTC40
<h1>monolith planner, draft 2</h1>
cd7085e54c88atagen2026-10-16 06:58 UTC41
<p class="sub">Design for the multiphase layout animation planner (milestone M1). Every demo runs a small JavaScript port of the proposed algorithm, so what you see is what the design does. Red means two tiled windows overlap. Orthogonal is the default style, and the selector at the top switches every demo to direct.</p>
05668b0d4753atagen2026-10-16 05:50 UTC42
05668b0d4753atagen2026-10-16 05:50 UTC43
<h2>1. What the planner does</h2>
05668b0d4753atagen2026-10-16 05:50 UTC44
<p>The layout engine produces a target rectangle for every window. The planner turns "where windows are on screen now" and "where they should be" into a short sequence of keyframe layouts. Each step between two keyframes is a <em>phase</em>. Every window in a phase moves on the same curve and the phase takes exactly <code>t</code>, the configured duration, so a plan lasts <code>phases × t</code>.</p>
05668b0d4753atagen2026-10-16 05:50 UTC45
<p>Rules carried over from wry <code>master</code>.</p>
05668b0d4753atagen2026-10-16 05:50 UTC46
<ul>
05668b0d4753atagen2026-10-16 05:50 UTC47
  <li>Tiled windows never overlap while they move.</li>
05668b0d4753atagen2026-10-16 05:50 UTC48
  <li>Phases are synchronised. Everything in a phase starts and stops together.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC49
  <li>In orthogonal style, the default, a window moves or resizes along one axis per phase.</li>
05668b0d4753atagen2026-10-16 05:50 UTC50
  <li>An interrupt throws the plan away and replans from what is on screen. Plans never stack.</li>
440b67836dd5atagen2026-10-16 07:57 UTC51
  <li>Neighbours whose edges touch at both ends of a transition keep touching throughout. No holes open mid-plan.</li>
440b67836dd5atagen2026-10-16 07:57 UTC52
  <li>Planning B to A is the exact reverse of planning A to B, so undoing a change looks like rewinding it.</li>
05668b0d4753atagen2026-10-16 05:50 UTC53
</ul>
05668b0d4753atagen2026-10-16 05:50 UTC54
<p>New requirements wry never met.</p>
05668b0d4753atagen2026-10-16 05:50 UTC55
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC56
  <li>Moves between outputs, and large groups such as viewing or hiding a tag.</li>
05668b0d4753atagen2026-10-16 05:50 UTC57
  <li>Best-effort velocity continuity when an interrupt lands mid-phase.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC58
  <li>A hard bound on plan length, and a construction that never needs a fallback.</li>
05668b0d4753atagen2026-10-16 05:50 UTC59
</ul>
05668b0d4753atagen2026-10-16 05:50 UTC60
05668b0d4753atagen2026-10-16 05:50 UTC61
<h2>2. The problem</h2>
05668b0d4753atagen2026-10-16 05:50 UTC62
<p>Swapping two windows by interpolating straight from start to end drives them through each other. Toggle "naive" to compare.</p>
05668b0d4753atagen2026-10-16 05:50 UTC63
<div class="demo" id="d1"></div>
05668b0d4753atagen2026-10-16 05:50 UTC64
05668b0d4753atagen2026-10-16 05:50 UTC65
<h2>3. The core rule, shared separators</h2>
05668b0d4753atagen2026-10-16 05:50 UTC66
<p>For any two windows there are four ways they can be apart. A is left of B, right of B, above B, or below B. Each is a linear inequality on window edges, for example <code>A.right ≤ B.left</code>. If an inequality holds at both ends of a phase and both windows move on the same curve, it holds for the whole phase, because every edge is the same weighted average of its start and end value.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC67
<p>So the planner's entire job reduces to this. <strong>Pick keyframes so that for every pair of windows, every two consecutive keyframes share at least one separating relation.</strong> wry checked overlap after building a plan. This rule makes plans correct by construction.</p>
05668b0d4753atagen2026-10-16 05:50 UTC68
<p>Looking at a single pair gives a lower bound on phases.</p>
05668b0d4753atagen2026-10-16 05:50 UTC69
<table>
05668b0d4753atagen2026-10-16 05:50 UTC70
  <tr><th>pair goes from</th><th>to</th><th>minimum phases</th><th>why</th></tr>
05668b0d4753atagen2026-10-16 05:50 UTC71
  <tr><td>left of</td><td>left of (any sizes)</td><td>1</td><td>relation shared</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC72
  <tr><td>left of</td><td>above</td><td>2</td><td>needs a middle keyframe where A is both left of and above B</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC73
  <tr><td>left of</td><td>right of</td><td>3</td><td>left and right can't both hold, so the pair must pass through above or below on the way</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC74
</table>
cd7085e54c88atagen2026-10-16 06:58 UTC75
<p>Pairs interact, so a whole layout can need one more phase than its worst pair (section 7). <strong>Every plan is 1 to 4 phases, and the 4-phase construction works for any input</strong>, so no fallback is ever required. The rule is sufficient, not necessary. Some overlap-free phases break it, and the Rust validator keeps an exact check for those, but the planner only builds plans that satisfy it.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC76
<p class="note">Stress test of the JavaScript port. 6,000 random pairs of layouts with 2 to 7 windows, each target a full random reshuffle, in both styles. Zero overlapping frames, zero windows moving on two axes in one phase in orthogonal style, zero fallbacks. Real edits are far smaller than random reshuffles.</p>
05668b0d4753atagen2026-10-16 05:50 UTC77
cd7085e54c88atagen2026-10-16 06:58 UTC78
<h2>4. One phase, and seeds</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC79
<p>Anything that keeps the arrangement of windows intact is a single phase. That covers ratio changes, inserting and removing windows, splitting a column, and viewing or hiding a tag.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC80
<p><strong>Seeds.</strong> A window that appears starts as a zero-size rectangle and grows while its neighbours make room, concurrently, in the same phase. The only question is where it starts growing from.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC81
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC82
  <li><strong>Line on the old boundary</strong> (default). A zero-width line where the two neighbours currently meet. It is always free space, so the whole insertion is one phase in either style.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC83
  <li><strong>Point at the middle of the old boundary.</strong> Looks like growing from a centre, and never overlaps. Growing from a point changes both axes, so orthogonal style splits it into two phases (grow tall, then wide).</li>
cd7085e54c88atagen2026-10-16 06:58 UTC84
  <li><strong>Point at the centre of the new window's target.</strong> At the start of the phase that point is usually still covered by a neighbour that hasn't moved out yet. The planner has to push the neighbours away first and grow second. With naive on you see the overlap instead.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC85
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC86
<p>Cycle the seed mode below and insert D to compare.</p>
05668b0d4753atagen2026-10-16 05:50 UTC87
<div class="demo" id="d2"></div>
05668b0d4753atagen2026-10-16 05:50 UTC88
<p>Viewing and hiding a tag is the same thing at a larger scale. The new tag's windows are seeds on the edge where their row will open.</p>
05668b0d4753atagen2026-10-16 05:50 UTC89
<div class="demo" id="d3"></div>
05668b0d4753atagen2026-10-16 05:50 UTC90
05668b0d4753atagen2026-10-16 05:50 UTC91
<h2>5. Two phases, axis order</h2>
05668b0d4753atagen2026-10-16 05:50 UTC92
<p>When a pair turns a corner (left of becomes above), the middle keyframe is built from a <em>mixed</em> rectangle. Each window takes its horizontal extent from one end and its vertical extent from the other. "X first" means the window takes its target horizontal extent in phase 1 and its target vertical extent in phase 2. "Y first" is the reverse. Every window then moves along exactly one axis per phase.</p>
05668b0d4753atagen2026-10-16 05:50 UTC93
<p>Each window's choice is one boolean. Every pair rules out some of the four combinations, and each forbidden combination is a clause over two variables. That makes it a <strong>2-SAT problem, solvable in linear time</strong> with no search. Windows that only change one axis use the same boolean to choose whether they move in phase 1 or phase 2, and the planner prefers phase 1 so nothing waits needlessly.</p>
440b67836dd5atagen2026-10-16 07:57 UTC94
<p><strong>Coupled edges.</strong> Avoiding overlap is not enough. A window can vanish in phase 1 while the neighbour that absorbs its space only moves in phase 2, leaving a hole in between. Any two neighbours whose edges touch at both ends must also touch in the middle keyframe. Among valid orderings the solver picks one that keeps every such edge together, and only accepts a hole if none exists. In the 6,000-case stress test no two-phase plan needed one.</p>
440b67836dd5atagen2026-10-16 07:57 UTC95
<p><strong>Rewind symmetry.</strong> Every pair of layouts is planned in one canonical direction, and the other direction plays that plan backwards. Insert D and split B with E together, and D arrives first, then E. Undo it, and E leaves first, then D.</p>
05668b0d4753atagen2026-10-16 05:50 UTC96
<div class="demo" id="d4a"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC97
<p>Growing a window in both directions inside a grid. Orthogonal makes space along one axis and then grows along the other. Direct does it in one diagonal phase.</p>
05668b0d4753atagen2026-10-16 05:50 UTC98
<div class="demo" id="d4b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC99
05668b0d4753atagen2026-10-16 05:50 UTC100
<h2>6. Three phases, lanes</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC101
<p>A <strong>lane</strong> is a temporary strip, like a motorway lane used for overtaking. Before two windows can pass each other along one axis, each shrinks along the other axis into its own strip of the space they share. For a horizontal swap the strips are horizontal, so each window gets a slice of the height. In phase 2 they slide past each other inside their strips, and in phase 3 they expand back to full size.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC102
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC103
  <li>Two windows may share a lane only if they never pass each other.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC104
  <li>Windows stacked along the strip axis must keep their stacking order in the lanes, at both ends.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC105
  <li>Only windows that pass something enter lanes. Everything else moves in whichever phase validates, preferring the slide phase.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC106
  <li>The strips span only the band the involved windows occupy, not the whole output.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC107
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC108
<p>Picking lanes is a small constrained colouring problem. For a pure reordering of a row, the lane count equals the length of the longest decreasing subsequence of the new order (Dilworth's theorem). Swapping two neighbours or rotating needs 2 lanes, and fully reversing four windows needs 4.</p>
05668b0d4753atagen2026-10-16 05:50 UTC109
<div class="demo" id="d5"></div>
05668b0d4753atagen2026-10-16 05:50 UTC110
cd7085e54c88atagen2026-10-16 06:58 UTC111
<h2>7. Four phases, the grid</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC112
<p>Some transitions reverse windows along both axes at once. In <code>(A over D) | (C over B)</code> to <code>(A over B over C) | D</code>, D has to pass B and C horizontally while B and C swap vertical order. Lanes along either axis contradict the other reversal. The grid handles it.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC113
<ol>
cd7085e54c88atagen2026-10-16 06:58 UTC114
  <li>Every involved window shrinks vertically into its own horizontal strip, ordered by where it starts.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC115
  <li>It shrinks horizontally into its own column, ordered by where it ends. Every window is now a separate small cell.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC116
  <li>It moves vertically to its final height. Its column keeps it clear of everything.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC117
  <li>It expands horizontally to its final width.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC118
</ol>
cd7085e54c88atagen2026-10-16 06:58 UTC119
<p>Each step provably keeps every pair separated, whatever the input, so this is the guarantee behind "no fallback needed". It is also the ugliest plan, so the planner only reaches it after everything else fails, and only for the windows involved.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC120
<div class="demo" id="d9c"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC121
cd7085e54c88atagen2026-10-16 06:58 UTC122
<h2>8. Joining and leaving stacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC123
<p>These have historically been the hard cases. Notation <code>A | B</code> is side by side and <code>A / B</code> is A stacked over B. Click any layout to animate to it from whatever is on screen, including mid-animation.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC124
<div class="demo" id="d9a"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC125
<div class="demo" id="d9b"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC126
cd7085e54c88atagen2026-10-16 06:58 UTC127
<h2>9. Rigid blocks and movement commands</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC128
<p>Swapping the order of two viewed tags reverses every window of one tag against every window of the other. Planned window by window, that needs one lane per window. Planned with each tag as a <strong>rigid block</strong>, it needs 2 lanes and the windows ride inside their block. The block planner is the same algorithm one level up.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC129
<p>Both movement units exist as commands, for example <code>meta+shift+hjkl</code> moves a window and <code>meta+ctrl+hjkl</code> moves its block. The unit being moved decides the plan. A block move plans with rigid blocks, a window move plans per window.</p>
05668b0d4753atagen2026-10-16 05:50 UTC130
<div class="demo" id="d3b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC131
cd7085e54c88atagen2026-10-16 06:58 UTC132
<h2>10. Interrupts and velocity</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC133
<p>An interrupt samples the current on-screen rectangles and plans from there to the new target. The new plan obeys the same bound however many times you interrupt. Try the storm button, which fires a new target every few hundred milliseconds.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC134
<p><strong>Velocity.</strong> The base curve starts and ends at rest, so a window moving when interrupted stops dead. Velocity matching measures how fast things were moving and starts the first phase of the new plan at that speed, by setting the bezier's first control point so the initial slope matches. It only applies when the first phase continues in the direction things were already moving. Otherwise the window stops and turns. The control point's height is clamped to 1, which rules out overshoot, since a cubic bezier never leaves the range of its control points.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC135
<p>One curve per <em>motion group</em>, never per window. Windows that share an edge must move it on the same curve, or the edge tears open or overlaps. Since phases are bounded by time rather than distance, the group's curve follows its fastest mover. The graph shows window A's speed. Turn matching off to see the drop to zero at every interrupt.</p>
05668b0d4753atagen2026-10-16 05:50 UTC136
<div class="demo" id="d6"></div>
05668b0d4753atagen2026-10-16 05:50 UTC137
cd7085e54c88atagen2026-10-16 06:58 UTC138
<h2>11. Across outputs</h2>
05668b0d4753atagen2026-10-16 05:50 UTC139
<p>Outputs live in one global coordinate space, so a move between outputs is planned like any other move and drawn clipped to each output. The window crosses the bezel and appears on the other side. Moving B over is one phase. Moving A over means A has to pass B, a reversal, so it takes lanes, and the lanes span both outputs.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC140
<p class="note">Not shown. Outputs at different scales draw the same window at different sizes, since the planner works in logical coordinates and each output scales its part. If two outputs aren't adjacent in the global space, the window travels through empty space.</p>
05668b0d4753atagen2026-10-16 05:50 UTC141
<div class="demo" id="d7"></div>
05668b0d4753atagen2026-10-16 05:50 UTC142
cd7085e54c88atagen2026-10-16 06:58 UTC143
<h2>12. Floating to tiled</h2>
aa790e7e33a0atagen2026-10-16 08:03 UTC144
<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>
aa790e7e33a0atagen2026-10-16 08:03 UTC145
<ul>
aa790e7e33a0atagen2026-10-16 08:03 UTC146
  <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>
aa790e7e33a0atagen2026-10-16 08:03 UTC147
  <li><strong>Direct glide.</strong> F lerps straight into the slot while the gap opens, in one phase.</li>
aa790e7e33a0atagen2026-10-16 08:03 UTC148
  <li><strong>Slot opens first.</strong> The gap opens while F is still travelling, then F lands. This was draft 2 behaviour.</li>
aa790e7e33a0atagen2026-10-16 08:03 UTC149
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC150
<div class="demo" id="d10"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC151
cd7085e54c88atagen2026-10-16 06:58 UTC152
<h2>13. Fallbacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC153
<p>The grid construction means no plan should ever need a fallback, and the stress test never reached one. Two remain wired in as safety nets for malformed input. <strong>Pinch</strong> shrinks a window to a point, jumps invisibly and regrows it. <strong>Lerp</strong> interpolates straight to the target and accepts overlap. Which one is the net is a matter of trying both.</p>
05668b0d4753atagen2026-10-16 05:50 UTC154
cd7085e54c88atagen2026-10-16 06:58 UTC155
<h2>14. Algorithm</h2>
05668b0d4753atagen2026-10-16 05:50 UTC156
<pre>plan(from, to):
440b67836dd5atagen2026-10-16 07:57 UTC157
  if from > to in a canonical order: return reverse(plan(to, from))
05668b0d4753atagen2026-10-16 05:50 UTC158
  groups = motion groups (windows whose swept areas can touch, transitively)
05668b0d4753atagen2026-10-16 05:50 UTC159
  for each group:
05668b0d4753atagen2026-10-16 05:50 UTC160
    if every pair shares a relation between from and to
05668b0d4753atagen2026-10-16 05:50 UTC161
       and (style is direct or every window changes only one axis):
05668b0d4753atagen2026-10-16 05:50 UTC162
      return [from, to]                                    # 1 phase
440b67836dd5atagen2026-10-16 07:57 UTC163
    if 2-SAT over axis order finds K1, preferring no holes: # mixed rects
05668b0d4753atagen2026-10-16 05:50 UTC164
      return [from, K1, to]                                # 2 phases
05668b0d4753atagen2026-10-16 05:50 UTC165
    for lane_axis in [y, x]:                               # strips, then columns
05668b0d4753atagen2026-10-16 05:50 UTC166
      for set in [windows in reversals, all movers, all]:
05668b0d4753atagen2026-10-16 05:50 UTC167
        for k in 2..|set|:
05668b0d4753atagen2026-10-16 05:50 UTC168
          if lanes assignable with k lanes and all three phases validate:
05668b0d4753atagen2026-10-16 05:50 UTC169
            return [from, K1, K2, to]                      # 3 phases
cd7085e54c88atagen2026-10-16 06:58 UTC170
    for set in [windows in reversals, all movers, all]:
cd7085e54c88atagen2026-10-16 06:58 UTC171
      if the grid validates for set:                       # always true for all
cd7085e54c88atagen2026-10-16 06:58 UTC172
        return [from, K1, K2, K3, to]                      # 4 phases
cd7085e54c88atagen2026-10-16 06:58 UTC173
    return fallback(from, to)                              # unreachable in theory
05668b0d4753atagen2026-10-16 05:50 UTC174
05668b0d4753atagen2026-10-16 05:50 UTC175
  rigid blocks: run plan() on block rectangles first, then map windows into them
cd7085e54c88atagen2026-10-16 06:58 UTC176
  floating:     slot opens via plan() with a seed, floater travels x, then y, then lands
05668b0d4753atagen2026-10-16 05:50 UTC177
  interrupts:   from = rectangles sampled at the current presentation time
05668b0d4753atagen2026-10-16 05:50 UTC178
  velocity:     first-phase curve per group, slope from the fastest mover,
05668b0d4753atagen2026-10-16 05:50 UTC179
                applied only if moving the same way, control point height ≤ 1
05668b0d4753atagen2026-10-16 05:50 UTC180
  validation:   exact pairwise check over the curve actually used</pre>
05668b0d4753atagen2026-10-16 05:50 UTC181
<p>Cost. Pair checks are O(n²) per group. 2-SAT is linear in clauses, so also O(n²). Lane assignment is backtracking over small sets with a greedy cut-off for large ones. Fifty windows is about 1,200 pairs, which is microseconds.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC182
<p>Everything here is pure geometry with no smithay dependency, so the <code>planner</code> crate gets property tests. Generate random layouts and random interrupt sequences, then check every frame of every plan for overlap, every plan for at most 4 phases, every orthogonal phase for single-axis motion, and every interrupt for continuity of position.</p>
05668b0d4753atagen2026-10-16 05:50 UTC183
cd7085e54c88atagen2026-10-16 06:58 UTC184
<h2>15. Compared with wry master</h2>
05668b0d4753atagen2026-10-16 05:50 UTC185
<table>
05668b0d4753atagen2026-10-16 05:50 UTC186
  <tr><th>wry master</th><th>this design</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC187
  <tr><td>SingleAction, MixedSinglePhase</td><td>1 phase</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC188
  <tr><td>SpaceThenOrthogonalGrowth, OrientationChange, HierarchyOrderedScales</td><td>2 phases, one 2-SAT problem instead of three strategies</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC189
  <tr><td>SwapLanes</td><td>3 phases, general lane assignment for any permutation</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC190
  <tr><td>no equivalent</td><td>4 phases, the grid, for simultaneous reversals on both axes</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC191
  <tr><td>ReversedForwardPlan</td><td>not needed, the construction is symmetric</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC192
  <tr><td>Unplannable groups fall back to overlapping plain interpolation</td><td>every input has a plan, fallbacks are only a safety net</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC193
  <tr><td>Bridged retarget can make plans longer than the original</td><td>every replan obeys the same bound</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC194
  <tr><td>Spawns grow from the target centre</td><td>spawns grow from a seed on the old boundary, concurrently</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC195
  <tr><td>Exact rational overlap check validates every plan</td><td>plans are correct by construction, the exact check remains as a safety net and for velocity-shaped curves</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC196
  <tr><td>Cross-output moves skip animation</td><td>planned in global coordinates, clipped per output</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC197
</table>
05668b0d4753atagen2026-10-16 05:50 UTC198
cd7085e54c88atagen2026-10-16 06:58 UTC199
<h2>16. Interspersing tags (open, changes the layout model)</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC200
<p>Row stacking answers "what does a combined view look like" but not "what happens when I drag A from tag 1's row in between D and E of tag 2". There are five answers.</p>
05668b0d4753atagen2026-10-16 05:50 UTC201
<table>
05668b0d4753atagen2026-10-16 05:50 UTC202
  <tr><th>option</th><th>meaning</th><th>verdict</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC203
  <tr><td>Forbid it</td><td>tags stay in their rows</td><td>limiting</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC204
  <tr><td>Dragging retags</td><td>moving A into tag 2's row makes A a tag 2 window</td><td>honest and simple, but A then vanishes from tag 1 viewed alone, which is usually not what the drag meant</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC205
  <tr><td>2D positional merging (L/C/R × T/M/B)</td><td>windows merge into slots by position</td><td>kill it. Users must predict an implicit rule, and every edit in a combined view is ambiguous about which tag's tree it changes</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC206
  <tr><td>Layout per view combination</td><td>each set of viewed tags gets its own tree, seeded by row stacking</td><td>no surprises between views, but N combinations of trees to keep in sync, and every new window has to be inserted into all of them</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC207
  <tr><td><strong>One tree per output, tags filter it</strong></td><td>every window of every tag lives in a single tree per output. Viewing tags shows a projection with hidden windows collapsed. Each tag starts as its own row, so row stacking becomes the default arrangement instead of a merge rule</td><td><strong>recommended</strong></td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC208
</table>
cd7085e54c88atagen2026-10-16 06:58 UTC209
<p>The last option is dwm's model (one client list, tags as a filter) with a tree in place of the list. The tree is the record of every relative position, permanently, because hidden windows never leave it. Nothing has to be restored. And for the planner, <strong>viewing or hiding any tag is always a single phase</strong>, because hidden windows are zero-weight seeds sitting at their own spot. Switching views never rearranges anything.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC210
<p>Its cost is action at a distance. Intersperse A into tag 2's row below, then view tag 1 alone. A now sits under B and C, because in the tree it lives in the lower row. You changed tag 1's single view without ever viewing it alone.</p>
05668b0d4753atagen2026-10-16 05:50 UTC211
<div class="demo" id="d8"></div>
05668b0d4753atagen2026-10-16 05:50 UTC212
cd7085e54c88atagen2026-10-16 06:58 UTC213
<h2>17. Decisions</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC214
<h3>Settled</h3>
cd7085e54c88atagen2026-10-16 06:58 UTC215
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC216
  <li>Orthogonal is the default and the flagship look. Direct is a toggle.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC217
  <li>Window moves and block moves both exist as commands. Tag reordering moves rigid blocks.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC218
  <li>One curve per motion group, bounded by phase time rather than amount of motion.</li>
aa790e7e33a0atagen2026-10-16 08:03 UTC219
  <li>Seeds start as a line on the old boundary.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC220
  <li>The planner must never need a fallback. Pinch and lerp stay as safety nets to try.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC221
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC222
<h3>Open</h3>
aa790e7e33a0atagen2026-10-16 08:03 UTC223
<div class="q"><strong>Floating choreography.</strong> Slot opens last, direct glide, or slot opens first (section 12).</div>
cd7085e54c88atagen2026-10-16 06:58 UTC224
<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>
cd7085e54c88atagen2026-10-16 06:58 UTC225
<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>
cd7085e54c88atagen2026-10-16 06:58 UTC226
<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>
05668b0d4753atagen2026-10-16 05:50 UTC227
</main>
05668b0d4753atagen2026-10-16 05:50 UTC228
05668b0d4753atagen2026-10-16 05:50 UTC229
<script id="planner">
05668b0d4753atagen2026-10-16 05:50 UTC230
const G = 10;
05668b0d4753atagen2026-10-16 05:50 UTC231
const EPS = 0.5;
05668b0d4753atagen2026-10-16 05:50 UTC232
let PHASE_MS = 450;
cd7085e54c88atagen2026-10-16 06:58 UTC233
let STYLE = 'orthogonal';
05668b0d4753atagen2026-10-16 05:50 UTC234
05668b0d4753atagen2026-10-16 05:50 UTC235
const R = (x, y, w, h) => ({ x, y, w, h });
05668b0d4753atagen2026-10-16 05:50 UTC236
const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; };
05668b0d4753atagen2026-10-16 05:50 UTC237
const eq = (a, b) => Math.abs(a - b) < 0.05;
05668b0d4753atagen2026-10-16 05:50 UTC238
const lerp = (a, b, s) => a + (b - a) * s;
05668b0d4753atagen2026-10-16 05:50 UTC239
const lerpRect = (a, b, s) => R(lerp(a.x, b.x, s), lerp(a.y, b.y, s), lerp(a.w, b.w, s), lerp(a.h, b.h, s));
05668b0d4753atagen2026-10-16 05:50 UTC240
05668b0d4753atagen2026-10-16 05:50 UTC241
function splitX(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC242
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC243
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC244
  const avail = rect.w - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC245
  let x = rect.x;
05668b0d4753atagen2026-10-16 05:50 UTC246
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC247
  ids.forEach((id, i) => { const w = avail * ratios[i] / tot; out[id] = R(x, rect.y, w, rect.h); x += w + G; });
05668b0d4753atagen2026-10-16 05:50 UTC248
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC249
}
05668b0d4753atagen2026-10-16 05:50 UTC250
function splitY(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC251
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC252
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC253
  const avail = rect.h - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC254
  let y = rect.y;
05668b0d4753atagen2026-10-16 05:50 UTC255
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC256
  ids.forEach((id, i) => { const h = avail * ratios[i] / tot; out[id] = R(rect.x, y, rect.w, h); y += h + G; });
05668b0d4753atagen2026-10-16 05:50 UTC257
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC258
}
05668b0d4753atagen2026-10-16 05:50 UTC259
05668b0d4753atagen2026-10-16 05:50 UTC260
function relBits(a, b) {
05668b0d4753atagen2026-10-16 05:50 UTC261
  let s = 0;
05668b0d4753atagen2026-10-16 05:50 UTC262
  if (a.x + a.w <= b.x + EPS) s |= 1;
05668b0d4753atagen2026-10-16 05:50 UTC263
  if (b.x + b.w <= a.x + EPS) s |= 2;
05668b0d4753atagen2026-10-16 05:50 UTC264
  if (a.y + a.h <= b.y + EPS) s |= 4;
05668b0d4753atagen2026-10-16 05:50 UTC265
  if (b.y + b.h <= a.y + EPS) s |= 8;
05668b0d4753atagen2026-10-16 05:50 UTC266
  return s;
05668b0d4753atagen2026-10-16 05:50 UTC267
}
05668b0d4753atagen2026-10-16 05:50 UTC268
function phaseOk(K1, K2, list) {
05668b0d4753atagen2026-10-16 05:50 UTC269
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC270
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC271
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC272
      if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false;
05668b0d4753atagen2026-10-16 05:50 UTC273
    }
05668b0d4753atagen2026-10-16 05:50 UTC274
  return true;
05668b0d4753atagen2026-10-16 05:50 UTC275
}
05668b0d4753atagen2026-10-16 05:50 UTC276
function chg(a, b) { return { x: !(eq(a.x, b.x) && eq(a.w, b.w)), y: !(eq(a.y, b.y) && eq(a.h, b.h)) }; }
cd7085e54c88atagen2026-10-16 06:58 UTC277
function pureOk(keys) {
cd7085e54c88atagen2026-10-16 06:58 UTC278
  if (STYLE !== 'orthogonal') return true;
cd7085e54c88atagen2026-10-16 06:58 UTC279
  for (let k = 0; k + 1 < keys.length; k++)
cd7085e54c88atagen2026-10-16 06:58 UTC280
    for (const id in keys[k + 1]) { const c = chg(keys[k][id], keys[k + 1][id]); if (c.x && c.y) return false; }
cd7085e54c88atagen2026-10-16 06:58 UTC281
  return true;
cd7085e54c88atagen2026-10-16 06:58 UTC282
}
05668b0d4753atagen2026-10-16 05:50 UTC283
440b67836dd5atagen2026-10-16 07:57 UTC284
function signature(L) { return Object.keys(L).sort().map(id => { const r = L[id]; return [r.x, r.y, r.w, r.h].map(v => v.toFixed(1)).join(','); }).join(';'); }
440b67836dd5atagen2026-10-16 07:57 UTC285
05668b0d4753atagen2026-10-16 05:50 UTC286
function plan(from, to, opts = {}) {
440b67836dd5atagen2026-10-16 07:57 UTC287
  if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [PHASE_MS] };
440b67836dd5atagen2026-10-16 07:57 UTC288
  if (signature(from) <= signature(to)) return planOneWay(from, to);
440b67836dd5atagen2026-10-16 07:57 UTC289
  const p = planOneWay(to, from);
440b67836dd5atagen2026-10-16 07:57 UTC290
  return { kind: p.kind, keys: [...p.keys].reverse(), durs: [...p.durs].reverse() };
440b67836dd5atagen2026-10-16 07:57 UTC291
}
440b67836dd5atagen2026-10-16 07:57 UTC292
440b67836dd5atagen2026-10-16 07:57 UTC293
function planOneWay(from, to) {
05668b0d4753atagen2026-10-16 05:50 UTC294
  const list = Object.keys(to);
05668b0d4753atagen2026-10-16 05:50 UTC295
  const T = PHASE_MS;
05668b0d4753atagen2026-10-16 05:50 UTC296
  const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; });
05668b0d4753atagen2026-10-16 05:50 UTC297
  if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] };
05668b0d4753atagen2026-10-16 05:50 UTC298
  const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); });
05668b0d4753atagen2026-10-16 05:50 UTC299
  if (phaseOk(from, to, list) && (STYLE === 'direct' || pure))
05668b0d4753atagen2026-10-16 05:50 UTC300
    return { kind: '1 phase', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC301
  const two = axisOrder(from, to, list, movers);
05668b0d4753atagen2026-10-16 05:50 UTC302
  if (two) return two;
05668b0d4753atagen2026-10-16 05:50 UTC303
  for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; }
cd7085e54c88atagen2026-10-16 06:58 UTC304
  const g = grid(from, to, list, movers);
cd7085e54c88atagen2026-10-16 06:58 UTC305
  if (g) return g;
05668b0d4753atagen2026-10-16 05:50 UTC306
  return pinch(from, to, movers);
05668b0d4753atagen2026-10-16 05:50 UTC307
}
05668b0d4753atagen2026-10-16 05:50 UTC308
cd7085e54c88atagen2026-10-16 06:58 UTC309
function conflictSet(from, to, list) {
cd7085e54c88atagen2026-10-16 06:58 UTC310
  const out = new Set();
cd7085e54c88atagen2026-10-16 06:58 UTC311
  for (let i = 0; i < list.length; i++)
cd7085e54c88atagen2026-10-16 06:58 UTC312
    for (let j = i + 1; j < list.length; j++) {
cd7085e54c88atagen2026-10-16 06:58 UTC313
      const a = list[i], b = list[j];
cd7085e54c88atagen2026-10-16 06:58 UTC314
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { out.add(a); out.add(b); }
cd7085e54c88atagen2026-10-16 06:58 UTC315
    }
cd7085e54c88atagen2026-10-16 06:58 UTC316
  return [...out];
cd7085e54c88atagen2026-10-16 06:58 UTC317
}
cd7085e54c88atagen2026-10-16 06:58 UTC318
cd7085e54c88atagen2026-10-16 06:58 UTC319
function grid(from, to, list, movers) {
cd7085e54c88atagen2026-10-16 06:58 UTC320
  const cx = r => r.x + r.w / 2, cy = r => r.y + r.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC321
  for (const set of [conflictSet(from, to, list), movers, list]) {
cd7085e54c88atagen2026-10-16 06:58 UTC322
    const n = set.length;
cd7085e54c88atagen2026-10-16 06:58 UTC323
    if (n < 2) continue;
cd7085e54c88atagen2026-10-16 06:58 UTC324
    let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
cd7085e54c88atagen2026-10-16 06:58 UTC325
    for (const id of set) for (const r of [from[id], to[id]]) {
cd7085e54c88atagen2026-10-16 06:58 UTC326
      x0 = Math.min(x0, r.x); x1 = Math.max(x1, r.x + r.w); y0 = Math.min(y0, r.y); y1 = Math.max(y1, r.y + r.h);
cd7085e54c88atagen2026-10-16 06:58 UTC327
    }
cd7085e54c88atagen2026-10-16 06:58 UTC328
    const sh = (y1 - y0 - (n - 1) * G) / n, sw = (x1 - x0 - (n - 1) * G) / n;
cd7085e54c88atagen2026-10-16 06:58 UTC329
    const rowOf = {}, colOf = {};
cd7085e54c88atagen2026-10-16 06:58 UTC330
    [...set].sort((a, b) => cy(from[a]) - cy(from[b]) || cx(from[a]) - cx(from[b])).forEach((id, i) => rowOf[id] = y0 + i * (sh + G));
cd7085e54c88atagen2026-10-16 06:58 UTC331
    [...set].sort((a, b) => cx(to[a]) - cx(to[b]) || cy(to[a]) - cy(to[b])).forEach((id, i) => colOf[id] = x0 + i * (sw + G));
cd7085e54c88atagen2026-10-16 06:58 UTC332
    const inSet = new Set(set);
cd7085e54c88atagen2026-10-16 06:58 UTC333
    for (const phase of [3, 2, 4, 1]) {
cd7085e54c88atagen2026-10-16 06:58 UTC334
      const K = [{}, {}, {}];
cd7085e54c88atagen2026-10-16 06:58 UTC335
      for (const id of list) {
cd7085e54c88atagen2026-10-16 06:58 UTC336
        const f = from[id], t = to[id];
cd7085e54c88atagen2026-10-16 06:58 UTC337
        if (inSet.has(id)) {
cd7085e54c88atagen2026-10-16 06:58 UTC338
          K[0][id] = R(f.x, rowOf[id], f.w, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC339
          K[1][id] = R(colOf[id], rowOf[id], sw, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC340
          K[2][id] = R(colOf[id], t.y, sw, t.h);
cd7085e54c88atagen2026-10-16 06:58 UTC341
        } else for (let k = 0; k < 3; k++) K[k][id] = k + 1 >= phase ? t : f;
cd7085e54c88atagen2026-10-16 06:58 UTC342
      }
cd7085e54c88atagen2026-10-16 06:58 UTC343
      const keys = [from, ...K, to];
cd7085e54c88atagen2026-10-16 06:58 UTC344
      let ok = true;
cd7085e54c88atagen2026-10-16 06:58 UTC345
      for (let k = 0; k < 4 && ok; k++) ok = phaseOk(keys[k], keys[k + 1], list);
cd7085e54c88atagen2026-10-16 06:58 UTC346
      if (ok && pureOk(keys)) return { kind: `4 phases (${n}×${n} grid)`, keys, durs: [PHASE_MS, PHASE_MS, PHASE_MS, PHASE_MS] };
cd7085e54c88atagen2026-10-16 06:58 UTC347
    }
cd7085e54c88atagen2026-10-16 06:58 UTC348
  }
cd7085e54c88atagen2026-10-16 06:58 UTC349
  return null;
cd7085e54c88atagen2026-10-16 06:58 UTC350
}
cd7085e54c88atagen2026-10-16 06:58 UTC351
05668b0d4753atagen2026-10-16 05:50 UTC352
function mixed(f, t, xFirst) { return xFirst ? R(t.x, f.y, t.w, f.h) : R(f.x, t.y, f.w, t.h); }
05668b0d4753atagen2026-10-16 05:50 UTC353
440b67836dd5atagen2026-10-16 07:57 UTC354
function adjBits(a, b) {
440b67836dd5atagen2026-10-16 07:57 UTC355
  const ovX = a.x < b.x + b.w + EPS && b.x < a.x + a.w + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC356
  const ovY = a.y < b.y + b.h + EPS && b.y < a.y + a.h + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC357
  let s = 0;
440b67836dd5atagen2026-10-16 07:57 UTC358
  if (ovY && Math.abs(b.x - (a.x + a.w)) <= G + EPS) s |= 1;
440b67836dd5atagen2026-10-16 07:57 UTC359
  if (ovY && Math.abs(a.x - (b.x + b.w)) <= G + EPS) s |= 2;
440b67836dd5atagen2026-10-16 07:57 UTC360
  if (ovX && Math.abs(b.y - (a.y + a.h)) <= G + EPS) s |= 4;
440b67836dd5atagen2026-10-16 07:57 UTC361
  if (ovX && Math.abs(a.y - (b.y + b.h)) <= G + EPS) s |= 8;
440b67836dd5atagen2026-10-16 07:57 UTC362
  return s;
440b67836dd5atagen2026-10-16 07:57 UTC363
}
440b67836dd5atagen2026-10-16 07:57 UTC364
function coupledEdges(from, to, list) {
440b67836dd5atagen2026-10-16 07:57 UTC365
  const out = [];
440b67836dd5atagen2026-10-16 07:57 UTC366
  for (let i = 0; i < list.length; i++)
440b67836dd5atagen2026-10-16 07:57 UTC367
    for (let j = i + 1; j < list.length; j++) {
440b67836dd5atagen2026-10-16 07:57 UTC368
      const a = list[i], b = list[j], s = adjBits(from[a], from[b]) & adjBits(to[a], to[b]);
440b67836dd5atagen2026-10-16 07:57 UTC369
      if (s) out.push([a, b, s]);
440b67836dd5atagen2026-10-16 07:57 UTC370
    }
440b67836dd5atagen2026-10-16 07:57 UTC371
  return out;
440b67836dd5atagen2026-10-16 07:57 UTC372
}
440b67836dd5atagen2026-10-16 07:57 UTC373
function holes(K, coupled) {
440b67836dd5atagen2026-10-16 07:57 UTC374
  let n = 0;
440b67836dd5atagen2026-10-16 07:57 UTC375
  for (const [a, b, s] of coupled) if ((adjBits(K[a], K[b]) & s) !== s) n++;
440b67836dd5atagen2026-10-16 07:57 UTC376
  return n;
440b67836dd5atagen2026-10-16 07:57 UTC377
}
440b67836dd5atagen2026-10-16 07:57 UTC378
05668b0d4753atagen2026-10-16 05:50 UTC379
function axisOrder(from, to, list, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC380
  const n = movers.length;
05668b0d4753atagen2026-10-16 05:50 UTC381
  if (n > 14) return null;
440b67836dd5atagen2026-10-16 07:57 UTC382
  const coupled = coupledEdges(from, to, list);
05668b0d4753atagen2026-10-16 05:50 UTC383
  let best = null, bestScore = Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC384
  for (let mask = 0; mask < (1 << n); mask++) {
05668b0d4753atagen2026-10-16 05:50 UTC385
    const K1 = { ...from };
440b67836dd5atagen2026-10-16 07:57 UTC386
    let late = 0;
05668b0d4753atagen2026-10-16 05:50 UTC387
    movers.forEach((id, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC388
      const xf = !!(mask & (1 << i));
05668b0d4753atagen2026-10-16 05:50 UTC389
      const c = chg(from[id], to[id]);
05668b0d4753atagen2026-10-16 05:50 UTC390
      K1[id] = mixed(from[id], to[id], xf);
440b67836dd5atagen2026-10-16 07:57 UTC391
      if (c.x && !c.y && !xf) late++;
440b67836dd5atagen2026-10-16 07:57 UTC392
      if (c.y && !c.x && xf) late++;
05668b0d4753atagen2026-10-16 05:50 UTC393
    });
440b67836dd5atagen2026-10-16 07:57 UTC394
    if (late >= bestScore) continue;
440b67836dd5atagen2026-10-16 07:57 UTC395
    if (!phaseOk(from, K1, list) || !phaseOk(K1, to, list)) continue;
440b67836dd5atagen2026-10-16 07:57 UTC396
    const score = holes(K1, coupled) * 1000 + late;
440b67836dd5atagen2026-10-16 07:57 UTC397
    if (score < bestScore) { best = K1; bestScore = score; if (score === 0) break; }
05668b0d4753atagen2026-10-16 05:50 UTC398
  }
440b67836dd5atagen2026-10-16 07:57 UTC399
  return best ? { kind: '2 phases (axis order)' + (bestScore >= 1000 ? ', opens a hole' : ''), keys: [from, best, to], durs: [PHASE_MS, PHASE_MS] } : null;
05668b0d4753atagen2026-10-16 05:50 UTC400
}
05668b0d4753atagen2026-10-16 05:50 UTC401
05668b0d4753atagen2026-10-16 05:50 UTC402
function lanes(from, to, list, movers, ax) {
05668b0d4753atagen2026-10-16 05:50 UTC403
  const size = ax === 'y' ? 'h' : 'w';
05668b0d4753atagen2026-10-16 05:50 UTC404
  const bitsT = ax === 'y' ? 3 : 12;
05668b0d4753atagen2026-10-16 05:50 UTC405
  const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2;
05668b0d4753atagen2026-10-16 05:50 UTC406
  const conflict = new Set();
05668b0d4753atagen2026-10-16 05:50 UTC407
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC408
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC409
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC410
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); }
05668b0d4753atagen2026-10-16 05:50 UTC411
    }
05668b0d4753atagen2026-10-16 05:50 UTC412
  const sets = [[...conflict], movers, list];
05668b0d4753atagen2026-10-16 05:50 UTC413
  for (const set of sets) {
05668b0d4753atagen2026-10-16 05:50 UTC414
    const n = set.length;
05668b0d4753atagen2026-10-16 05:50 UTC415
    if (n < 2) continue;
05668b0d4753atagen2026-10-16 05:50 UTC416
    let lo = Infinity, hi = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC417
    for (const id of set) for (const r of [from[id], to[id]]) { lo = Math.min(lo, r[ax]); hi = Math.max(hi, r[ax] + r[size]); }
05668b0d4753atagen2026-10-16 05:50 UTC418
    const diff = [], lt = [];
05668b0d4753atagen2026-10-16 05:50 UTC419
    for (let i = 0; i < n; i++) { diff.push(new Array(n).fill(false)); lt.push(new Array(n).fill(false)); }
05668b0d4753atagen2026-10-16 05:50 UTC420
    for (let i = 0; i < n; i++)
05668b0d4753atagen2026-10-16 05:50 UTC421
      for (let j = 0; j < n; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC422
        if (i === j) continue;
05668b0d4753atagen2026-10-16 05:50 UTC423
        const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]);
05668b0d4753atagen2026-10-16 05:50 UTC424
        if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC425
        for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC426
      }
05668b0d4753atagen2026-10-16 05:50 UTC427
    for (let k = 2; k <= n; k++) {
05668b0d4753atagen2026-10-16 05:50 UTC428
      const s = (hi - lo - (k - 1) * G) / k;
05668b0d4753atagen2026-10-16 05:50 UTC429
      if (s < 6) break;
05668b0d4753atagen2026-10-16 05:50 UTC430
      const lane = new Array(n).fill(-1);
05668b0d4753atagen2026-10-16 05:50 UTC431
      const assign = i => {
05668b0d4753atagen2026-10-16 05:50 UTC432
        if (i === n) return true;
05668b0d4753atagen2026-10-16 05:50 UTC433
        for (let l = 0; l < k; l++) {
05668b0d4753atagen2026-10-16 05:50 UTC434
          let ok = true;
05668b0d4753atagen2026-10-16 05:50 UTC435
          for (let j = 0; j < i && ok; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC436
            if (diff[i][j] && lane[j] === l) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC437
            if (lt[i][j] && !(l < lane[j])) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC438
            if (lt[j][i] && !(lane[j] < l)) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC439
          }
05668b0d4753atagen2026-10-16 05:50 UTC440
          if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; }
05668b0d4753atagen2026-10-16 05:50 UTC441
        }
05668b0d4753atagen2026-10-16 05:50 UTC442
        return false;
05668b0d4753atagen2026-10-16 05:50 UTC443
      };
05668b0d4753atagen2026-10-16 05:50 UTC444
      if (!assign(0)) continue;
05668b0d4753atagen2026-10-16 05:50 UTC445
      const strip = l => lo + l * (s + G);
05668b0d4753atagen2026-10-16 05:50 UTC446
      const inSet = new Set(set);
05668b0d4753atagen2026-10-16 05:50 UTC447
      for (const phase of [2, 1, 3]) {
05668b0d4753atagen2026-10-16 05:50 UTC448
        const K1 = {}, K2 = {};
05668b0d4753atagen2026-10-16 05:50 UTC449
        for (const id of list) {
05668b0d4753atagen2026-10-16 05:50 UTC450
          const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC451
          if (inSet.has(id)) {
05668b0d4753atagen2026-10-16 05:50 UTC452
            const l = lane[set.indexOf(id)];
05668b0d4753atagen2026-10-16 05:50 UTC453
            K1[id] = ax === 'y' ? R(f.x, strip(l), f.w, s) : R(strip(l), f.y, s, f.h);
05668b0d4753atagen2026-10-16 05:50 UTC454
            K2[id] = ax === 'y' ? R(t.x, strip(l), t.w, s) : R(strip(l), t.y, s, t.h);
05668b0d4753atagen2026-10-16 05:50 UTC455
          } else if (phase === 1) { K1[id] = t; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC456
          else if (phase === 2) { K1[id] = f; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC457
          else { K1[id] = f; K2[id] = f; }
05668b0d4753atagen2026-10-16 05:50 UTC458
        }
cd7085e54c88atagen2026-10-16 06:58 UTC459
        if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list) && pureOk([from, K1, K2, to]))
05668b0d4753atagen2026-10-16 05:50 UTC460
          return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC461
      }
05668b0d4753atagen2026-10-16 05:50 UTC462
    }
05668b0d4753atagen2026-10-16 05:50 UTC463
  }
05668b0d4753atagen2026-10-16 05:50 UTC464
  return null;
05668b0d4753atagen2026-10-16 05:50 UTC465
}
05668b0d4753atagen2026-10-16 05:50 UTC466
05668b0d4753atagen2026-10-16 05:50 UTC467
function pinch(from, to, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC468
  const K1 = { ...from }, K2 = { ...to };
05668b0d4753atagen2026-10-16 05:50 UTC469
  for (const id of movers) {
05668b0d4753atagen2026-10-16 05:50 UTC470
    const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC471
    K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC472
    K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC473
  }
05668b0d4753atagen2026-10-16 05:50 UTC474
  return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC475
}
05668b0d4753atagen2026-10-16 05:50 UTC476
05668b0d4753atagen2026-10-16 05:50 UTC477
function bbox(rects) {
05668b0d4753atagen2026-10-16 05:50 UTC478
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC479
  for (const r of rects) { x0 = Math.min(x0, r.x); y0 = Math.min(y0, r.y); x1 = Math.max(x1, r.x + r.w); y1 = Math.max(y1, r.y + r.h); }
05668b0d4753atagen2026-10-16 05:50 UTC480
  return R(x0, y0, x1 - x0, y1 - y0);
05668b0d4753atagen2026-10-16 05:50 UTC481
}
05668b0d4753atagen2026-10-16 05:50 UTC482
function planBlocks(from, to, groups) {
05668b0d4753atagen2026-10-16 05:50 UTC483
  const bf = {}, bt = {};
05668b0d4753atagen2026-10-16 05:50 UTC484
  for (const b in groups) { bf[b] = bbox(groups[b].map(id => from[id])); bt[b] = bbox(groups[b].map(id => to[id])); }
05668b0d4753atagen2026-10-16 05:50 UTC485
  const bp = plan(bf, bt);
05668b0d4753atagen2026-10-16 05:50 UTC486
  const last = bp.keys.length - 1;
05668b0d4753atagen2026-10-16 05:50 UTC487
  const keys = bp.keys.map((K, ki) => {
05668b0d4753atagen2026-10-16 05:50 UTC488
    if (ki === 0) return from;
05668b0d4753atagen2026-10-16 05:50 UTC489
    if (ki === last) return to;
05668b0d4753atagen2026-10-16 05:50 UTC490
    const L = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC491
    for (const b in groups) for (const id of groups[b]) {
05668b0d4753atagen2026-10-16 05:50 UTC492
      const r = from[id], box = bf[b], k = K[b];
05668b0d4753atagen2026-10-16 05:50 UTC493
      const nx = box.w ? (r.x - box.x) / box.w : 0, ny = box.h ? (r.y - box.y) / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC494
      const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC495
      L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h);
05668b0d4753atagen2026-10-16 05:50 UTC496
    }
05668b0d4753atagen2026-10-16 05:50 UTC497
    return L;
05668b0d4753atagen2026-10-16 05:50 UTC498
  });
05668b0d4753atagen2026-10-16 05:50 UTC499
  return { kind: bp.kind + ' on blocks', keys, durs: bp.durs };
05668b0d4753atagen2026-10-16 05:50 UTC500
}
05668b0d4753atagen2026-10-16 05:50 UTC501
05668b0d4753atagen2026-10-16 05:50 UTC502
function bezier(x1, y1, x2, y2) {
05668b0d4753atagen2026-10-16 05:50 UTC503
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
05668b0d4753atagen2026-10-16 05:50 UTC504
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
05668b0d4753atagen2026-10-16 05:50 UTC505
  const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u;
05668b0d4753atagen2026-10-16 05:50 UTC506
  const dX = u => (3 * ax * u + 2 * bx) * u + cx;
05668b0d4753atagen2026-10-16 05:50 UTC507
  return t => {
05668b0d4753atagen2026-10-16 05:50 UTC508
    if (t <= 0) return 0;
05668b0d4753atagen2026-10-16 05:50 UTC509
    if (t >= 1) return 1;
05668b0d4753atagen2026-10-16 05:50 UTC510
    let u = t;
05668b0d4753atagen2026-10-16 05:50 UTC511
    for (let i = 0; i < 8; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC512
      const x = X(u) - t;
05668b0d4753atagen2026-10-16 05:50 UTC513
      if (Math.abs(x) < 1e-6) return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC514
      const d = dX(u);
05668b0d4753atagen2026-10-16 05:50 UTC515
      if (Math.abs(d) < 1e-6) break;
05668b0d4753atagen2026-10-16 05:50 UTC516
      u -= x / d;
05668b0d4753atagen2026-10-16 05:50 UTC517
    }
05668b0d4753atagen2026-10-16 05:50 UTC518
    let lo = 0, hi = 1; u = t;
05668b0d4753atagen2026-10-16 05:50 UTC519
    for (let i = 0; i < 40; i++) { if (X(u) < t) lo = u; else hi = u; u = (lo + hi) / 2; }
05668b0d4753atagen2026-10-16 05:50 UTC520
    return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC521
  };
05668b0d4753atagen2026-10-16 05:50 UTC522
}
05668b0d4753atagen2026-10-16 05:50 UTC523
const BASE = bezier(0.45, 0, 0.2, 1);
05668b0d4753atagen2026-10-16 05:50 UTC524
const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h];
05668b0d4753atagen2026-10-16 05:50 UTC525
</script>
05668b0d4753atagen2026-10-16 05:50 UTC526
05668b0d4753atagen2026-10-16 05:50 UTC527
<script>
05668b0d4753atagen2026-10-16 05:50 UTC528
const PAL = { A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' };
05668b0d4753atagen2026-10-16 05:50 UTC529
const demos = [];
05668b0d4753atagen2026-10-16 05:50 UTC530
05668b0d4753atagen2026-10-16 05:50 UTC531
class Demo {
05668b0d4753atagen2026-10-16 05:50 UTC532
  constructor(id, W, H, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC533
    this.W = W; this.H = H;
05668b0d4753atagen2026-10-16 05:50 UTC534
    this.outputs = opts.outputs || [R(0, 0, W, H)];
cd7085e54c88atagen2026-10-16 06:58 UTC535
    this.floating = new Set(opts.floating || []);
05668b0d4753atagen2026-10-16 05:50 UTC536
    const el = document.getElementById(id);
05668b0d4753atagen2026-10-16 05:50 UTC537
    this.canvas = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC538
    const dpr = window.devicePixelRatio || 1;
05668b0d4753atagen2026-10-16 05:50 UTC539
    this.canvas.width = W * dpr; this.canvas.height = H * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC540
    this.canvas.style.maxWidth = W + 'px';
05668b0d4753atagen2026-10-16 05:50 UTC541
    this.ctx = this.canvas.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC542
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC543
    el.appendChild(this.canvas);
05668b0d4753atagen2026-10-16 05:50 UTC544
    if (opts.graph) {
05668b0d4753atagen2026-10-16 05:50 UTC545
      this.graph = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC546
      this.graph.width = W * dpr; this.graph.height = 90 * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC547
      this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px';
05668b0d4753atagen2026-10-16 05:50 UTC548
      this.gctx = this.graph.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC549
      this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC550
      el.appendChild(this.graph);
05668b0d4753atagen2026-10-16 05:50 UTC551
      this.hist = [];
05668b0d4753atagen2026-10-16 05:50 UTC552
    }
05668b0d4753atagen2026-10-16 05:50 UTC553
    this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls);
05668b0d4753atagen2026-10-16 05:50 UTC554
    this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status);
05668b0d4753atagen2026-10-16 05:50 UTC555
    this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle';
05668b0d4753atagen2026-10-16 05:50 UTC556
    demos.push(this);
05668b0d4753atagen2026-10-16 05:50 UTC557
  }
05668b0d4753atagen2026-10-16 05:50 UTC558
  button(label, fn) { const b = document.createElement('button'); b.textContent = label; b.onclick = fn; this.controls.appendChild(b); return b; }
05668b0d4753atagen2026-10-16 05:50 UTC559
  check(label, init, fn) {
05668b0d4753atagen2026-10-16 05:50 UTC560
    const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init;
05668b0d4753atagen2026-10-16 05:50 UTC561
    c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c;
05668b0d4753atagen2026-10-16 05:50 UTC562
  }
05668b0d4753atagen2026-10-16 05:50 UTC563
  set(L) { this.layout = clone(L); this.anim = null; }
05668b0d4753atagen2026-10-16 05:50 UTC564
  go(target, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC565
    const now = performance.now();
05668b0d4753atagen2026-10-16 05:50 UTC566
    const cur = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC567
    const from = {};
05668b0d4753atagen2026-10-16 05:50 UTC568
    for (const id in target) from[id] = cur[id] || target[id];
cd7085e54c88atagen2026-10-16 06:58 UTC569
    const p = opts.planFn ? opts.planFn(from) : opts.blocks ? planBlocks(from, target, opts.blocks) : plan(from, target, { naive: this.naive });
05668b0d4753atagen2026-10-16 05:50 UTC570
    const curves = p.durs.map(() => BASE);
05668b0d4753atagen2026-10-16 05:50 UTC571
    const interrupted = !!this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC572
    let matched = false;
05668b0d4753atagen2026-10-16 05:50 UTC573
    if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) {
05668b0d4753atagen2026-10-16 05:50 UTC574
      const K1 = p.keys[1];
05668b0d4753atagen2026-10-16 05:50 UTC575
      let best = null, bs = 0;
05668b0d4753atagen2026-10-16 05:50 UTC576
      for (const id in K1) {
05668b0d4753atagen2026-10-16 05:50 UTC577
        const v = this.vel[id]; if (!v) continue;
05668b0d4753atagen2026-10-16 05:50 UTC578
        const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]);
05668b0d4753atagen2026-10-16 05:50 UTC579
        const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue;
05668b0d4753atagen2026-10-16 05:50 UTC580
        const sp = Math.hypot(...v);
05668b0d4753atagen2026-10-16 05:50 UTC581
        if (sp > bs) { bs = sp; best = { v, D, dd }; }
05668b0d4753atagen2026-10-16 05:50 UTC582
      }
05668b0d4753atagen2026-10-16 05:50 UTC583
      if (best) {
05668b0d4753atagen2026-10-16 05:50 UTC584
        const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd;
05668b0d4753atagen2026-10-16 05:50 UTC585
        const slope = s * p.durs[0];
05668b0d4753atagen2026-10-16 05:50 UTC586
        if (slope > 0.05) { const x1 = 0.3; curves[0] = bezier(x1, Math.min(1, x1 * slope), 0.2, 1); matched = true; }
05668b0d4753atagen2026-10-16 05:50 UTC587
      }
05668b0d4753atagen2026-10-16 05:50 UTC588
    }
05668b0d4753atagen2026-10-16 05:50 UTC589
    this.anim = { keys: p.keys, durs: p.durs, curves, start: now };
05668b0d4753atagen2026-10-16 05:50 UTC590
    this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : '');
05668b0d4753atagen2026-10-16 05:50 UTC591
    this.overlapSeen = false;
05668b0d4753atagen2026-10-16 05:50 UTC592
  }
05668b0d4753atagen2026-10-16 05:50 UTC593
  sampleAt(t) {
05668b0d4753atagen2026-10-16 05:50 UTC594
    if (!this.anim) return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC595
    let e = t - this.anim.start;
05668b0d4753atagen2026-10-16 05:50 UTC596
    const { keys, durs, curves } = this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC597
    for (let i = 0; i < durs.length; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC598
      const d = durs[i];
05668b0d4753atagen2026-10-16 05:50 UTC599
      if (e < d) {
05668b0d4753atagen2026-10-16 05:50 UTC600
        this.phase = i;
05668b0d4753atagen2026-10-16 05:50 UTC601
        const s = curves[i](e / d);
05668b0d4753atagen2026-10-16 05:50 UTC602
        const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC603
        for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s);
05668b0d4753atagen2026-10-16 05:50 UTC604
        return L;
05668b0d4753atagen2026-10-16 05:50 UTC605
      }
05668b0d4753atagen2026-10-16 05:50 UTC606
      e -= d;
05668b0d4753atagen2026-10-16 05:50 UTC607
    }
05668b0d4753atagen2026-10-16 05:50 UTC608
    this.layout = clone(keys[keys.length - 1]);
05668b0d4753atagen2026-10-16 05:50 UTC609
    this.anim = null;
05668b0d4753atagen2026-10-16 05:50 UTC610
    return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC611
  }
05668b0d4753atagen2026-10-16 05:50 UTC612
  frame(now) {
05668b0d4753atagen2026-10-16 05:50 UTC613
    const L = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC614
    if (this.prev) {
05668b0d4753atagen2026-10-16 05:50 UTC615
      const dt = now - this.prev.t;
05668b0d4753atagen2026-10-16 05:50 UTC616
      if (dt > 0) for (const id in L) if (this.prev.L[id]) {
05668b0d4753atagen2026-10-16 05:50 UTC617
        const a = edges(this.prev.L[id]), b = edges(L[id]);
05668b0d4753atagen2026-10-16 05:50 UTC618
        this.vel[id] = b.map((v, i) => (v - a[i]) / dt);
05668b0d4753atagen2026-10-16 05:50 UTC619
      }
05668b0d4753atagen2026-10-16 05:50 UTC620
    }
05668b0d4753atagen2026-10-16 05:50 UTC621
    this.prev = { t: now, L };
05668b0d4753atagen2026-10-16 05:50 UTC622
    this.draw(L);
05668b0d4753atagen2026-10-16 05:50 UTC623
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC624
    if (overlaps.length) this.overlapSeen = true;
05668b0d4753atagen2026-10-16 05:50 UTC625
    const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : '';
05668b0d4753atagen2026-10-16 05:50 UTC626
    this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : '');
05668b0d4753atagen2026-10-16 05:50 UTC627
    if (this.graph) this.drawGraph(now);
05668b0d4753atagen2026-10-16 05:50 UTC628
  }
05668b0d4753atagen2026-10-16 05:50 UTC629
  findOverlaps(L) {
cd7085e54c88atagen2026-10-16 06:58 UTC630
    const ids = Object.keys(L).filter(id => L[id].w > 0.5 && L[id].h > 0.5 && !this.floating.has(id));
05668b0d4753atagen2026-10-16 05:50 UTC631
    const out = [];
05668b0d4753atagen2026-10-16 05:50 UTC632
    for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC633
      const a = L[ids[i]], b = L[ids[j]];
05668b0d4753atagen2026-10-16 05:50 UTC634
      const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
05668b0d4753atagen2026-10-16 05:50 UTC635
      const x1 = Math.min(a.x + a.w, b.x + b.w), y1 = Math.min(a.y + a.h, b.y + b.h);
05668b0d4753atagen2026-10-16 05:50 UTC636
      if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0));
05668b0d4753atagen2026-10-16 05:50 UTC637
    }
05668b0d4753atagen2026-10-16 05:50 UTC638
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC639
  }
05668b0d4753atagen2026-10-16 05:50 UTC640
  draw(L) {
05668b0d4753atagen2026-10-16 05:50 UTC641
    const c = this.ctx;
05668b0d4753atagen2026-10-16 05:50 UTC642
    c.clearRect(0, 0, this.W, this.H);
05668b0d4753atagen2026-10-16 05:50 UTC643
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC644
    for (const o of this.outputs) {
05668b0d4753atagen2026-10-16 05:50 UTC645
      c.save();
05668b0d4753atagen2026-10-16 05:50 UTC646
      c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h);
05668b0d4753atagen2026-10-16 05:50 UTC647
      c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip();
cd7085e54c88atagen2026-10-16 06:58 UTC648
      for (const id of Object.keys(L).sort((a, b) => this.floating.has(a) - this.floating.has(b))) {
05668b0d4753atagen2026-10-16 05:50 UTC649
        const r = L[id];
05668b0d4753atagen2026-10-16 05:50 UTC650
        if (r.w < 0.5 || r.h < 0.5) continue;
05668b0d4753atagen2026-10-16 05:50 UTC651
        const rad = Math.min(8, r.w / 2, r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC652
        c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad);
05668b0d4753atagen2026-10-16 05:50 UTC653
        c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill();
05668b0d4753atagen2026-10-16 05:50 UTC654
        c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC655
        if (r.w > 22 && r.h > 22) {
05668b0d4753atagen2026-10-16 05:50 UTC656
          c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle';
05668b0d4753atagen2026-10-16 05:50 UTC657
          c.fillText(id, r.x + r.w / 2, r.y + r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC658
        }
05668b0d4753atagen2026-10-16 05:50 UTC659
      }
05668b0d4753atagen2026-10-16 05:50 UTC660
      for (const o2 of overlaps) { c.fillStyle = 'rgba(255,77,94,0.55)'; c.fillRect(o2.x, o2.y, o2.w, o2.h); c.strokeStyle = '#ff4d5e'; c.lineWidth = 2; c.strokeRect(o2.x, o2.y, o2.w, o2.h); }
05668b0d4753atagen2026-10-16 05:50 UTC661
      c.restore();
05668b0d4753atagen2026-10-16 05:50 UTC662
    }
05668b0d4753atagen2026-10-16 05:50 UTC663
  }
05668b0d4753atagen2026-10-16 05:50 UTC664
  drawGraph(now) {
05668b0d4753atagen2026-10-16 05:50 UTC665
    const v = this.vel.A;
05668b0d4753atagen2026-10-16 05:50 UTC666
    const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0;
05668b0d4753atagen2026-10-16 05:50 UTC667
    this.hist.push([now, sp]);
05668b0d4753atagen2026-10-16 05:50 UTC668
    while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift();
05668b0d4753atagen2026-10-16 05:50 UTC669
    const c = this.gctx, W = this.W, H = 90;
05668b0d4753atagen2026-10-16 05:50 UTC670
    c.clearRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC671
    c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC672
    const max = Math.max(400, ...this.hist.map(h => h[1]));
05668b0d4753atagen2026-10-16 05:50 UTC673
    c.beginPath();
05668b0d4753atagen2026-10-16 05:50 UTC674
    this.hist.forEach(([t, s], i) => { const x = W - (now - t) / 4000 * W, y = H - 6 - s / max * (H - 16); i ? c.lineTo(x, y) : c.moveTo(x, y); });
05668b0d4753atagen2026-10-16 05:50 UTC675
    c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC676
    c.fillStyle = '#8a909c'; c.font = '12px system-ui'; c.textAlign = 'left'; c.fillText('speed of window A, last 4 s', 8, 14);
05668b0d4753atagen2026-10-16 05:50 UTC677
  }
05668b0d4753atagen2026-10-16 05:50 UTC678
}
05668b0d4753atagen2026-10-16 05:50 UTC679
05668b0d4753atagen2026-10-16 05:50 UTC680
const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G);
05668b0d4753atagen2026-10-16 05:50 UTC681
05668b0d4753atagen2026-10-16 05:50 UTC682
{ // d1 swap
05668b0d4753atagen2026-10-16 05:50 UTC683
  const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC684
  let swapped = false;
05668b0d4753atagen2026-10-16 05:50 UTC685
  const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']);
05668b0d4753atagen2026-10-16 05:50 UTC686
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC687
  d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC688
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC689
}
05668b0d4753atagen2026-10-16 05:50 UTC690
05668b0d4753atagen2026-10-16 05:50 UTC691
{ // d2 one phase
05668b0d4753atagen2026-10-16 05:50 UTC692
  const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC693
  const s = { wide: false, d: false, e: false };
cd7085e54c88atagen2026-10-16 06:58 UTC694
  const modes = ['line on old boundary', 'point on old boundary', 'target centre'];
cd7085e54c88atagen2026-10-16 06:58 UTC695
  let mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC696
  const ratios = ids => ids.map(id => (id === 'A' && s.wide) ? 2 : 1);
05668b0d4753atagen2026-10-16 05:50 UTC697
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC698
    const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C'];
cd7085e54c88atagen2026-10-16 06:58 UTC699
    const out = splitX(I, ids, ratios(ids));
cd7085e54c88atagen2026-10-16 06:58 UTC700
    if (!s.d) {
cd7085e54c88atagen2026-10-16 06:58 UTC701
      const bx = (out.B.x + out.B.w + out.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC702
      const T = splitX(I, ['A', 'B', 'D', 'C'], ratios(['A', 'B', 'D', 'C'])).D;
cd7085e54c88atagen2026-10-16 06:58 UTC703
      out.D = [R(bx, I.y, 0, I.h), R(bx, I.y + I.h / 2, 0, 0), R(T.x + T.w / 2, T.y + T.h / 2, 0, 0)][mode];
cd7085e54c88atagen2026-10-16 06:58 UTC704
    }
05668b0d4753atagen2026-10-16 05:50 UTC705
    const b = out.B;
05668b0d4753atagen2026-10-16 05:50 UTC706
    if (s.e) Object.assign(out, splitY(b, ['B', 'E'])); else out.E = R(b.x, b.y + b.h, b.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC707
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC708
  };
05668b0d4753atagen2026-10-16 05:50 UTC709
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC710
  d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC711
  d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC712
  d.button('Split B with E', () => { s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC713
  d.button('All at once', () => { s.wide = !s.wide; s.d = !s.d; s.e = !s.e; d.go(L()); });
cd7085e54c88atagen2026-10-16 06:58 UTC714
  const mb = d.button('seed: ' + modes[0], () => {
cd7085e54c88atagen2026-10-16 06:58 UTC715
    mode = (mode + 1) % modes.length; mb.textContent = 'seed: ' + modes[mode];
cd7085e54c88atagen2026-10-16 06:58 UTC716
    if (!s.d && !d.anim) d.set(L());
cd7085e54c88atagen2026-10-16 06:58 UTC717
  });
05668b0d4753atagen2026-10-16 05:50 UTC718
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC719
}
05668b0d4753atagen2026-10-16 05:50 UTC720
function tagLayout(I, view2, order) {
05668b0d4753atagen2026-10-16 05:50 UTC721
  const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC722
  if (view2) {
05668b0d4753atagen2026-10-16 05:50 UTC723
    const rows = splitY(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC724
    Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC725
  } else {
05668b0d4753atagen2026-10-16 05:50 UTC726
    Object.assign(L, splitX(I, ['A', 'B', 'C']));
05668b0d4753atagen2026-10-16 05:50 UTC727
    const y = order[0] === 't2' ? I.y : I.y + I.h;
05668b0d4753atagen2026-10-16 05:50 UTC728
    Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC729
  }
05668b0d4753atagen2026-10-16 05:50 UTC730
  return L;
05668b0d4753atagen2026-10-16 05:50 UTC731
}
05668b0d4753atagen2026-10-16 05:50 UTC732
05668b0d4753atagen2026-10-16 05:50 UTC733
{ // d3 tag toggle
05668b0d4753atagen2026-10-16 05:50 UTC734
  const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC735
  let view2 = false;
05668b0d4753atagen2026-10-16 05:50 UTC736
  d.set(tagLayout(I, view2, ['t1', 't2']));
05668b0d4753atagen2026-10-16 05:50 UTC737
  d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); });
05668b0d4753atagen2026-10-16 05:50 UTC738
}
05668b0d4753atagen2026-10-16 05:50 UTC739
05668b0d4753atagen2026-10-16 05:50 UTC740
{ // d3b tag reorder
05668b0d4753atagen2026-10-16 05:50 UTC741
  const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC742
  let order = ['t1', 't2'], blocks = true;
05668b0d4753atagen2026-10-16 05:50 UTC743
  d.set(tagLayout(I, true, order));
05668b0d4753atagen2026-10-16 05:50 UTC744
  d.button('Swap tag order', () => { order = [order[1], order[0]]; d.go(tagLayout(I, true, order), blocks ? { blocks: { t1: ['A', 'B', 'C'], t2: ['D', 'E'] } } : {}); });
05668b0d4753atagen2026-10-16 05:50 UTC745
  d.check('rigid blocks', true, v => blocks = v);
05668b0d4753atagen2026-10-16 05:50 UTC746
}
05668b0d4753atagen2026-10-16 05:50 UTC747
05668b0d4753atagen2026-10-16 05:50 UTC748
{ // d4a orientation flip
05668b0d4753atagen2026-10-16 05:50 UTC749
  const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC750
  let rows = false;
05668b0d4753atagen2026-10-16 05:50 UTC751
  const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']);
05668b0d4753atagen2026-10-16 05:50 UTC752
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC753
  d.button('Flip orientation', () => { rows = !rows; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC754
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC755
}
05668b0d4753atagen2026-10-16 05:50 UTC756
05668b0d4753atagen2026-10-16 05:50 UTC757
{ // d4b grow both axes
05668b0d4753atagen2026-10-16 05:50 UTC758
  const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC759
  let wide = false;
05668b0d4753atagen2026-10-16 05:50 UTC760
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC761
    const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]);
05668b0d4753atagen2026-10-16 05:50 UTC762
    return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) };
05668b0d4753atagen2026-10-16 05:50 UTC763
  };
05668b0d4753atagen2026-10-16 05:50 UTC764
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC765
  d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC766
}
05668b0d4753atagen2026-10-16 05:50 UTC767
05668b0d4753atagen2026-10-16 05:50 UTC768
{ // d5 lanes
05668b0d4753atagen2026-10-16 05:50 UTC769
  const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC770
  let order = ['A', 'B', 'C', 'D'];
05668b0d4753atagen2026-10-16 05:50 UTC771
  const L = () => splitX(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC772
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC773
  d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC774
  d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC775
  d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC776
  d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC777
}
05668b0d4753atagen2026-10-16 05:50 UTC778
05668b0d4753atagen2026-10-16 05:50 UTC779
{ // d6 interrupts
05668b0d4753atagen2026-10-16 05:50 UTC780
  const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
aa790e7e33a0atagen2026-10-16 08:03 UTC781
  const names = ["A B C", "C A B", "A wide", "B C A", "close B"];
05668b0d4753atagen2026-10-16 05:50 UTC782
  const targets = [
05668b0d4753atagen2026-10-16 05:50 UTC783
    () => splitX(I, ['A', 'B', 'C']),
05668b0d4753atagen2026-10-16 05:50 UTC784
    () => splitX(I, ['C', 'A', 'B']),
05668b0d4753atagen2026-10-16 05:50 UTC785
    () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]),
05668b0d4753atagen2026-10-16 05:50 UTC786
    () => splitX(I, ['B', 'C', 'A']),
05668b0d4753atagen2026-10-16 05:50 UTC787
    () => ({ ...splitX(I, ['A', 'C'], [2, 1]), B: R(I.x + I.w * 0.66, I.y + I.h, I.w * 0.34, 0) }),
05668b0d4753atagen2026-10-16 05:50 UTC788
  ];
05668b0d4753atagen2026-10-16 05:50 UTC789
  const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
05668b0d4753atagen2026-10-16 05:50 UTC790
  let idx = 0;
05668b0d4753atagen2026-10-16 05:50 UTC791
  d.set(withB(targets[0]()));
aa790e7e33a0atagen2026-10-16 08:03 UTC792
  const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); d.lastKind += ` · target "${names[idx]}"`; };
05668b0d4753atagen2026-10-16 05:50 UTC793
  d.button('Next target', next);
05668b0d4753atagen2026-10-16 05:50 UTC794
  d.button('Storm (8 rapid interrupts)', () => {
05668b0d4753atagen2026-10-16 05:50 UTC795
    let n = 0;
05668b0d4753atagen2026-10-16 05:50 UTC796
    const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); };
05668b0d4753atagen2026-10-16 05:50 UTC797
    fire();
05668b0d4753atagen2026-10-16 05:50 UTC798
  });
05668b0d4753atagen2026-10-16 05:50 UTC799
  d.check('match velocity', true, v => d.vmatch = v);
05668b0d4753atagen2026-10-16 05:50 UTC800
}
05668b0d4753atagen2026-10-16 05:50 UTC801
05668b0d4753atagen2026-10-16 05:50 UTC802
{ // d7 across outputs
05668b0d4753atagen2026-10-16 05:50 UTC803
  const W = 680, H = 260;
05668b0d4753atagen2026-10-16 05:50 UTC804
  const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H);
05668b0d4753atagen2026-10-16 05:50 UTC805
  const d = new Demo('d7', W, H, { outputs: [o1, o2] });
05668b0d4753atagen2026-10-16 05:50 UTC806
  const I1 = inner(320, H, 0), I2 = inner(320, H, 360);
05668b0d4753atagen2026-10-16 05:50 UTC807
  let s1 = ['A', 'B'], s2 = ['C'];
05668b0d4753atagen2026-10-16 05:50 UTC808
  const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) });
05668b0d4753atagen2026-10-16 05:50 UTC809
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC810
  d.button('Move B across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC811
    if (s1.includes('B')) { s1 = s1.filter(x => x !== 'B'); s2 = ['B', ...s2]; } else { s2 = s2.filter(x => x !== 'B'); s1 = [...s1, 'B']; }
05668b0d4753atagen2026-10-16 05:50 UTC812
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC813
  });
05668b0d4753atagen2026-10-16 05:50 UTC814
  d.button('Move A across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC815
    if (s1.includes('A')) { s1 = s1.filter(x => x !== 'A'); s2 = ['A', ...s2]; } else { s2 = s2.filter(x => x !== 'A'); s1 = ['A', ...s1]; }
05668b0d4753atagen2026-10-16 05:50 UTC816
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC817
  });
05668b0d4753atagen2026-10-16 05:50 UTC818
  d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC819
}
05668b0d4753atagen2026-10-16 05:50 UTC820
05668b0d4753atagen2026-10-16 05:50 UTC821
function layoutTree(node, rect, visible, out) {
05668b0d4753atagen2026-10-16 05:50 UTC822
  if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; }
05668b0d4753atagen2026-10-16 05:50 UTC823
  const horiz = node.dir === 'row';
05668b0d4753atagen2026-10-16 05:50 UTC824
  const shown = node.kids.map(k => anyVisible(k, visible));
05668b0d4753atagen2026-10-16 05:50 UTC825
  const m = shown.filter(Boolean).length;
05668b0d4753atagen2026-10-16 05:50 UTC826
  const len = horiz ? rect.w : rect.h;
05668b0d4753atagen2026-10-16 05:50 UTC827
  const avail = len - G * Math.max(0, m - 1);
05668b0d4753atagen2026-10-16 05:50 UTC828
  let pos = horiz ? rect.x : rect.y, seen = 0;
05668b0d4753atagen2026-10-16 05:50 UTC829
  node.kids.forEach((k, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC830
    if (!shown[i]) {
05668b0d4753atagen2026-10-16 05:50 UTC831
      const p = seen ? pos - G / 2 : pos;
05668b0d4753atagen2026-10-16 05:50 UTC832
      const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC833
      layoutTree(k, r, () => false, out);
05668b0d4753atagen2026-10-16 05:50 UTC834
      return;
05668b0d4753atagen2026-10-16 05:50 UTC835
    }
05668b0d4753atagen2026-10-16 05:50 UTC836
    const size = avail / m;
05668b0d4753atagen2026-10-16 05:50 UTC837
    const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size);
05668b0d4753atagen2026-10-16 05:50 UTC838
    layoutTree(k, r, visible, out);
05668b0d4753atagen2026-10-16 05:50 UTC839
    pos += size + G; seen++;
05668b0d4753atagen2026-10-16 05:50 UTC840
  });
05668b0d4753atagen2026-10-16 05:50 UTC841
}
05668b0d4753atagen2026-10-16 05:50 UTC842
function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); }
05668b0d4753atagen2026-10-16 05:50 UTC843
05668b0d4753atagen2026-10-16 05:50 UTC844
{ // d8 one tree, tags filter
05668b0d4753atagen2026-10-16 05:50 UTC845
  const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC846
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
05668b0d4753atagen2026-10-16 05:50 UTC847
  const view = { 1: true, 2: true };
05668b0d4753atagen2026-10-16 05:50 UTC848
  let mixed = false;
05668b0d4753atagen2026-10-16 05:50 UTC849
  const tree = () => mixed
05668b0d4753atagen2026-10-16 05:50 UTC850
    ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] }
05668b0d4753atagen2026-10-16 05:50 UTC851
    : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] };
05668b0d4753atagen2026-10-16 05:50 UTC852
  const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; };
05668b0d4753atagen2026-10-16 05:50 UTC853
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC854
  d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC855
  d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC856
  d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC857
}
05668b0d4753atagen2026-10-16 05:50 UTC858
cd7085e54c88atagen2026-10-16 06:58 UTC859
function stackLayouts(I) {
cd7085e54c88atagen2026-10-16 06:58 UTC860
  const col = (L, id, ids) => { const r = L[id]; delete L[id]; Object.assign(L, splitY(r, ids)); return L; };
cd7085e54c88atagen2026-10-16 06:58 UTC861
  const row = (L, id, ids) => { const r = L[id]; delete L[id]; Object.assign(L, splitX(r, ids)); return L; };
cd7085e54c88atagen2026-10-16 06:58 UTC862
  return {
cd7085e54c88atagen2026-10-16 06:58 UTC863
    'A | B | C': () => splitX(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC864
    'A | (B / C)': () => col(splitX(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC865
    '(A / C) | B': () => col(splitX(I, ['L', 'B']), 'L', ['A', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC866
    '(A / B) | C': () => col(splitX(I, ['L', 'C']), 'L', ['A', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC867
    'B | C | A': () => splitX(I, ['B', 'C', 'A']),
cd7085e54c88atagen2026-10-16 06:58 UTC868
    'A / B / C': () => splitY(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC869
    'A / (B | C)': () => row(splitY(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC870
    '(A / B) | (C / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'B']), 'R', ['C', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC871
    'A | (C / B / D)': () => col(splitX(I, ['A', 'R']), 'R', ['C', 'B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC872
    '(A / D) | (C / B)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'D']), 'R', ['C', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC873
    '(A / B / C) | D': () => col(splitX(I, ['L', 'D']), 'L', ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC874
    '(A / C) | (B / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'C']), 'R', ['B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC875
  };
cd7085e54c88atagen2026-10-16 06:58 UTC876
}
cd7085e54c88atagen2026-10-16 06:58 UTC877
cd7085e54c88atagen2026-10-16 06:58 UTC878
for (const [id, names] of [
cd7085e54c88atagen2026-10-16 06:58 UTC879
  ['d9a', ['A | B | C', 'A | (B / C)', '(A / C) | B', '(A / B) | C', 'B | C | A', 'A / B / C', 'A / (B | C)']],
cd7085e54c88atagen2026-10-16 06:58 UTC880
  ['d9b', ['(A / B) | (C / D)', 'A | (C / B / D)', '(A / D) | (C / B)', '(A / B / C) | D', '(A / C) | (B / D)']],
cd7085e54c88atagen2026-10-16 06:58 UTC881
]) {
cd7085e54c88atagen2026-10-16 06:58 UTC882
  const W = 640, H = 300, d = new Demo(id, W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC883
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC884
  for (const n of names) d.button(n, () => d.go(S[n]()));
cd7085e54c88atagen2026-10-16 06:58 UTC885
}
cd7085e54c88atagen2026-10-16 06:58 UTC886
cd7085e54c88atagen2026-10-16 06:58 UTC887
{ // d9c grid
cd7085e54c88atagen2026-10-16 06:58 UTC888
  const W = 640, H = 300, d = new Demo('d9c', W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC889
  let at = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC890
  const names = ['(A / D) | (C / B)', '(A / B / C) | D'];
cd7085e54c88atagen2026-10-16 06:58 UTC891
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC892
  d.button('(A / D) | (C / B)  ⇄  (A / B / C) | D', () => { at = 1 - at; d.go(S[names[at]]()); });
cd7085e54c88atagen2026-10-16 06:58 UTC893
}
cd7085e54c88atagen2026-10-16 06:58 UTC894
cd7085e54c88atagen2026-10-16 06:58 UTC895
{ // d10 floating to tiled
cd7085e54c88atagen2026-10-16 06:58 UTC896
  const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H);
cd7085e54c88atagen2026-10-16 06:58 UTC897
  const FL = R(220, 40, 220, 130);
aa790e7e33a0atagen2026-10-16 08:03 UTC898
  const modes = ['slot opens last', 'direct glide', 'slot opens first'];
aa790e7e33a0atagen2026-10-16 08:03 UTC899
  let tiled = false, mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC900
  const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']);
cd7085e54c88atagen2026-10-16 06:58 UTC901
  const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL });
cd7085e54c88atagen2026-10-16 06:58 UTC902
  const ortho = () => STYLE === 'orthogonal';
cd7085e54c88atagen2026-10-16 06:58 UTC903
  const assemble = (n, tiledAt, fAt, kind) => {
cd7085e54c88atagen2026-10-16 06:58 UTC904
    const keys = [];
cd7085e54c88atagen2026-10-16 06:58 UTC905
    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) }); }
cd7085e54c88atagen2026-10-16 06:58 UTC906
    return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) };
cd7085e54c88atagen2026-10-16 06:58 UTC907
  };
aa790e7e33a0atagen2026-10-16 08:03 UTC908
  const slotPlan = (from, to, opening) => {
aa790e7e33a0atagen2026-10-16 08:03 UTC909
    const fromBx = (from.B.x + from.B.w + from.C.x) / 2, toBx = (to.B.x + to.B.w + to.C.x) / 2;
aa790e7e33a0atagen2026-10-16 08:03 UTC910
    return opening
aa790e7e33a0atagen2026-10-16 08:03 UTC911
      ? 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 })
aa790e7e33a0atagen2026-10-16 08:03 UTC912
      : 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) });
aa790e7e33a0atagen2026-10-16 08:03 UTC913
  };
aa790e7e33a0atagen2026-10-16 08:03 UTC914
  const at = (tp, i) => tp.keys[Math.max(0, Math.min(i, tp.durs.length))];
cd7085e54c88atagen2026-10-16 06:58 UTC915
  const floatIn = from => {
aa790e7e33a0atagen2026-10-16 08:03 UTC916
    const to = tiledL(), slot = to.F, tp = slotPlan(from, to, true), k = tp.durs.length, f = from.F;
aa790e7e33a0atagen2026-10-16 08:03 UTC917
    if (modes[mode] === 'direct glide') {
aa790e7e33a0atagen2026-10-16 08:03 UTC918
      const n = Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC919
      return assemble(n, i => at(tp, i), i => i === 0 ? f : slot, `glide in, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC920
    }
cd7085e54c88atagen2026-10-16 06:58 UTC921
    const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC922
    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)];
aa790e7e33a0atagen2026-10-16 08:03 UTC923
    const travel = path.length - 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC924
    if (modes[mode] === 'slot opens last') {
aa790e7e33a0atagen2026-10-16 08:03 UTC925
      const n = travel + Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC926
      return assemble(n, i => at(tp, i - (n - k)), i => i === n ? slot : path[Math.min(i, travel)], `float in, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC927
    }
aa790e7e33a0atagen2026-10-16 08:03 UTC928
    const n = Math.max(travel, k) + 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC929
    return assemble(n, i => at(tp, i), i => i === n ? slot : path[Math.min(i, travel)], `float in, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC930
  };
cd7085e54c88atagen2026-10-16 06:58 UTC931
  const floatOut = from => {
aa790e7e33a0atagen2026-10-16 08:03 UTC932
    const to = floatL(), slot = from.F, tp = slotPlan(from, to, false), k = tp.durs.length;
aa790e7e33a0atagen2026-10-16 08:03 UTC933
    if (modes[mode] === 'direct glide') {
aa790e7e33a0atagen2026-10-16 08:03 UTC934
      const n = Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC935
      return assemble(n, i => at(tp, i), i => i === 0 ? slot : FL, `glide out, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC936
    }
cd7085e54c88atagen2026-10-16 06:58 UTC937
    const lift = R(slot.x + slot.w / 2 - FL.w / 2, slot.y + slot.h / 2 - FL.h / 2, FL.w, FL.h);
cd7085e54c88atagen2026-10-16 06:58 UTC938
    const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL];
aa790e7e33a0atagen2026-10-16 08:03 UTC939
    const travel = path.length - 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC940
    if (modes[mode] === 'slot opens last') {
aa790e7e33a0atagen2026-10-16 08:03 UTC941
      const n = Math.max(travel, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC942
      return assemble(n, i => at(tp, i), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC943
    }
aa790e7e33a0atagen2026-10-16 08:03 UTC944
    const n = Math.max(travel, k + 1);
aa790e7e33a0atagen2026-10-16 08:03 UTC945
    return assemble(n, i => at(tp, i - 1), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC946
  };
cd7085e54c88atagen2026-10-16 06:58 UTC947
  d.set(floatL());
cd7085e54c88atagen2026-10-16 06:58 UTC948
  d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); });
aa790e7e33a0atagen2026-10-16 08:03 UTC949
  const mb = d.button('mode: ' + modes[0], () => { mode = (mode + 1) % modes.length; mb.textContent = 'mode: ' + modes[mode]; });
cd7085e54c88atagen2026-10-16 06:58 UTC950
}
cd7085e54c88atagen2026-10-16 06:58 UTC951
05668b0d4753atagen2026-10-16 05:50 UTC952
document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };
05668b0d4753atagen2026-10-16 05:50 UTC953
document.getElementById('style').onchange = e => { STYLE = e.target.value; };
05668b0d4753atagen2026-10-16 05:50 UTC954
const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); };
05668b0d4753atagen2026-10-16 05:50 UTC955
requestAnimationFrame(loop);
05668b0d4753atagen2026-10-16 05:50 UTC956
</script>
05668b0d4753atagen2026-10-16 05:50 UTC957
</body>
05668b0d4753atagen2026-10-16 05:50 UTC958
</html>