atagen

monolith

public
Star0
Watch0
440b67836dd595993e4b0354ed0e838832bb836a
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>
cd7085e54c88atagen2026-10-16 06:58 UTC144
<p>A floating window joining the tiled layout is phased, unlike a spawn. The tiled windows open a slot for it, concurrently and following the same rules as an insertion. Meanwhile the floating window travels above them, one axis per phase, until it sits over the slot. In the last phase it falls into place. Lifting a tiled window back out runs the same steps in reverse. The floating window is drawn above the tiled layer and is exempt from the overlap rule while it travels.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC145
<div class="demo" id="d10"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC146
cd7085e54c88atagen2026-10-16 06:58 UTC147
<h2>13. Fallbacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC148
<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 UTC149
cd7085e54c88atagen2026-10-16 06:58 UTC150
<h2>14. Algorithm</h2>
05668b0d4753atagen2026-10-16 05:50 UTC151
<pre>plan(from, to):
440b67836dd5atagen2026-10-16 07:57 UTC152
  if from > to in a canonical order: return reverse(plan(to, from))
05668b0d4753atagen2026-10-16 05:50 UTC153
  groups = motion groups (windows whose swept areas can touch, transitively)
05668b0d4753atagen2026-10-16 05:50 UTC154
  for each group:
05668b0d4753atagen2026-10-16 05:50 UTC155
    if every pair shares a relation between from and to
05668b0d4753atagen2026-10-16 05:50 UTC156
       and (style is direct or every window changes only one axis):
05668b0d4753atagen2026-10-16 05:50 UTC157
      return [from, to]                                    # 1 phase
440b67836dd5atagen2026-10-16 07:57 UTC158
    if 2-SAT over axis order finds K1, preferring no holes: # mixed rects
05668b0d4753atagen2026-10-16 05:50 UTC159
      return [from, K1, to]                                # 2 phases
05668b0d4753atagen2026-10-16 05:50 UTC160
    for lane_axis in [y, x]:                               # strips, then columns
05668b0d4753atagen2026-10-16 05:50 UTC161
      for set in [windows in reversals, all movers, all]:
05668b0d4753atagen2026-10-16 05:50 UTC162
        for k in 2..|set|:
05668b0d4753atagen2026-10-16 05:50 UTC163
          if lanes assignable with k lanes and all three phases validate:
05668b0d4753atagen2026-10-16 05:50 UTC164
            return [from, K1, K2, to]                      # 3 phases
cd7085e54c88atagen2026-10-16 06:58 UTC165
    for set in [windows in reversals, all movers, all]:
cd7085e54c88atagen2026-10-16 06:58 UTC166
      if the grid validates for set:                       # always true for all
cd7085e54c88atagen2026-10-16 06:58 UTC167
        return [from, K1, K2, K3, to]                      # 4 phases
cd7085e54c88atagen2026-10-16 06:58 UTC168
    return fallback(from, to)                              # unreachable in theory
05668b0d4753atagen2026-10-16 05:50 UTC169
05668b0d4753atagen2026-10-16 05:50 UTC170
  rigid blocks: run plan() on block rectangles first, then map windows into them
cd7085e54c88atagen2026-10-16 06:58 UTC171
  floating:     slot opens via plan() with a seed, floater travels x, then y, then lands
05668b0d4753atagen2026-10-16 05:50 UTC172
  interrupts:   from = rectangles sampled at the current presentation time
05668b0d4753atagen2026-10-16 05:50 UTC173
  velocity:     first-phase curve per group, slope from the fastest mover,
05668b0d4753atagen2026-10-16 05:50 UTC174
                applied only if moving the same way, control point height ≤ 1
05668b0d4753atagen2026-10-16 05:50 UTC175
  validation:   exact pairwise check over the curve actually used</pre>
05668b0d4753atagen2026-10-16 05:50 UTC176
<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 UTC177
<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 UTC178
cd7085e54c88atagen2026-10-16 06:58 UTC179
<h2>15. Compared with wry master</h2>
05668b0d4753atagen2026-10-16 05:50 UTC180
<table>
05668b0d4753atagen2026-10-16 05:50 UTC181
  <tr><th>wry master</th><th>this design</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC182
  <tr><td>SingleAction, MixedSinglePhase</td><td>1 phase</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC183
  <tr><td>SpaceThenOrthogonalGrowth, OrientationChange, HierarchyOrderedScales</td><td>2 phases, one 2-SAT problem instead of three strategies</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC184
  <tr><td>SwapLanes</td><td>3 phases, general lane assignment for any permutation</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC185
  <tr><td>no equivalent</td><td>4 phases, the grid, for simultaneous reversals on both axes</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC186
  <tr><td>ReversedForwardPlan</td><td>not needed, the construction is symmetric</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC187
  <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 UTC188
  <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 UTC189
  <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 UTC190
  <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 UTC191
  <tr><td>Cross-output moves skip animation</td><td>planned in global coordinates, clipped per output</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC192
</table>
05668b0d4753atagen2026-10-16 05:50 UTC193
cd7085e54c88atagen2026-10-16 06:58 UTC194
<h2>16. Interspersing tags (open, changes the layout model)</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC195
<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 UTC196
<table>
05668b0d4753atagen2026-10-16 05:50 UTC197
  <tr><th>option</th><th>meaning</th><th>verdict</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC198
  <tr><td>Forbid it</td><td>tags stay in their rows</td><td>limiting</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC199
  <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 UTC200
  <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 UTC201
  <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 UTC202
  <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 UTC203
</table>
cd7085e54c88atagen2026-10-16 06:58 UTC204
<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 UTC205
<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 UTC206
<div class="demo" id="d8"></div>
05668b0d4753atagen2026-10-16 05:50 UTC207
cd7085e54c88atagen2026-10-16 06:58 UTC208
<h2>17. Decisions</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC209
<h3>Settled</h3>
cd7085e54c88atagen2026-10-16 06:58 UTC210
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC211
  <li>Orthogonal is the default and the flagship look. Direct is a toggle.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC212
  <li>Window moves and block moves both exist as commands. Tag reordering moves rigid blocks.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC213
  <li>One curve per motion group, bounded by phase time rather than amount of motion.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC214
  <li>Floating to tiled is phased, travel one axis per phase, then land.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC215
  <li>The planner must never need a fallback. Pinch and lerp stay as safety nets to try.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC216
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC217
<h3>Open</h3>
cd7085e54c88atagen2026-10-16 06:58 UTC218
<div class="q"><strong>Seed shape.</strong> Line on the old boundary, point on the old boundary, or target centre (section 4). Line is always one phase. Point looks more like your "grow from centre" but costs a phase in orthogonal style.</div>
cd7085e54c88atagen2026-10-16 06:58 UTC219
<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 UTC220
<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 UTC221
<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 UTC222
</main>
05668b0d4753atagen2026-10-16 05:50 UTC223
05668b0d4753atagen2026-10-16 05:50 UTC224
<script id="planner">
05668b0d4753atagen2026-10-16 05:50 UTC225
const G = 10;
05668b0d4753atagen2026-10-16 05:50 UTC226
const EPS = 0.5;
05668b0d4753atagen2026-10-16 05:50 UTC227
let PHASE_MS = 450;
cd7085e54c88atagen2026-10-16 06:58 UTC228
let STYLE = 'orthogonal';
05668b0d4753atagen2026-10-16 05:50 UTC229
05668b0d4753atagen2026-10-16 05:50 UTC230
const R = (x, y, w, h) => ({ x, y, w, h });
05668b0d4753atagen2026-10-16 05:50 UTC231
const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; };
05668b0d4753atagen2026-10-16 05:50 UTC232
const eq = (a, b) => Math.abs(a - b) < 0.05;
05668b0d4753atagen2026-10-16 05:50 UTC233
const lerp = (a, b, s) => a + (b - a) * s;
05668b0d4753atagen2026-10-16 05:50 UTC234
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 UTC235
05668b0d4753atagen2026-10-16 05:50 UTC236
function splitX(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC237
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC238
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC239
  const avail = rect.w - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC240
  let x = rect.x;
05668b0d4753atagen2026-10-16 05:50 UTC241
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC242
  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 UTC243
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC244
}
05668b0d4753atagen2026-10-16 05:50 UTC245
function splitY(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC246
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC247
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC248
  const avail = rect.h - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC249
  let y = rect.y;
05668b0d4753atagen2026-10-16 05:50 UTC250
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC251
  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 UTC252
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC253
}
05668b0d4753atagen2026-10-16 05:50 UTC254
05668b0d4753atagen2026-10-16 05:50 UTC255
function relBits(a, b) {
05668b0d4753atagen2026-10-16 05:50 UTC256
  let s = 0;
05668b0d4753atagen2026-10-16 05:50 UTC257
  if (a.x + a.w <= b.x + EPS) s |= 1;
05668b0d4753atagen2026-10-16 05:50 UTC258
  if (b.x + b.w <= a.x + EPS) s |= 2;
05668b0d4753atagen2026-10-16 05:50 UTC259
  if (a.y + a.h <= b.y + EPS) s |= 4;
05668b0d4753atagen2026-10-16 05:50 UTC260
  if (b.y + b.h <= a.y + EPS) s |= 8;
05668b0d4753atagen2026-10-16 05:50 UTC261
  return s;
05668b0d4753atagen2026-10-16 05:50 UTC262
}
05668b0d4753atagen2026-10-16 05:50 UTC263
function phaseOk(K1, K2, list) {
05668b0d4753atagen2026-10-16 05:50 UTC264
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC265
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC266
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC267
      if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false;
05668b0d4753atagen2026-10-16 05:50 UTC268
    }
05668b0d4753atagen2026-10-16 05:50 UTC269
  return true;
05668b0d4753atagen2026-10-16 05:50 UTC270
}
05668b0d4753atagen2026-10-16 05:50 UTC271
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 UTC272
function pureOk(keys) {
cd7085e54c88atagen2026-10-16 06:58 UTC273
  if (STYLE !== 'orthogonal') return true;
cd7085e54c88atagen2026-10-16 06:58 UTC274
  for (let k = 0; k + 1 < keys.length; k++)
cd7085e54c88atagen2026-10-16 06:58 UTC275
    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 UTC276
  return true;
cd7085e54c88atagen2026-10-16 06:58 UTC277
}
05668b0d4753atagen2026-10-16 05:50 UTC278
440b67836dd5atagen2026-10-16 07:57 UTC279
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 UTC280
05668b0d4753atagen2026-10-16 05:50 UTC281
function plan(from, to, opts = {}) {
440b67836dd5atagen2026-10-16 07:57 UTC282
  if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [PHASE_MS] };
440b67836dd5atagen2026-10-16 07:57 UTC283
  if (signature(from) <= signature(to)) return planOneWay(from, to);
440b67836dd5atagen2026-10-16 07:57 UTC284
  const p = planOneWay(to, from);
440b67836dd5atagen2026-10-16 07:57 UTC285
  return { kind: p.kind, keys: [...p.keys].reverse(), durs: [...p.durs].reverse() };
440b67836dd5atagen2026-10-16 07:57 UTC286
}
440b67836dd5atagen2026-10-16 07:57 UTC287
440b67836dd5atagen2026-10-16 07:57 UTC288
function planOneWay(from, to) {
05668b0d4753atagen2026-10-16 05:50 UTC289
  const list = Object.keys(to);
05668b0d4753atagen2026-10-16 05:50 UTC290
  const T = PHASE_MS;
05668b0d4753atagen2026-10-16 05:50 UTC291
  const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; });
05668b0d4753atagen2026-10-16 05:50 UTC292
  if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] };
05668b0d4753atagen2026-10-16 05:50 UTC293
  const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); });
05668b0d4753atagen2026-10-16 05:50 UTC294
  if (phaseOk(from, to, list) && (STYLE === 'direct' || pure))
05668b0d4753atagen2026-10-16 05:50 UTC295
    return { kind: '1 phase', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC296
  const two = axisOrder(from, to, list, movers);
05668b0d4753atagen2026-10-16 05:50 UTC297
  if (two) return two;
05668b0d4753atagen2026-10-16 05:50 UTC298
  for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; }
cd7085e54c88atagen2026-10-16 06:58 UTC299
  const g = grid(from, to, list, movers);
cd7085e54c88atagen2026-10-16 06:58 UTC300
  if (g) return g;
05668b0d4753atagen2026-10-16 05:50 UTC301
  return pinch(from, to, movers);
05668b0d4753atagen2026-10-16 05:50 UTC302
}
05668b0d4753atagen2026-10-16 05:50 UTC303
cd7085e54c88atagen2026-10-16 06:58 UTC304
function conflictSet(from, to, list) {
cd7085e54c88atagen2026-10-16 06:58 UTC305
  const out = new Set();
cd7085e54c88atagen2026-10-16 06:58 UTC306
  for (let i = 0; i < list.length; i++)
cd7085e54c88atagen2026-10-16 06:58 UTC307
    for (let j = i + 1; j < list.length; j++) {
cd7085e54c88atagen2026-10-16 06:58 UTC308
      const a = list[i], b = list[j];
cd7085e54c88atagen2026-10-16 06:58 UTC309
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { out.add(a); out.add(b); }
cd7085e54c88atagen2026-10-16 06:58 UTC310
    }
cd7085e54c88atagen2026-10-16 06:58 UTC311
  return [...out];
cd7085e54c88atagen2026-10-16 06:58 UTC312
}
cd7085e54c88atagen2026-10-16 06:58 UTC313
cd7085e54c88atagen2026-10-16 06:58 UTC314
function grid(from, to, list, movers) {
cd7085e54c88atagen2026-10-16 06:58 UTC315
  const cx = r => r.x + r.w / 2, cy = r => r.y + r.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC316
  for (const set of [conflictSet(from, to, list), movers, list]) {
cd7085e54c88atagen2026-10-16 06:58 UTC317
    const n = set.length;
cd7085e54c88atagen2026-10-16 06:58 UTC318
    if (n < 2) continue;
cd7085e54c88atagen2026-10-16 06:58 UTC319
    let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
cd7085e54c88atagen2026-10-16 06:58 UTC320
    for (const id of set) for (const r of [from[id], to[id]]) {
cd7085e54c88atagen2026-10-16 06:58 UTC321
      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 UTC322
    }
cd7085e54c88atagen2026-10-16 06:58 UTC323
    const sh = (y1 - y0 - (n - 1) * G) / n, sw = (x1 - x0 - (n - 1) * G) / n;
cd7085e54c88atagen2026-10-16 06:58 UTC324
    const rowOf = {}, colOf = {};
cd7085e54c88atagen2026-10-16 06:58 UTC325
    [...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 UTC326
    [...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 UTC327
    const inSet = new Set(set);
cd7085e54c88atagen2026-10-16 06:58 UTC328
    for (const phase of [3, 2, 4, 1]) {
cd7085e54c88atagen2026-10-16 06:58 UTC329
      const K = [{}, {}, {}];
cd7085e54c88atagen2026-10-16 06:58 UTC330
      for (const id of list) {
cd7085e54c88atagen2026-10-16 06:58 UTC331
        const f = from[id], t = to[id];
cd7085e54c88atagen2026-10-16 06:58 UTC332
        if (inSet.has(id)) {
cd7085e54c88atagen2026-10-16 06:58 UTC333
          K[0][id] = R(f.x, rowOf[id], f.w, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC334
          K[1][id] = R(colOf[id], rowOf[id], sw, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC335
          K[2][id] = R(colOf[id], t.y, sw, t.h);
cd7085e54c88atagen2026-10-16 06:58 UTC336
        } else for (let k = 0; k < 3; k++) K[k][id] = k + 1 >= phase ? t : f;
cd7085e54c88atagen2026-10-16 06:58 UTC337
      }
cd7085e54c88atagen2026-10-16 06:58 UTC338
      const keys = [from, ...K, to];
cd7085e54c88atagen2026-10-16 06:58 UTC339
      let ok = true;
cd7085e54c88atagen2026-10-16 06:58 UTC340
      for (let k = 0; k < 4 && ok; k++) ok = phaseOk(keys[k], keys[k + 1], list);
cd7085e54c88atagen2026-10-16 06:58 UTC341
      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 UTC342
    }
cd7085e54c88atagen2026-10-16 06:58 UTC343
  }
cd7085e54c88atagen2026-10-16 06:58 UTC344
  return null;
cd7085e54c88atagen2026-10-16 06:58 UTC345
}
cd7085e54c88atagen2026-10-16 06:58 UTC346
05668b0d4753atagen2026-10-16 05:50 UTC347
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 UTC348
440b67836dd5atagen2026-10-16 07:57 UTC349
function adjBits(a, b) {
440b67836dd5atagen2026-10-16 07:57 UTC350
  const ovX = a.x < b.x + b.w + EPS && b.x < a.x + a.w + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC351
  const ovY = a.y < b.y + b.h + EPS && b.y < a.y + a.h + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC352
  let s = 0;
440b67836dd5atagen2026-10-16 07:57 UTC353
  if (ovY && Math.abs(b.x - (a.x + a.w)) <= G + EPS) s |= 1;
440b67836dd5atagen2026-10-16 07:57 UTC354
  if (ovY && Math.abs(a.x - (b.x + b.w)) <= G + EPS) s |= 2;
440b67836dd5atagen2026-10-16 07:57 UTC355
  if (ovX && Math.abs(b.y - (a.y + a.h)) <= G + EPS) s |= 4;
440b67836dd5atagen2026-10-16 07:57 UTC356
  if (ovX && Math.abs(a.y - (b.y + b.h)) <= G + EPS) s |= 8;
440b67836dd5atagen2026-10-16 07:57 UTC357
  return s;
440b67836dd5atagen2026-10-16 07:57 UTC358
}
440b67836dd5atagen2026-10-16 07:57 UTC359
function coupledEdges(from, to, list) {
440b67836dd5atagen2026-10-16 07:57 UTC360
  const out = [];
440b67836dd5atagen2026-10-16 07:57 UTC361
  for (let i = 0; i < list.length; i++)
440b67836dd5atagen2026-10-16 07:57 UTC362
    for (let j = i + 1; j < list.length; j++) {
440b67836dd5atagen2026-10-16 07:57 UTC363
      const a = list[i], b = list[j], s = adjBits(from[a], from[b]) & adjBits(to[a], to[b]);
440b67836dd5atagen2026-10-16 07:57 UTC364
      if (s) out.push([a, b, s]);
440b67836dd5atagen2026-10-16 07:57 UTC365
    }
440b67836dd5atagen2026-10-16 07:57 UTC366
  return out;
440b67836dd5atagen2026-10-16 07:57 UTC367
}
440b67836dd5atagen2026-10-16 07:57 UTC368
function holes(K, coupled) {
440b67836dd5atagen2026-10-16 07:57 UTC369
  let n = 0;
440b67836dd5atagen2026-10-16 07:57 UTC370
  for (const [a, b, s] of coupled) if ((adjBits(K[a], K[b]) & s) !== s) n++;
440b67836dd5atagen2026-10-16 07:57 UTC371
  return n;
440b67836dd5atagen2026-10-16 07:57 UTC372
}
440b67836dd5atagen2026-10-16 07:57 UTC373
05668b0d4753atagen2026-10-16 05:50 UTC374
function axisOrder(from, to, list, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC375
  const n = movers.length;
05668b0d4753atagen2026-10-16 05:50 UTC376
  if (n > 14) return null;
440b67836dd5atagen2026-10-16 07:57 UTC377
  const coupled = coupledEdges(from, to, list);
05668b0d4753atagen2026-10-16 05:50 UTC378
  let best = null, bestScore = Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC379
  for (let mask = 0; mask < (1 << n); mask++) {
05668b0d4753atagen2026-10-16 05:50 UTC380
    const K1 = { ...from };
440b67836dd5atagen2026-10-16 07:57 UTC381
    let late = 0;
05668b0d4753atagen2026-10-16 05:50 UTC382
    movers.forEach((id, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC383
      const xf = !!(mask & (1 << i));
05668b0d4753atagen2026-10-16 05:50 UTC384
      const c = chg(from[id], to[id]);
05668b0d4753atagen2026-10-16 05:50 UTC385
      K1[id] = mixed(from[id], to[id], xf);
440b67836dd5atagen2026-10-16 07:57 UTC386
      if (c.x && !c.y && !xf) late++;
440b67836dd5atagen2026-10-16 07:57 UTC387
      if (c.y && !c.x && xf) late++;
05668b0d4753atagen2026-10-16 05:50 UTC388
    });
440b67836dd5atagen2026-10-16 07:57 UTC389
    if (late >= bestScore) continue;
440b67836dd5atagen2026-10-16 07:57 UTC390
    if (!phaseOk(from, K1, list) || !phaseOk(K1, to, list)) continue;
440b67836dd5atagen2026-10-16 07:57 UTC391
    const score = holes(K1, coupled) * 1000 + late;
440b67836dd5atagen2026-10-16 07:57 UTC392
    if (score < bestScore) { best = K1; bestScore = score; if (score === 0) break; }
05668b0d4753atagen2026-10-16 05:50 UTC393
  }
440b67836dd5atagen2026-10-16 07:57 UTC394
  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 UTC395
}
05668b0d4753atagen2026-10-16 05:50 UTC396
05668b0d4753atagen2026-10-16 05:50 UTC397
function lanes(from, to, list, movers, ax) {
05668b0d4753atagen2026-10-16 05:50 UTC398
  const size = ax === 'y' ? 'h' : 'w';
05668b0d4753atagen2026-10-16 05:50 UTC399
  const bitsT = ax === 'y' ? 3 : 12;
05668b0d4753atagen2026-10-16 05:50 UTC400
  const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2;
05668b0d4753atagen2026-10-16 05:50 UTC401
  const conflict = new Set();
05668b0d4753atagen2026-10-16 05:50 UTC402
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC403
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC404
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC405
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); }
05668b0d4753atagen2026-10-16 05:50 UTC406
    }
05668b0d4753atagen2026-10-16 05:50 UTC407
  const sets = [[...conflict], movers, list];
05668b0d4753atagen2026-10-16 05:50 UTC408
  for (const set of sets) {
05668b0d4753atagen2026-10-16 05:50 UTC409
    const n = set.length;
05668b0d4753atagen2026-10-16 05:50 UTC410
    if (n < 2) continue;
05668b0d4753atagen2026-10-16 05:50 UTC411
    let lo = Infinity, hi = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC412
    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 UTC413
    const diff = [], lt = [];
05668b0d4753atagen2026-10-16 05:50 UTC414
    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 UTC415
    for (let i = 0; i < n; i++)
05668b0d4753atagen2026-10-16 05:50 UTC416
      for (let j = 0; j < n; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC417
        if (i === j) continue;
05668b0d4753atagen2026-10-16 05:50 UTC418
        const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]);
05668b0d4753atagen2026-10-16 05:50 UTC419
        if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC420
        for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC421
      }
05668b0d4753atagen2026-10-16 05:50 UTC422
    for (let k = 2; k <= n; k++) {
05668b0d4753atagen2026-10-16 05:50 UTC423
      const s = (hi - lo - (k - 1) * G) / k;
05668b0d4753atagen2026-10-16 05:50 UTC424
      if (s < 6) break;
05668b0d4753atagen2026-10-16 05:50 UTC425
      const lane = new Array(n).fill(-1);
05668b0d4753atagen2026-10-16 05:50 UTC426
      const assign = i => {
05668b0d4753atagen2026-10-16 05:50 UTC427
        if (i === n) return true;
05668b0d4753atagen2026-10-16 05:50 UTC428
        for (let l = 0; l < k; l++) {
05668b0d4753atagen2026-10-16 05:50 UTC429
          let ok = true;
05668b0d4753atagen2026-10-16 05:50 UTC430
          for (let j = 0; j < i && ok; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC431
            if (diff[i][j] && lane[j] === l) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC432
            if (lt[i][j] && !(l < lane[j])) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC433
            if (lt[j][i] && !(lane[j] < l)) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC434
          }
05668b0d4753atagen2026-10-16 05:50 UTC435
          if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; }
05668b0d4753atagen2026-10-16 05:50 UTC436
        }
05668b0d4753atagen2026-10-16 05:50 UTC437
        return false;
05668b0d4753atagen2026-10-16 05:50 UTC438
      };
05668b0d4753atagen2026-10-16 05:50 UTC439
      if (!assign(0)) continue;
05668b0d4753atagen2026-10-16 05:50 UTC440
      const strip = l => lo + l * (s + G);
05668b0d4753atagen2026-10-16 05:50 UTC441
      const inSet = new Set(set);
05668b0d4753atagen2026-10-16 05:50 UTC442
      for (const phase of [2, 1, 3]) {
05668b0d4753atagen2026-10-16 05:50 UTC443
        const K1 = {}, K2 = {};
05668b0d4753atagen2026-10-16 05:50 UTC444
        for (const id of list) {
05668b0d4753atagen2026-10-16 05:50 UTC445
          const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC446
          if (inSet.has(id)) {
05668b0d4753atagen2026-10-16 05:50 UTC447
            const l = lane[set.indexOf(id)];
05668b0d4753atagen2026-10-16 05:50 UTC448
            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 UTC449
            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 UTC450
          } else if (phase === 1) { K1[id] = t; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC451
          else if (phase === 2) { K1[id] = f; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC452
          else { K1[id] = f; K2[id] = f; }
05668b0d4753atagen2026-10-16 05:50 UTC453
        }
cd7085e54c88atagen2026-10-16 06:58 UTC454
        if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list) && pureOk([from, K1, K2, to]))
05668b0d4753atagen2026-10-16 05:50 UTC455
          return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC456
      }
05668b0d4753atagen2026-10-16 05:50 UTC457
    }
05668b0d4753atagen2026-10-16 05:50 UTC458
  }
05668b0d4753atagen2026-10-16 05:50 UTC459
  return null;
05668b0d4753atagen2026-10-16 05:50 UTC460
}
05668b0d4753atagen2026-10-16 05:50 UTC461
05668b0d4753atagen2026-10-16 05:50 UTC462
function pinch(from, to, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC463
  const K1 = { ...from }, K2 = { ...to };
05668b0d4753atagen2026-10-16 05:50 UTC464
  for (const id of movers) {
05668b0d4753atagen2026-10-16 05:50 UTC465
    const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC466
    K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC467
    K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC468
  }
05668b0d4753atagen2026-10-16 05:50 UTC469
  return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC470
}
05668b0d4753atagen2026-10-16 05:50 UTC471
05668b0d4753atagen2026-10-16 05:50 UTC472
function bbox(rects) {
05668b0d4753atagen2026-10-16 05:50 UTC473
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC474
  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 UTC475
  return R(x0, y0, x1 - x0, y1 - y0);
05668b0d4753atagen2026-10-16 05:50 UTC476
}
05668b0d4753atagen2026-10-16 05:50 UTC477
function planBlocks(from, to, groups) {
05668b0d4753atagen2026-10-16 05:50 UTC478
  const bf = {}, bt = {};
05668b0d4753atagen2026-10-16 05:50 UTC479
  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 UTC480
  const bp = plan(bf, bt);
05668b0d4753atagen2026-10-16 05:50 UTC481
  const last = bp.keys.length - 1;
05668b0d4753atagen2026-10-16 05:50 UTC482
  const keys = bp.keys.map((K, ki) => {
05668b0d4753atagen2026-10-16 05:50 UTC483
    if (ki === 0) return from;
05668b0d4753atagen2026-10-16 05:50 UTC484
    if (ki === last) return to;
05668b0d4753atagen2026-10-16 05:50 UTC485
    const L = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC486
    for (const b in groups) for (const id of groups[b]) {
05668b0d4753atagen2026-10-16 05:50 UTC487
      const r = from[id], box = bf[b], k = K[b];
05668b0d4753atagen2026-10-16 05:50 UTC488
      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 UTC489
      const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC490
      L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h);
05668b0d4753atagen2026-10-16 05:50 UTC491
    }
05668b0d4753atagen2026-10-16 05:50 UTC492
    return L;
05668b0d4753atagen2026-10-16 05:50 UTC493
  });
05668b0d4753atagen2026-10-16 05:50 UTC494
  return { kind: bp.kind + ' on blocks', keys, durs: bp.durs };
05668b0d4753atagen2026-10-16 05:50 UTC495
}
05668b0d4753atagen2026-10-16 05:50 UTC496
05668b0d4753atagen2026-10-16 05:50 UTC497
function bezier(x1, y1, x2, y2) {
05668b0d4753atagen2026-10-16 05:50 UTC498
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
05668b0d4753atagen2026-10-16 05:50 UTC499
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
05668b0d4753atagen2026-10-16 05:50 UTC500
  const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u;
05668b0d4753atagen2026-10-16 05:50 UTC501
  const dX = u => (3 * ax * u + 2 * bx) * u + cx;
05668b0d4753atagen2026-10-16 05:50 UTC502
  return t => {
05668b0d4753atagen2026-10-16 05:50 UTC503
    if (t <= 0) return 0;
05668b0d4753atagen2026-10-16 05:50 UTC504
    if (t >= 1) return 1;
05668b0d4753atagen2026-10-16 05:50 UTC505
    let u = t;
05668b0d4753atagen2026-10-16 05:50 UTC506
    for (let i = 0; i < 8; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC507
      const x = X(u) - t;
05668b0d4753atagen2026-10-16 05:50 UTC508
      if (Math.abs(x) < 1e-6) return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC509
      const d = dX(u);
05668b0d4753atagen2026-10-16 05:50 UTC510
      if (Math.abs(d) < 1e-6) break;
05668b0d4753atagen2026-10-16 05:50 UTC511
      u -= x / d;
05668b0d4753atagen2026-10-16 05:50 UTC512
    }
05668b0d4753atagen2026-10-16 05:50 UTC513
    let lo = 0, hi = 1; u = t;
05668b0d4753atagen2026-10-16 05:50 UTC514
    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 UTC515
    return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC516
  };
05668b0d4753atagen2026-10-16 05:50 UTC517
}
05668b0d4753atagen2026-10-16 05:50 UTC518
const BASE = bezier(0.45, 0, 0.2, 1);
05668b0d4753atagen2026-10-16 05:50 UTC519
const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h];
05668b0d4753atagen2026-10-16 05:50 UTC520
</script>
05668b0d4753atagen2026-10-16 05:50 UTC521
05668b0d4753atagen2026-10-16 05:50 UTC522
<script>
05668b0d4753atagen2026-10-16 05:50 UTC523
const PAL = { A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' };
05668b0d4753atagen2026-10-16 05:50 UTC524
const demos = [];
05668b0d4753atagen2026-10-16 05:50 UTC525
05668b0d4753atagen2026-10-16 05:50 UTC526
class Demo {
05668b0d4753atagen2026-10-16 05:50 UTC527
  constructor(id, W, H, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC528
    this.W = W; this.H = H;
05668b0d4753atagen2026-10-16 05:50 UTC529
    this.outputs = opts.outputs || [R(0, 0, W, H)];
cd7085e54c88atagen2026-10-16 06:58 UTC530
    this.floating = new Set(opts.floating || []);
05668b0d4753atagen2026-10-16 05:50 UTC531
    const el = document.getElementById(id);
05668b0d4753atagen2026-10-16 05:50 UTC532
    this.canvas = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC533
    const dpr = window.devicePixelRatio || 1;
05668b0d4753atagen2026-10-16 05:50 UTC534
    this.canvas.width = W * dpr; this.canvas.height = H * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC535
    this.canvas.style.maxWidth = W + 'px';
05668b0d4753atagen2026-10-16 05:50 UTC536
    this.ctx = this.canvas.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC537
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC538
    el.appendChild(this.canvas);
05668b0d4753atagen2026-10-16 05:50 UTC539
    if (opts.graph) {
05668b0d4753atagen2026-10-16 05:50 UTC540
      this.graph = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC541
      this.graph.width = W * dpr; this.graph.height = 90 * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC542
      this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px';
05668b0d4753atagen2026-10-16 05:50 UTC543
      this.gctx = this.graph.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC544
      this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC545
      el.appendChild(this.graph);
05668b0d4753atagen2026-10-16 05:50 UTC546
      this.hist = [];
05668b0d4753atagen2026-10-16 05:50 UTC547
    }
05668b0d4753atagen2026-10-16 05:50 UTC548
    this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls);
05668b0d4753atagen2026-10-16 05:50 UTC549
    this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status);
05668b0d4753atagen2026-10-16 05:50 UTC550
    this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle';
05668b0d4753atagen2026-10-16 05:50 UTC551
    demos.push(this);
05668b0d4753atagen2026-10-16 05:50 UTC552
  }
05668b0d4753atagen2026-10-16 05:50 UTC553
  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 UTC554
  check(label, init, fn) {
05668b0d4753atagen2026-10-16 05:50 UTC555
    const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init;
05668b0d4753atagen2026-10-16 05:50 UTC556
    c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c;
05668b0d4753atagen2026-10-16 05:50 UTC557
  }
05668b0d4753atagen2026-10-16 05:50 UTC558
  set(L) { this.layout = clone(L); this.anim = null; }
05668b0d4753atagen2026-10-16 05:50 UTC559
  go(target, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC560
    const now = performance.now();
05668b0d4753atagen2026-10-16 05:50 UTC561
    const cur = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC562
    const from = {};
05668b0d4753atagen2026-10-16 05:50 UTC563
    for (const id in target) from[id] = cur[id] || target[id];
cd7085e54c88atagen2026-10-16 06:58 UTC564
    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 UTC565
    const curves = p.durs.map(() => BASE);
05668b0d4753atagen2026-10-16 05:50 UTC566
    const interrupted = !!this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC567
    let matched = false;
05668b0d4753atagen2026-10-16 05:50 UTC568
    if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) {
05668b0d4753atagen2026-10-16 05:50 UTC569
      const K1 = p.keys[1];
05668b0d4753atagen2026-10-16 05:50 UTC570
      let best = null, bs = 0;
05668b0d4753atagen2026-10-16 05:50 UTC571
      for (const id in K1) {
05668b0d4753atagen2026-10-16 05:50 UTC572
        const v = this.vel[id]; if (!v) continue;
05668b0d4753atagen2026-10-16 05:50 UTC573
        const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]);
05668b0d4753atagen2026-10-16 05:50 UTC574
        const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue;
05668b0d4753atagen2026-10-16 05:50 UTC575
        const sp = Math.hypot(...v);
05668b0d4753atagen2026-10-16 05:50 UTC576
        if (sp > bs) { bs = sp; best = { v, D, dd }; }
05668b0d4753atagen2026-10-16 05:50 UTC577
      }
05668b0d4753atagen2026-10-16 05:50 UTC578
      if (best) {
05668b0d4753atagen2026-10-16 05:50 UTC579
        const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd;
05668b0d4753atagen2026-10-16 05:50 UTC580
        const slope = s * p.durs[0];
05668b0d4753atagen2026-10-16 05:50 UTC581
        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 UTC582
      }
05668b0d4753atagen2026-10-16 05:50 UTC583
    }
05668b0d4753atagen2026-10-16 05:50 UTC584
    this.anim = { keys: p.keys, durs: p.durs, curves, start: now };
05668b0d4753atagen2026-10-16 05:50 UTC585
    this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : '');
05668b0d4753atagen2026-10-16 05:50 UTC586
    this.overlapSeen = false;
05668b0d4753atagen2026-10-16 05:50 UTC587
  }
05668b0d4753atagen2026-10-16 05:50 UTC588
  sampleAt(t) {
05668b0d4753atagen2026-10-16 05:50 UTC589
    if (!this.anim) return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC590
    let e = t - this.anim.start;
05668b0d4753atagen2026-10-16 05:50 UTC591
    const { keys, durs, curves } = this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC592
    for (let i = 0; i < durs.length; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC593
      const d = durs[i];
05668b0d4753atagen2026-10-16 05:50 UTC594
      if (e < d) {
05668b0d4753atagen2026-10-16 05:50 UTC595
        this.phase = i;
05668b0d4753atagen2026-10-16 05:50 UTC596
        const s = curves[i](e / d);
05668b0d4753atagen2026-10-16 05:50 UTC597
        const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC598
        for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s);
05668b0d4753atagen2026-10-16 05:50 UTC599
        return L;
05668b0d4753atagen2026-10-16 05:50 UTC600
      }
05668b0d4753atagen2026-10-16 05:50 UTC601
      e -= d;
05668b0d4753atagen2026-10-16 05:50 UTC602
    }
05668b0d4753atagen2026-10-16 05:50 UTC603
    this.layout = clone(keys[keys.length - 1]);
05668b0d4753atagen2026-10-16 05:50 UTC604
    this.anim = null;
05668b0d4753atagen2026-10-16 05:50 UTC605
    return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC606
  }
05668b0d4753atagen2026-10-16 05:50 UTC607
  frame(now) {
05668b0d4753atagen2026-10-16 05:50 UTC608
    const L = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC609
    if (this.prev) {
05668b0d4753atagen2026-10-16 05:50 UTC610
      const dt = now - this.prev.t;
05668b0d4753atagen2026-10-16 05:50 UTC611
      if (dt > 0) for (const id in L) if (this.prev.L[id]) {
05668b0d4753atagen2026-10-16 05:50 UTC612
        const a = edges(this.prev.L[id]), b = edges(L[id]);
05668b0d4753atagen2026-10-16 05:50 UTC613
        this.vel[id] = b.map((v, i) => (v - a[i]) / dt);
05668b0d4753atagen2026-10-16 05:50 UTC614
      }
05668b0d4753atagen2026-10-16 05:50 UTC615
    }
05668b0d4753atagen2026-10-16 05:50 UTC616
    this.prev = { t: now, L };
05668b0d4753atagen2026-10-16 05:50 UTC617
    this.draw(L);
05668b0d4753atagen2026-10-16 05:50 UTC618
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC619
    if (overlaps.length) this.overlapSeen = true;
05668b0d4753atagen2026-10-16 05:50 UTC620
    const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : '';
05668b0d4753atagen2026-10-16 05:50 UTC621
    this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : '');
05668b0d4753atagen2026-10-16 05:50 UTC622
    if (this.graph) this.drawGraph(now);
05668b0d4753atagen2026-10-16 05:50 UTC623
  }
05668b0d4753atagen2026-10-16 05:50 UTC624
  findOverlaps(L) {
cd7085e54c88atagen2026-10-16 06:58 UTC625
    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 UTC626
    const out = [];
05668b0d4753atagen2026-10-16 05:50 UTC627
    for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC628
      const a = L[ids[i]], b = L[ids[j]];
05668b0d4753atagen2026-10-16 05:50 UTC629
      const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
05668b0d4753atagen2026-10-16 05:50 UTC630
      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 UTC631
      if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0));
05668b0d4753atagen2026-10-16 05:50 UTC632
    }
05668b0d4753atagen2026-10-16 05:50 UTC633
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC634
  }
05668b0d4753atagen2026-10-16 05:50 UTC635
  draw(L) {
05668b0d4753atagen2026-10-16 05:50 UTC636
    const c = this.ctx;
05668b0d4753atagen2026-10-16 05:50 UTC637
    c.clearRect(0, 0, this.W, this.H);
05668b0d4753atagen2026-10-16 05:50 UTC638
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC639
    for (const o of this.outputs) {
05668b0d4753atagen2026-10-16 05:50 UTC640
      c.save();
05668b0d4753atagen2026-10-16 05:50 UTC641
      c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h);
05668b0d4753atagen2026-10-16 05:50 UTC642
      c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip();
cd7085e54c88atagen2026-10-16 06:58 UTC643
      for (const id of Object.keys(L).sort((a, b) => this.floating.has(a) - this.floating.has(b))) {
05668b0d4753atagen2026-10-16 05:50 UTC644
        const r = L[id];
05668b0d4753atagen2026-10-16 05:50 UTC645
        if (r.w < 0.5 || r.h < 0.5) continue;
05668b0d4753atagen2026-10-16 05:50 UTC646
        const rad = Math.min(8, r.w / 2, r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC647
        c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad);
05668b0d4753atagen2026-10-16 05:50 UTC648
        c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill();
05668b0d4753atagen2026-10-16 05:50 UTC649
        c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC650
        if (r.w > 22 && r.h > 22) {
05668b0d4753atagen2026-10-16 05:50 UTC651
          c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle';
05668b0d4753atagen2026-10-16 05:50 UTC652
          c.fillText(id, r.x + r.w / 2, r.y + r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC653
        }
05668b0d4753atagen2026-10-16 05:50 UTC654
      }
05668b0d4753atagen2026-10-16 05:50 UTC655
      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 UTC656
      c.restore();
05668b0d4753atagen2026-10-16 05:50 UTC657
    }
05668b0d4753atagen2026-10-16 05:50 UTC658
  }
05668b0d4753atagen2026-10-16 05:50 UTC659
  drawGraph(now) {
05668b0d4753atagen2026-10-16 05:50 UTC660
    const v = this.vel.A;
05668b0d4753atagen2026-10-16 05:50 UTC661
    const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0;
05668b0d4753atagen2026-10-16 05:50 UTC662
    this.hist.push([now, sp]);
05668b0d4753atagen2026-10-16 05:50 UTC663
    while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift();
05668b0d4753atagen2026-10-16 05:50 UTC664
    const c = this.gctx, W = this.W, H = 90;
05668b0d4753atagen2026-10-16 05:50 UTC665
    c.clearRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC666
    c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC667
    const max = Math.max(400, ...this.hist.map(h => h[1]));
05668b0d4753atagen2026-10-16 05:50 UTC668
    c.beginPath();
05668b0d4753atagen2026-10-16 05:50 UTC669
    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 UTC670
    c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC671
    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 UTC672
  }
05668b0d4753atagen2026-10-16 05:50 UTC673
}
05668b0d4753atagen2026-10-16 05:50 UTC674
05668b0d4753atagen2026-10-16 05:50 UTC675
const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G);
05668b0d4753atagen2026-10-16 05:50 UTC676
05668b0d4753atagen2026-10-16 05:50 UTC677
{ // d1 swap
05668b0d4753atagen2026-10-16 05:50 UTC678
  const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC679
  let swapped = false;
05668b0d4753atagen2026-10-16 05:50 UTC680
  const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']);
05668b0d4753atagen2026-10-16 05:50 UTC681
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC682
  d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC683
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC684
}
05668b0d4753atagen2026-10-16 05:50 UTC685
05668b0d4753atagen2026-10-16 05:50 UTC686
{ // d2 one phase
05668b0d4753atagen2026-10-16 05:50 UTC687
  const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC688
  const s = { wide: false, d: false, e: false };
cd7085e54c88atagen2026-10-16 06:58 UTC689
  const modes = ['line on old boundary', 'point on old boundary', 'target centre'];
cd7085e54c88atagen2026-10-16 06:58 UTC690
  let mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC691
  const ratios = ids => ids.map(id => (id === 'A' && s.wide) ? 2 : 1);
05668b0d4753atagen2026-10-16 05:50 UTC692
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC693
    const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C'];
cd7085e54c88atagen2026-10-16 06:58 UTC694
    const out = splitX(I, ids, ratios(ids));
cd7085e54c88atagen2026-10-16 06:58 UTC695
    if (!s.d) {
cd7085e54c88atagen2026-10-16 06:58 UTC696
      const bx = (out.B.x + out.B.w + out.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC697
      const T = splitX(I, ['A', 'B', 'D', 'C'], ratios(['A', 'B', 'D', 'C'])).D;
cd7085e54c88atagen2026-10-16 06:58 UTC698
      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 UTC699
    }
05668b0d4753atagen2026-10-16 05:50 UTC700
    const b = out.B;
05668b0d4753atagen2026-10-16 05:50 UTC701
    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 UTC702
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC703
  };
05668b0d4753atagen2026-10-16 05:50 UTC704
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC705
  d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC706
  d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC707
  d.button('Split B with E', () => { s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC708
  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 UTC709
  const mb = d.button('seed: ' + modes[0], () => {
cd7085e54c88atagen2026-10-16 06:58 UTC710
    mode = (mode + 1) % modes.length; mb.textContent = 'seed: ' + modes[mode];
cd7085e54c88atagen2026-10-16 06:58 UTC711
    if (!s.d && !d.anim) d.set(L());
cd7085e54c88atagen2026-10-16 06:58 UTC712
  });
05668b0d4753atagen2026-10-16 05:50 UTC713
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC714
}
05668b0d4753atagen2026-10-16 05:50 UTC715
function tagLayout(I, view2, order) {
05668b0d4753atagen2026-10-16 05:50 UTC716
  const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC717
  if (view2) {
05668b0d4753atagen2026-10-16 05:50 UTC718
    const rows = splitY(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC719
    Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC720
  } else {
05668b0d4753atagen2026-10-16 05:50 UTC721
    Object.assign(L, splitX(I, ['A', 'B', 'C']));
05668b0d4753atagen2026-10-16 05:50 UTC722
    const y = order[0] === 't2' ? I.y : I.y + I.h;
05668b0d4753atagen2026-10-16 05:50 UTC723
    Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC724
  }
05668b0d4753atagen2026-10-16 05:50 UTC725
  return L;
05668b0d4753atagen2026-10-16 05:50 UTC726
}
05668b0d4753atagen2026-10-16 05:50 UTC727
05668b0d4753atagen2026-10-16 05:50 UTC728
{ // d3 tag toggle
05668b0d4753atagen2026-10-16 05:50 UTC729
  const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC730
  let view2 = false;
05668b0d4753atagen2026-10-16 05:50 UTC731
  d.set(tagLayout(I, view2, ['t1', 't2']));
05668b0d4753atagen2026-10-16 05:50 UTC732
  d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); });
05668b0d4753atagen2026-10-16 05:50 UTC733
}
05668b0d4753atagen2026-10-16 05:50 UTC734
05668b0d4753atagen2026-10-16 05:50 UTC735
{ // d3b tag reorder
05668b0d4753atagen2026-10-16 05:50 UTC736
  const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC737
  let order = ['t1', 't2'], blocks = true;
05668b0d4753atagen2026-10-16 05:50 UTC738
  d.set(tagLayout(I, true, order));
05668b0d4753atagen2026-10-16 05:50 UTC739
  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 UTC740
  d.check('rigid blocks', true, v => blocks = v);
05668b0d4753atagen2026-10-16 05:50 UTC741
}
05668b0d4753atagen2026-10-16 05:50 UTC742
05668b0d4753atagen2026-10-16 05:50 UTC743
{ // d4a orientation flip
05668b0d4753atagen2026-10-16 05:50 UTC744
  const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC745
  let rows = false;
05668b0d4753atagen2026-10-16 05:50 UTC746
  const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']);
05668b0d4753atagen2026-10-16 05:50 UTC747
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC748
  d.button('Flip orientation', () => { rows = !rows; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC749
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC750
}
05668b0d4753atagen2026-10-16 05:50 UTC751
05668b0d4753atagen2026-10-16 05:50 UTC752
{ // d4b grow both axes
05668b0d4753atagen2026-10-16 05:50 UTC753
  const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC754
  let wide = false;
05668b0d4753atagen2026-10-16 05:50 UTC755
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC756
    const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]);
05668b0d4753atagen2026-10-16 05:50 UTC757
    return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) };
05668b0d4753atagen2026-10-16 05:50 UTC758
  };
05668b0d4753atagen2026-10-16 05:50 UTC759
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC760
  d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC761
}
05668b0d4753atagen2026-10-16 05:50 UTC762
05668b0d4753atagen2026-10-16 05:50 UTC763
{ // d5 lanes
05668b0d4753atagen2026-10-16 05:50 UTC764
  const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC765
  let order = ['A', 'B', 'C', 'D'];
05668b0d4753atagen2026-10-16 05:50 UTC766
  const L = () => splitX(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC767
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC768
  d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC769
  d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC770
  d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC771
  d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC772
}
05668b0d4753atagen2026-10-16 05:50 UTC773
05668b0d4753atagen2026-10-16 05:50 UTC774
{ // d6 interrupts
05668b0d4753atagen2026-10-16 05:50 UTC775
  const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC776
  const targets = [
05668b0d4753atagen2026-10-16 05:50 UTC777
    () => splitX(I, ['A', 'B', 'C']),
05668b0d4753atagen2026-10-16 05:50 UTC778
    () => splitX(I, ['C', 'A', 'B']),
05668b0d4753atagen2026-10-16 05:50 UTC779
    () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]),
05668b0d4753atagen2026-10-16 05:50 UTC780
    () => splitX(I, ['B', 'C', 'A']),
05668b0d4753atagen2026-10-16 05:50 UTC781
    () => ({ ...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 UTC782
  ];
05668b0d4753atagen2026-10-16 05:50 UTC783
  const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
05668b0d4753atagen2026-10-16 05:50 UTC784
  let idx = 0;
05668b0d4753atagen2026-10-16 05:50 UTC785
  d.set(withB(targets[0]()));
05668b0d4753atagen2026-10-16 05:50 UTC786
  const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); };
05668b0d4753atagen2026-10-16 05:50 UTC787
  d.button('Next target', next);
05668b0d4753atagen2026-10-16 05:50 UTC788
  d.button('Storm (8 rapid interrupts)', () => {
05668b0d4753atagen2026-10-16 05:50 UTC789
    let n = 0;
05668b0d4753atagen2026-10-16 05:50 UTC790
    const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); };
05668b0d4753atagen2026-10-16 05:50 UTC791
    fire();
05668b0d4753atagen2026-10-16 05:50 UTC792
  });
05668b0d4753atagen2026-10-16 05:50 UTC793
  d.check('match velocity', true, v => d.vmatch = v);
05668b0d4753atagen2026-10-16 05:50 UTC794
}
05668b0d4753atagen2026-10-16 05:50 UTC795
05668b0d4753atagen2026-10-16 05:50 UTC796
{ // d7 across outputs
05668b0d4753atagen2026-10-16 05:50 UTC797
  const W = 680, H = 260;
05668b0d4753atagen2026-10-16 05:50 UTC798
  const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H);
05668b0d4753atagen2026-10-16 05:50 UTC799
  const d = new Demo('d7', W, H, { outputs: [o1, o2] });
05668b0d4753atagen2026-10-16 05:50 UTC800
  const I1 = inner(320, H, 0), I2 = inner(320, H, 360);
05668b0d4753atagen2026-10-16 05:50 UTC801
  let s1 = ['A', 'B'], s2 = ['C'];
05668b0d4753atagen2026-10-16 05:50 UTC802
  const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) });
05668b0d4753atagen2026-10-16 05:50 UTC803
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC804
  d.button('Move B across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC805
    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 UTC806
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC807
  });
05668b0d4753atagen2026-10-16 05:50 UTC808
  d.button('Move A across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC809
    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 UTC810
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC811
  });
05668b0d4753atagen2026-10-16 05:50 UTC812
  d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC813
}
05668b0d4753atagen2026-10-16 05:50 UTC814
05668b0d4753atagen2026-10-16 05:50 UTC815
function layoutTree(node, rect, visible, out) {
05668b0d4753atagen2026-10-16 05:50 UTC816
  if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; }
05668b0d4753atagen2026-10-16 05:50 UTC817
  const horiz = node.dir === 'row';
05668b0d4753atagen2026-10-16 05:50 UTC818
  const shown = node.kids.map(k => anyVisible(k, visible));
05668b0d4753atagen2026-10-16 05:50 UTC819
  const m = shown.filter(Boolean).length;
05668b0d4753atagen2026-10-16 05:50 UTC820
  const len = horiz ? rect.w : rect.h;
05668b0d4753atagen2026-10-16 05:50 UTC821
  const avail = len - G * Math.max(0, m - 1);
05668b0d4753atagen2026-10-16 05:50 UTC822
  let pos = horiz ? rect.x : rect.y, seen = 0;
05668b0d4753atagen2026-10-16 05:50 UTC823
  node.kids.forEach((k, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC824
    if (!shown[i]) {
05668b0d4753atagen2026-10-16 05:50 UTC825
      const p = seen ? pos - G / 2 : pos;
05668b0d4753atagen2026-10-16 05:50 UTC826
      const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC827
      layoutTree(k, r, () => false, out);
05668b0d4753atagen2026-10-16 05:50 UTC828
      return;
05668b0d4753atagen2026-10-16 05:50 UTC829
    }
05668b0d4753atagen2026-10-16 05:50 UTC830
    const size = avail / m;
05668b0d4753atagen2026-10-16 05:50 UTC831
    const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size);
05668b0d4753atagen2026-10-16 05:50 UTC832
    layoutTree(k, r, visible, out);
05668b0d4753atagen2026-10-16 05:50 UTC833
    pos += size + G; seen++;
05668b0d4753atagen2026-10-16 05:50 UTC834
  });
05668b0d4753atagen2026-10-16 05:50 UTC835
}
05668b0d4753atagen2026-10-16 05:50 UTC836
function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); }
05668b0d4753atagen2026-10-16 05:50 UTC837
05668b0d4753atagen2026-10-16 05:50 UTC838
{ // d8 one tree, tags filter
05668b0d4753atagen2026-10-16 05:50 UTC839
  const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC840
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
05668b0d4753atagen2026-10-16 05:50 UTC841
  const view = { 1: true, 2: true };
05668b0d4753atagen2026-10-16 05:50 UTC842
  let mixed = false;
05668b0d4753atagen2026-10-16 05:50 UTC843
  const tree = () => mixed
05668b0d4753atagen2026-10-16 05:50 UTC844
    ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] }
05668b0d4753atagen2026-10-16 05:50 UTC845
    : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] };
05668b0d4753atagen2026-10-16 05:50 UTC846
  const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; };
05668b0d4753atagen2026-10-16 05:50 UTC847
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC848
  d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC849
  d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC850
  d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC851
}
05668b0d4753atagen2026-10-16 05:50 UTC852
cd7085e54c88atagen2026-10-16 06:58 UTC853
function stackLayouts(I) {
cd7085e54c88atagen2026-10-16 06:58 UTC854
  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 UTC855
  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 UTC856
  return {
cd7085e54c88atagen2026-10-16 06:58 UTC857
    'A | B | C': () => splitX(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC858
    'A | (B / C)': () => col(splitX(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC859
    '(A / C) | B': () => col(splitX(I, ['L', 'B']), 'L', ['A', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC860
    '(A / B) | C': () => col(splitX(I, ['L', 'C']), 'L', ['A', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC861
    'B | C | A': () => splitX(I, ['B', 'C', 'A']),
cd7085e54c88atagen2026-10-16 06:58 UTC862
    'A / B / C': () => splitY(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC863
    'A / (B | C)': () => row(splitY(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC864
    '(A / B) | (C / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'B']), 'R', ['C', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC865
    'A | (C / B / D)': () => col(splitX(I, ['A', 'R']), 'R', ['C', 'B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC866
    '(A / D) | (C / B)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'D']), 'R', ['C', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC867
    '(A / B / C) | D': () => col(splitX(I, ['L', 'D']), 'L', ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC868
    '(A / C) | (B / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'C']), 'R', ['B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC869
  };
cd7085e54c88atagen2026-10-16 06:58 UTC870
}
cd7085e54c88atagen2026-10-16 06:58 UTC871
cd7085e54c88atagen2026-10-16 06:58 UTC872
for (const [id, names] of [
cd7085e54c88atagen2026-10-16 06:58 UTC873
  ['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 UTC874
  ['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 UTC875
]) {
cd7085e54c88atagen2026-10-16 06:58 UTC876
  const W = 640, H = 300, d = new Demo(id, W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC877
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC878
  for (const n of names) d.button(n, () => d.go(S[n]()));
cd7085e54c88atagen2026-10-16 06:58 UTC879
}
cd7085e54c88atagen2026-10-16 06:58 UTC880
cd7085e54c88atagen2026-10-16 06:58 UTC881
{ // d9c grid
cd7085e54c88atagen2026-10-16 06:58 UTC882
  const W = 640, H = 300, d = new Demo('d9c', W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC883
  let at = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC884
  const names = ['(A / D) | (C / B)', '(A / B / C) | D'];
cd7085e54c88atagen2026-10-16 06:58 UTC885
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC886
  d.button('(A / D) | (C / B)  ⇄  (A / B / C) | D', () => { at = 1 - at; d.go(S[names[at]]()); });
cd7085e54c88atagen2026-10-16 06:58 UTC887
}
cd7085e54c88atagen2026-10-16 06:58 UTC888
cd7085e54c88atagen2026-10-16 06:58 UTC889
{ // d10 floating to tiled
cd7085e54c88atagen2026-10-16 06:58 UTC890
  const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H);
cd7085e54c88atagen2026-10-16 06:58 UTC891
  const FL = R(220, 40, 220, 130);
cd7085e54c88atagen2026-10-16 06:58 UTC892
  let tiled = false;
cd7085e54c88atagen2026-10-16 06:58 UTC893
  const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']);
cd7085e54c88atagen2026-10-16 06:58 UTC894
  const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL });
cd7085e54c88atagen2026-10-16 06:58 UTC895
  const ortho = () => STYLE === 'orthogonal';
cd7085e54c88atagen2026-10-16 06:58 UTC896
  const assemble = (n, tiledAt, fAt, kind) => {
cd7085e54c88atagen2026-10-16 06:58 UTC897
    const keys = [];
cd7085e54c88atagen2026-10-16 06:58 UTC898
    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 UTC899
    return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) };
cd7085e54c88atagen2026-10-16 06:58 UTC900
  };
cd7085e54c88atagen2026-10-16 06:58 UTC901
  const floatIn = from => {
cd7085e54c88atagen2026-10-16 06:58 UTC902
    const slot = tiledL().F, to = tiledL();
cd7085e54c88atagen2026-10-16 06:58 UTC903
    const bx = (from.B.x + from.B.w + from.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC904
    const tp = plan({ A: from.A, B: from.B, C: from.C, P: R(bx, I.y, 0, I.h) }, { A: to.A, B: to.B, C: to.C, P: slot });
cd7085e54c88atagen2026-10-16 06:58 UTC905
    const k = tp.durs.length, f = from.F;
cd7085e54c88atagen2026-10-16 06:58 UTC906
    const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC907
    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)];
cd7085e54c88atagen2026-10-16 06:58 UTC908
    const n = Math.max(path.length - 1, k) + 1;
cd7085e54c88atagen2026-10-16 06:58 UTC909
    return assemble(n, i => tp.keys[Math.min(i, k)], i => i === n ? slot : path[Math.min(i, path.length - 1)], `float in, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC910
  };
cd7085e54c88atagen2026-10-16 06:58 UTC911
  const floatOut = from => {
cd7085e54c88atagen2026-10-16 06:58 UTC912
    const slot = from.F, to = floatL();
cd7085e54c88atagen2026-10-16 06:58 UTC913
    const bx = (to.B.x + to.B.w + to.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC914
    const tp = plan({ A: from.A, B: from.B, C: from.C, P: slot }, { A: to.A, B: to.B, C: to.C, P: R(bx, I.y, 0, I.h) });
cd7085e54c88atagen2026-10-16 06:58 UTC915
    const k = tp.durs.length;
cd7085e54c88atagen2026-10-16 06:58 UTC916
    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 UTC917
    const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL];
cd7085e54c88atagen2026-10-16 06:58 UTC918
    const n = Math.max(path.length - 1, k + 1);
cd7085e54c88atagen2026-10-16 06:58 UTC919
    return assemble(n, i => tp.keys[Math.max(0, Math.min(i - 1, k))], i => path[Math.min(i, path.length - 1)], `lift out, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC920
  };
cd7085e54c88atagen2026-10-16 06:58 UTC921
  d.set(floatL());
cd7085e54c88atagen2026-10-16 06:58 UTC922
  d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); });
cd7085e54c88atagen2026-10-16 06:58 UTC923
}
cd7085e54c88atagen2026-10-16 06:58 UTC924
05668b0d4753atagen2026-10-16 05:50 UTC925
document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };
05668b0d4753atagen2026-10-16 05:50 UTC926
document.getElementById('style').onchange = e => { STYLE = e.target.value; };
05668b0d4753atagen2026-10-16 05:50 UTC927
const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); };
05668b0d4753atagen2026-10-16 05:50 UTC928
requestAnimationFrame(loop);
05668b0d4753atagen2026-10-16 05:50 UTC929
</script>
05668b0d4753atagen2026-10-16 05:50 UTC930
</body>
05668b0d4753atagen2026-10-16 05:50 UTC931
</html>