atagen

monolith

public
Star0
Watch0
944fc4782ac3cc4eee18a8f5a9b71665eecf7b8f
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>
944fc4782ac3atagen2026-10-16 08:09 UTC110
<p><strong>Lane extent.</strong> Below, T fills the top 70% and A, B, C share a short bottom row. Reversing the row needs 3 lanes. Spanning only the involved band, each lane is a third of the short row, so the windows get thin but T never moves. Spanning the whole output, the lanes are thicker, but T is in the way and has to squeeze into a lane too.</p>
944fc4782ac3atagen2026-10-16 08:09 UTC111
<div class="demo" id="d12"></div>
05668b0d4753atagen2026-10-16 05:50 UTC112
cd7085e54c88atagen2026-10-16 06:58 UTC113
<h2>7. Four phases, the grid</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC114
<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 UTC115
<ol>
cd7085e54c88atagen2026-10-16 06:58 UTC116
  <li>Every involved window shrinks vertically into its own horizontal strip, ordered by where it starts.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC117
  <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 UTC118
  <li>It moves vertically to its final height. Its column keeps it clear of everything.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC119
  <li>It expands horizontally to its final width.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC120
</ol>
cd7085e54c88atagen2026-10-16 06:58 UTC121
<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 UTC122
<div class="demo" id="d9c"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC123
cd7085e54c88atagen2026-10-16 06:58 UTC124
<h2>8. Joining and leaving stacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC125
<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 UTC126
<div class="demo" id="d9a"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC127
<div class="demo" id="d9b"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC128
cd7085e54c88atagen2026-10-16 06:58 UTC129
<h2>9. Rigid blocks and movement commands</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC130
<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 UTC131
<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 UTC132
<div class="demo" id="d3b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC133
cd7085e54c88atagen2026-10-16 06:58 UTC134
<h2>10. Interrupts and velocity</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC135
<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 UTC136
<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 UTC137
<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 UTC138
<div class="demo" id="d6"></div>
05668b0d4753atagen2026-10-16 05:50 UTC139
cd7085e54c88atagen2026-10-16 06:58 UTC140
<h2>11. Across outputs</h2>
05668b0d4753atagen2026-10-16 05:50 UTC141
<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 UTC142
<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 UTC143
<div class="demo" id="d7"></div>
05668b0d4753atagen2026-10-16 05:50 UTC144
944fc4782ac3atagen2026-10-16 08:09 UTC145
<h2>12. Floating, fullscreen and expand</h2>
aa790e7e33a0atagen2026-10-16 08:03 UTC146
<p>A floating window joining the tiled layout travels above the tiled layer and is exempt from the overlap rule while it does. The mode button cycles three choreographies. Lifting out plays each one in reverse.</p>
aa790e7e33a0atagen2026-10-16 08:03 UTC147
<ul>
944fc4782ac3atagen2026-10-16 08:09 UTC148
  <li><strong>Direct glide</strong> (default). F lerps straight into the slot while the gap opens, in one phase.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC149
  <li><strong>Slot opens last.</strong> F travels one axis per phase until it hovers over its slot, then the gap opens and F lands as it widens.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC150
  <li><strong>Slot opens first.</strong> The gap opens while F is still travelling, then F lands.</li>
aa790e7e33a0atagen2026-10-16 08:03 UTC151
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC152
<div class="demo" id="d10"></div>
944fc4782ac3atagen2026-10-16 08:09 UTC153
<h3>Fullscreen</h3>
944fc4782ac3atagen2026-10-16 08:09 UTC154
<p>Fullscreen uses the same direct glide. The window lifts above the tiled layer and lerps to cover the output in one phase. Its tiled slot stays where it is underneath, so leaving fullscreen glides back into it and nothing else moves either way.</p>
944fc4782ac3atagen2026-10-16 08:09 UTC155
<div class="demo" id="d13"></div>
944fc4782ac3atagen2026-10-16 08:09 UTC156
<h3>Expand</h3>
944fc4782ac3atagen2026-10-16 08:09 UTC157
<p>Expand is a tiled operation, not a glide. The expanded window or subtree grows to fill its slot while its siblings are squeezed to zero-size lines against the slot's edges, so it follows the normal planner rules, orthogonal included. Expanding a window inside its column is one vertical phase. Expanding it to the whole output changes both axes, so orthogonal style takes two.</p>
944fc4782ac3atagen2026-10-16 08:09 UTC158
<div class="demo" id="d14"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC159
cd7085e54c88atagen2026-10-16 06:58 UTC160
<h2>13. Fallbacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC161
<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 UTC162
cd7085e54c88atagen2026-10-16 06:58 UTC163
<h2>14. Algorithm</h2>
05668b0d4753atagen2026-10-16 05:50 UTC164
<pre>plan(from, to):
440b67836dd5atagen2026-10-16 07:57 UTC165
  if from > to in a canonical order: return reverse(plan(to, from))
05668b0d4753atagen2026-10-16 05:50 UTC166
  groups = motion groups (windows whose swept areas can touch, transitively)
05668b0d4753atagen2026-10-16 05:50 UTC167
  for each group:
05668b0d4753atagen2026-10-16 05:50 UTC168
    if every pair shares a relation between from and to
05668b0d4753atagen2026-10-16 05:50 UTC169
       and (style is direct or every window changes only one axis):
05668b0d4753atagen2026-10-16 05:50 UTC170
      return [from, to]                                    # 1 phase
440b67836dd5atagen2026-10-16 07:57 UTC171
    if 2-SAT over axis order finds K1, preferring no holes: # mixed rects
05668b0d4753atagen2026-10-16 05:50 UTC172
      return [from, K1, to]                                # 2 phases
05668b0d4753atagen2026-10-16 05:50 UTC173
    for lane_axis in [y, x]:                               # strips, then columns
05668b0d4753atagen2026-10-16 05:50 UTC174
      for set in [windows in reversals, all movers, all]:
05668b0d4753atagen2026-10-16 05:50 UTC175
        for k in 2..|set|:
05668b0d4753atagen2026-10-16 05:50 UTC176
          if lanes assignable with k lanes and all three phases validate:
05668b0d4753atagen2026-10-16 05:50 UTC177
            return [from, K1, K2, to]                      # 3 phases
cd7085e54c88atagen2026-10-16 06:58 UTC178
    for set in [windows in reversals, all movers, all]:
cd7085e54c88atagen2026-10-16 06:58 UTC179
      if the grid validates for set:                       # always true for all
cd7085e54c88atagen2026-10-16 06:58 UTC180
        return [from, K1, K2, K3, to]                      # 4 phases
cd7085e54c88atagen2026-10-16 06:58 UTC181
    return fallback(from, to)                              # unreachable in theory
05668b0d4753atagen2026-10-16 05:50 UTC182
05668b0d4753atagen2026-10-16 05:50 UTC183
  rigid blocks: run plan() on block rectangles first, then map windows into them
cd7085e54c88atagen2026-10-16 06:58 UTC184
  floating:     slot opens via plan() with a seed, floater travels x, then y, then lands
05668b0d4753atagen2026-10-16 05:50 UTC185
  interrupts:   from = rectangles sampled at the current presentation time
05668b0d4753atagen2026-10-16 05:50 UTC186
  velocity:     first-phase curve per group, slope from the fastest mover,
05668b0d4753atagen2026-10-16 05:50 UTC187
                applied only if moving the same way, control point height ≤ 1
05668b0d4753atagen2026-10-16 05:50 UTC188
  validation:   exact pairwise check over the curve actually used</pre>
05668b0d4753atagen2026-10-16 05:50 UTC189
<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 UTC190
<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 UTC191
cd7085e54c88atagen2026-10-16 06:58 UTC192
<h2>15. Compared with wry master</h2>
05668b0d4753atagen2026-10-16 05:50 UTC193
<table>
05668b0d4753atagen2026-10-16 05:50 UTC194
  <tr><th>wry master</th><th>this design</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC195
  <tr><td>SingleAction, MixedSinglePhase</td><td>1 phase</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC196
  <tr><td>SpaceThenOrthogonalGrowth, OrientationChange, HierarchyOrderedScales</td><td>2 phases, one 2-SAT problem instead of three strategies</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC197
  <tr><td>SwapLanes</td><td>3 phases, general lane assignment for any permutation</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC198
  <tr><td>no equivalent</td><td>4 phases, the grid, for simultaneous reversals on both axes</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC199
  <tr><td>ReversedForwardPlan</td><td>not needed, the construction is symmetric</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC200
  <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 UTC201
  <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 UTC202
  <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 UTC203
  <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 UTC204
  <tr><td>Cross-output moves skip animation</td><td>planned in global coordinates, clipped per output</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC205
</table>
05668b0d4753atagen2026-10-16 05:50 UTC206
cd7085e54c88atagen2026-10-16 06:58 UTC207
<h2>16. Interspersing tags (open, changes the layout model)</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC208
<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 UTC209
<table>
05668b0d4753atagen2026-10-16 05:50 UTC210
  <tr><th>option</th><th>meaning</th><th>verdict</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC211
  <tr><td>Forbid it</td><td>tags stay in their rows</td><td>limiting</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC212
  <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 UTC213
  <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 UTC214
  <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 UTC215
  <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 UTC216
</table>
cd7085e54c88atagen2026-10-16 06:58 UTC217
<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 UTC218
<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 UTC219
<div class="demo" id="d8"></div>
944fc4782ac3atagen2026-10-16 08:09 UTC220
<p><strong>Hidden windows keep their slot.</strong> They are not coupled to visible neighbours. My earlier phrasing was wrong, and your version is the rule.</p>
944fc4782ac3atagen2026-10-16 08:09 UTC221
<ul>
944fc4782ac3atagen2026-10-16 08:09 UTC222
  <li>The tree never moves a hidden leaf. Moves in a filtered view swap the slots of visible windows only. Swap A and B below with tag 2 hidden, then show it again, and D is still between them.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC223
  <li>Containers die by the normal rule, when one child is left, counting hidden children. A hidden only child is demoted into the grandparent at the container's position. Move C next to B below with tag 2 hidden. Row 2 is left holding only E, so it collapses and E becomes a full-width row of its own.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC224
  <li>A container holding only hidden children survives as long as it has two or more, invisible until they return.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC225
  <li>New windows insert next to the focused visible window.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC226
  <li>Hidden windows keep their share of a container's size when visible siblings are resized.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC227
</ul>
944fc4782ac3atagen2026-10-16 08:09 UTC228
<p>Tick "show hidden slots" to see where hidden windows will reappear.</p>
944fc4782ac3atagen2026-10-16 08:09 UTC229
<div class="demo" id="d11"></div>
05668b0d4753atagen2026-10-16 05:50 UTC230
cd7085e54c88atagen2026-10-16 06:58 UTC231
<h2>17. Decisions</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC232
<h3>Settled</h3>
cd7085e54c88atagen2026-10-16 06:58 UTC233
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC234
  <li>Orthogonal is the default and the flagship look. Direct is a toggle.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC235
  <li>Window moves and block moves both exist as commands. Tag reordering moves rigid blocks.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC236
  <li>One curve per motion group, bounded by phase time rather than amount of motion.</li>
aa790e7e33a0atagen2026-10-16 08:03 UTC237
  <li>Seeds start as a line on the old boundary.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC238
  <li>Floating to tiled and fullscreen use a direct glide. Expand is tiled and follows the planner.</li>
944fc4782ac3atagen2026-10-16 08:09 UTC239
  <li>One tree per output, tags filter it. Hidden windows keep their slots.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC240
  <li>The planner must never need a fallback. Pinch and lerp stay as safety nets to try.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC241
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC242
<h3>Open</h3>
944fc4782ac3atagen2026-10-16 08:09 UTC243
cd7085e54c88atagen2026-10-16 06:58 UTC244
<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>
944fc4782ac3atagen2026-10-16 08:09 UTC245
<div class="q"><strong>Hidden-slot rules (section 16).</strong> Check the rules list against your intent, especially demotion and insertion next to the focused window.</div>
05668b0d4753atagen2026-10-16 05:50 UTC246
</main>
05668b0d4753atagen2026-10-16 05:50 UTC247
05668b0d4753atagen2026-10-16 05:50 UTC248
<script id="planner">
05668b0d4753atagen2026-10-16 05:50 UTC249
const G = 10;
05668b0d4753atagen2026-10-16 05:50 UTC250
const EPS = 0.5;
05668b0d4753atagen2026-10-16 05:50 UTC251
let PHASE_MS = 450;
cd7085e54c88atagen2026-10-16 06:58 UTC252
let STYLE = 'orthogonal';
944fc4782ac3atagen2026-10-16 08:09 UTC253
let LANE_REGION = null;
05668b0d4753atagen2026-10-16 05:50 UTC254
05668b0d4753atagen2026-10-16 05:50 UTC255
const R = (x, y, w, h) => ({ x, y, w, h });
05668b0d4753atagen2026-10-16 05:50 UTC256
const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; };
05668b0d4753atagen2026-10-16 05:50 UTC257
const eq = (a, b) => Math.abs(a - b) < 0.05;
05668b0d4753atagen2026-10-16 05:50 UTC258
const lerp = (a, b, s) => a + (b - a) * s;
05668b0d4753atagen2026-10-16 05:50 UTC259
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 UTC260
05668b0d4753atagen2026-10-16 05:50 UTC261
function splitX(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC262
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC263
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC264
  const avail = rect.w - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC265
  let x = rect.x;
05668b0d4753atagen2026-10-16 05:50 UTC266
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC267
  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 UTC268
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC269
}
05668b0d4753atagen2026-10-16 05:50 UTC270
function splitY(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC271
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC272
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC273
  const avail = rect.h - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC274
  let y = rect.y;
05668b0d4753atagen2026-10-16 05:50 UTC275
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC276
  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 UTC277
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC278
}
05668b0d4753atagen2026-10-16 05:50 UTC279
05668b0d4753atagen2026-10-16 05:50 UTC280
function relBits(a, b) {
05668b0d4753atagen2026-10-16 05:50 UTC281
  let s = 0;
05668b0d4753atagen2026-10-16 05:50 UTC282
  if (a.x + a.w <= b.x + EPS) s |= 1;
05668b0d4753atagen2026-10-16 05:50 UTC283
  if (b.x + b.w <= a.x + EPS) s |= 2;
05668b0d4753atagen2026-10-16 05:50 UTC284
  if (a.y + a.h <= b.y + EPS) s |= 4;
05668b0d4753atagen2026-10-16 05:50 UTC285
  if (b.y + b.h <= a.y + EPS) s |= 8;
05668b0d4753atagen2026-10-16 05:50 UTC286
  return s;
05668b0d4753atagen2026-10-16 05:50 UTC287
}
05668b0d4753atagen2026-10-16 05:50 UTC288
function phaseOk(K1, K2, list) {
05668b0d4753atagen2026-10-16 05:50 UTC289
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC290
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC291
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC292
      if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false;
05668b0d4753atagen2026-10-16 05:50 UTC293
    }
05668b0d4753atagen2026-10-16 05:50 UTC294
  return true;
05668b0d4753atagen2026-10-16 05:50 UTC295
}
05668b0d4753atagen2026-10-16 05:50 UTC296
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 UTC297
function pureOk(keys) {
cd7085e54c88atagen2026-10-16 06:58 UTC298
  if (STYLE !== 'orthogonal') return true;
cd7085e54c88atagen2026-10-16 06:58 UTC299
  for (let k = 0; k + 1 < keys.length; k++)
cd7085e54c88atagen2026-10-16 06:58 UTC300
    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 UTC301
  return true;
cd7085e54c88atagen2026-10-16 06:58 UTC302
}
05668b0d4753atagen2026-10-16 05:50 UTC303
440b67836dd5atagen2026-10-16 07:57 UTC304
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 UTC305
05668b0d4753atagen2026-10-16 05:50 UTC306
function plan(from, to, opts = {}) {
440b67836dd5atagen2026-10-16 07:57 UTC307
  if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [PHASE_MS] };
440b67836dd5atagen2026-10-16 07:57 UTC308
  if (signature(from) <= signature(to)) return planOneWay(from, to);
440b67836dd5atagen2026-10-16 07:57 UTC309
  const p = planOneWay(to, from);
440b67836dd5atagen2026-10-16 07:57 UTC310
  return { kind: p.kind, keys: [...p.keys].reverse(), durs: [...p.durs].reverse() };
440b67836dd5atagen2026-10-16 07:57 UTC311
}
440b67836dd5atagen2026-10-16 07:57 UTC312
440b67836dd5atagen2026-10-16 07:57 UTC313
function planOneWay(from, to) {
05668b0d4753atagen2026-10-16 05:50 UTC314
  const list = Object.keys(to);
05668b0d4753atagen2026-10-16 05:50 UTC315
  const T = PHASE_MS;
05668b0d4753atagen2026-10-16 05:50 UTC316
  const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; });
05668b0d4753atagen2026-10-16 05:50 UTC317
  if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] };
05668b0d4753atagen2026-10-16 05:50 UTC318
  const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); });
05668b0d4753atagen2026-10-16 05:50 UTC319
  if (phaseOk(from, to, list) && (STYLE === 'direct' || pure))
05668b0d4753atagen2026-10-16 05:50 UTC320
    return { kind: '1 phase', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC321
  const two = axisOrder(from, to, list, movers);
05668b0d4753atagen2026-10-16 05:50 UTC322
  if (two) return two;
05668b0d4753atagen2026-10-16 05:50 UTC323
  for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; }
cd7085e54c88atagen2026-10-16 06:58 UTC324
  const g = grid(from, to, list, movers);
cd7085e54c88atagen2026-10-16 06:58 UTC325
  if (g) return g;
05668b0d4753atagen2026-10-16 05:50 UTC326
  return pinch(from, to, movers);
05668b0d4753atagen2026-10-16 05:50 UTC327
}
05668b0d4753atagen2026-10-16 05:50 UTC328
cd7085e54c88atagen2026-10-16 06:58 UTC329
function conflictSet(from, to, list) {
cd7085e54c88atagen2026-10-16 06:58 UTC330
  const out = new Set();
cd7085e54c88atagen2026-10-16 06:58 UTC331
  for (let i = 0; i < list.length; i++)
cd7085e54c88atagen2026-10-16 06:58 UTC332
    for (let j = i + 1; j < list.length; j++) {
cd7085e54c88atagen2026-10-16 06:58 UTC333
      const a = list[i], b = list[j];
cd7085e54c88atagen2026-10-16 06:58 UTC334
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { out.add(a); out.add(b); }
cd7085e54c88atagen2026-10-16 06:58 UTC335
    }
cd7085e54c88atagen2026-10-16 06:58 UTC336
  return [...out];
cd7085e54c88atagen2026-10-16 06:58 UTC337
}
cd7085e54c88atagen2026-10-16 06:58 UTC338
cd7085e54c88atagen2026-10-16 06:58 UTC339
function grid(from, to, list, movers) {
cd7085e54c88atagen2026-10-16 06:58 UTC340
  const cx = r => r.x + r.w / 2, cy = r => r.y + r.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC341
  for (const set of [conflictSet(from, to, list), movers, list]) {
cd7085e54c88atagen2026-10-16 06:58 UTC342
    const n = set.length;
cd7085e54c88atagen2026-10-16 06:58 UTC343
    if (n < 2) continue;
cd7085e54c88atagen2026-10-16 06:58 UTC344
    let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
cd7085e54c88atagen2026-10-16 06:58 UTC345
    for (const id of set) for (const r of [from[id], to[id]]) {
cd7085e54c88atagen2026-10-16 06:58 UTC346
      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 UTC347
    }
cd7085e54c88atagen2026-10-16 06:58 UTC348
    const sh = (y1 - y0 - (n - 1) * G) / n, sw = (x1 - x0 - (n - 1) * G) / n;
cd7085e54c88atagen2026-10-16 06:58 UTC349
    const rowOf = {}, colOf = {};
cd7085e54c88atagen2026-10-16 06:58 UTC350
    [...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 UTC351
    [...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 UTC352
    const inSet = new Set(set);
cd7085e54c88atagen2026-10-16 06:58 UTC353
    for (const phase of [3, 2, 4, 1]) {
cd7085e54c88atagen2026-10-16 06:58 UTC354
      const K = [{}, {}, {}];
cd7085e54c88atagen2026-10-16 06:58 UTC355
      for (const id of list) {
cd7085e54c88atagen2026-10-16 06:58 UTC356
        const f = from[id], t = to[id];
cd7085e54c88atagen2026-10-16 06:58 UTC357
        if (inSet.has(id)) {
cd7085e54c88atagen2026-10-16 06:58 UTC358
          K[0][id] = R(f.x, rowOf[id], f.w, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC359
          K[1][id] = R(colOf[id], rowOf[id], sw, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC360
          K[2][id] = R(colOf[id], t.y, sw, t.h);
cd7085e54c88atagen2026-10-16 06:58 UTC361
        } else for (let k = 0; k < 3; k++) K[k][id] = k + 1 >= phase ? t : f;
cd7085e54c88atagen2026-10-16 06:58 UTC362
      }
cd7085e54c88atagen2026-10-16 06:58 UTC363
      const keys = [from, ...K, to];
cd7085e54c88atagen2026-10-16 06:58 UTC364
      let ok = true;
cd7085e54c88atagen2026-10-16 06:58 UTC365
      for (let k = 0; k < 4 && ok; k++) ok = phaseOk(keys[k], keys[k + 1], list);
cd7085e54c88atagen2026-10-16 06:58 UTC366
      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 UTC367
    }
cd7085e54c88atagen2026-10-16 06:58 UTC368
  }
cd7085e54c88atagen2026-10-16 06:58 UTC369
  return null;
cd7085e54c88atagen2026-10-16 06:58 UTC370
}
cd7085e54c88atagen2026-10-16 06:58 UTC371
05668b0d4753atagen2026-10-16 05:50 UTC372
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 UTC373
440b67836dd5atagen2026-10-16 07:57 UTC374
function adjBits(a, b) {
440b67836dd5atagen2026-10-16 07:57 UTC375
  const ovX = a.x < b.x + b.w + EPS && b.x < a.x + a.w + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC376
  const ovY = a.y < b.y + b.h + EPS && b.y < a.y + a.h + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC377
  let s = 0;
440b67836dd5atagen2026-10-16 07:57 UTC378
  if (ovY && Math.abs(b.x - (a.x + a.w)) <= G + EPS) s |= 1;
440b67836dd5atagen2026-10-16 07:57 UTC379
  if (ovY && Math.abs(a.x - (b.x + b.w)) <= G + EPS) s |= 2;
440b67836dd5atagen2026-10-16 07:57 UTC380
  if (ovX && Math.abs(b.y - (a.y + a.h)) <= G + EPS) s |= 4;
440b67836dd5atagen2026-10-16 07:57 UTC381
  if (ovX && Math.abs(a.y - (b.y + b.h)) <= G + EPS) s |= 8;
440b67836dd5atagen2026-10-16 07:57 UTC382
  return s;
440b67836dd5atagen2026-10-16 07:57 UTC383
}
440b67836dd5atagen2026-10-16 07:57 UTC384
function coupledEdges(from, to, list) {
440b67836dd5atagen2026-10-16 07:57 UTC385
  const out = [];
440b67836dd5atagen2026-10-16 07:57 UTC386
  for (let i = 0; i < list.length; i++)
440b67836dd5atagen2026-10-16 07:57 UTC387
    for (let j = i + 1; j < list.length; j++) {
440b67836dd5atagen2026-10-16 07:57 UTC388
      const a = list[i], b = list[j], s = adjBits(from[a], from[b]) & adjBits(to[a], to[b]);
440b67836dd5atagen2026-10-16 07:57 UTC389
      if (s) out.push([a, b, s]);
440b67836dd5atagen2026-10-16 07:57 UTC390
    }
440b67836dd5atagen2026-10-16 07:57 UTC391
  return out;
440b67836dd5atagen2026-10-16 07:57 UTC392
}
440b67836dd5atagen2026-10-16 07:57 UTC393
function holes(K, coupled) {
440b67836dd5atagen2026-10-16 07:57 UTC394
  let n = 0;
440b67836dd5atagen2026-10-16 07:57 UTC395
  for (const [a, b, s] of coupled) if ((adjBits(K[a], K[b]) & s) !== s) n++;
440b67836dd5atagen2026-10-16 07:57 UTC396
  return n;
440b67836dd5atagen2026-10-16 07:57 UTC397
}
440b67836dd5atagen2026-10-16 07:57 UTC398
05668b0d4753atagen2026-10-16 05:50 UTC399
function axisOrder(from, to, list, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC400
  const n = movers.length;
05668b0d4753atagen2026-10-16 05:50 UTC401
  if (n > 14) return null;
440b67836dd5atagen2026-10-16 07:57 UTC402
  const coupled = coupledEdges(from, to, list);
05668b0d4753atagen2026-10-16 05:50 UTC403
  let best = null, bestScore = Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC404
  for (let mask = 0; mask < (1 << n); mask++) {
05668b0d4753atagen2026-10-16 05:50 UTC405
    const K1 = { ...from };
440b67836dd5atagen2026-10-16 07:57 UTC406
    let late = 0;
05668b0d4753atagen2026-10-16 05:50 UTC407
    movers.forEach((id, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC408
      const xf = !!(mask & (1 << i));
05668b0d4753atagen2026-10-16 05:50 UTC409
      const c = chg(from[id], to[id]);
05668b0d4753atagen2026-10-16 05:50 UTC410
      K1[id] = mixed(from[id], to[id], xf);
440b67836dd5atagen2026-10-16 07:57 UTC411
      if (c.x && !c.y && !xf) late++;
440b67836dd5atagen2026-10-16 07:57 UTC412
      if (c.y && !c.x && xf) late++;
05668b0d4753atagen2026-10-16 05:50 UTC413
    });
440b67836dd5atagen2026-10-16 07:57 UTC414
    if (late >= bestScore) continue;
440b67836dd5atagen2026-10-16 07:57 UTC415
    if (!phaseOk(from, K1, list) || !phaseOk(K1, to, list)) continue;
440b67836dd5atagen2026-10-16 07:57 UTC416
    const score = holes(K1, coupled) * 1000 + late;
440b67836dd5atagen2026-10-16 07:57 UTC417
    if (score < bestScore) { best = K1; bestScore = score; if (score === 0) break; }
05668b0d4753atagen2026-10-16 05:50 UTC418
  }
440b67836dd5atagen2026-10-16 07:57 UTC419
  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 UTC420
}
05668b0d4753atagen2026-10-16 05:50 UTC421
05668b0d4753atagen2026-10-16 05:50 UTC422
function lanes(from, to, list, movers, ax) {
05668b0d4753atagen2026-10-16 05:50 UTC423
  const size = ax === 'y' ? 'h' : 'w';
05668b0d4753atagen2026-10-16 05:50 UTC424
  const bitsT = ax === 'y' ? 3 : 12;
05668b0d4753atagen2026-10-16 05:50 UTC425
  const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2;
05668b0d4753atagen2026-10-16 05:50 UTC426
  const conflict = new Set();
05668b0d4753atagen2026-10-16 05:50 UTC427
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC428
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC429
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC430
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); }
05668b0d4753atagen2026-10-16 05:50 UTC431
    }
05668b0d4753atagen2026-10-16 05:50 UTC432
  const sets = [[...conflict], movers, list];
05668b0d4753atagen2026-10-16 05:50 UTC433
  for (const set of sets) {
05668b0d4753atagen2026-10-16 05:50 UTC434
    const n = set.length;
05668b0d4753atagen2026-10-16 05:50 UTC435
    if (n < 2) continue;
05668b0d4753atagen2026-10-16 05:50 UTC436
    let lo = Infinity, hi = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC437
    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]); }
944fc4782ac3atagen2026-10-16 08:09 UTC438
    if (LANE_REGION) { lo = LANE_REGION[ax]; hi = LANE_REGION[ax] + LANE_REGION[size]; }
05668b0d4753atagen2026-10-16 05:50 UTC439
    const diff = [], lt = [];
05668b0d4753atagen2026-10-16 05:50 UTC440
    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 UTC441
    for (let i = 0; i < n; i++)
05668b0d4753atagen2026-10-16 05:50 UTC442
      for (let j = 0; j < n; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC443
        if (i === j) continue;
05668b0d4753atagen2026-10-16 05:50 UTC444
        const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]);
05668b0d4753atagen2026-10-16 05:50 UTC445
        if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC446
        for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC447
      }
05668b0d4753atagen2026-10-16 05:50 UTC448
    for (let k = 2; k <= n; k++) {
05668b0d4753atagen2026-10-16 05:50 UTC449
      const s = (hi - lo - (k - 1) * G) / k;
05668b0d4753atagen2026-10-16 05:50 UTC450
      if (s < 6) break;
05668b0d4753atagen2026-10-16 05:50 UTC451
      const lane = new Array(n).fill(-1);
05668b0d4753atagen2026-10-16 05:50 UTC452
      const assign = i => {
05668b0d4753atagen2026-10-16 05:50 UTC453
        if (i === n) return true;
05668b0d4753atagen2026-10-16 05:50 UTC454
        for (let l = 0; l < k; l++) {
05668b0d4753atagen2026-10-16 05:50 UTC455
          let ok = true;
05668b0d4753atagen2026-10-16 05:50 UTC456
          for (let j = 0; j < i && ok; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC457
            if (diff[i][j] && lane[j] === l) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC458
            if (lt[i][j] && !(l < lane[j])) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC459
            if (lt[j][i] && !(lane[j] < l)) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC460
          }
05668b0d4753atagen2026-10-16 05:50 UTC461
          if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; }
05668b0d4753atagen2026-10-16 05:50 UTC462
        }
05668b0d4753atagen2026-10-16 05:50 UTC463
        return false;
05668b0d4753atagen2026-10-16 05:50 UTC464
      };
05668b0d4753atagen2026-10-16 05:50 UTC465
      if (!assign(0)) continue;
05668b0d4753atagen2026-10-16 05:50 UTC466
      const strip = l => lo + l * (s + G);
05668b0d4753atagen2026-10-16 05:50 UTC467
      const inSet = new Set(set);
05668b0d4753atagen2026-10-16 05:50 UTC468
      for (const phase of [2, 1, 3]) {
05668b0d4753atagen2026-10-16 05:50 UTC469
        const K1 = {}, K2 = {};
05668b0d4753atagen2026-10-16 05:50 UTC470
        for (const id of list) {
05668b0d4753atagen2026-10-16 05:50 UTC471
          const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC472
          if (inSet.has(id)) {
05668b0d4753atagen2026-10-16 05:50 UTC473
            const l = lane[set.indexOf(id)];
05668b0d4753atagen2026-10-16 05:50 UTC474
            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 UTC475
            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 UTC476
          } else if (phase === 1) { K1[id] = t; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC477
          else if (phase === 2) { K1[id] = f; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC478
          else { K1[id] = f; K2[id] = f; }
05668b0d4753atagen2026-10-16 05:50 UTC479
        }
cd7085e54c88atagen2026-10-16 06:58 UTC480
        if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list) && pureOk([from, K1, K2, to]))
05668b0d4753atagen2026-10-16 05:50 UTC481
          return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC482
      }
05668b0d4753atagen2026-10-16 05:50 UTC483
    }
05668b0d4753atagen2026-10-16 05:50 UTC484
  }
05668b0d4753atagen2026-10-16 05:50 UTC485
  return null;
05668b0d4753atagen2026-10-16 05:50 UTC486
}
05668b0d4753atagen2026-10-16 05:50 UTC487
05668b0d4753atagen2026-10-16 05:50 UTC488
function pinch(from, to, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC489
  const K1 = { ...from }, K2 = { ...to };
05668b0d4753atagen2026-10-16 05:50 UTC490
  for (const id of movers) {
05668b0d4753atagen2026-10-16 05:50 UTC491
    const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC492
    K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC493
    K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC494
  }
05668b0d4753atagen2026-10-16 05:50 UTC495
  return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC496
}
05668b0d4753atagen2026-10-16 05:50 UTC497
05668b0d4753atagen2026-10-16 05:50 UTC498
function bbox(rects) {
05668b0d4753atagen2026-10-16 05:50 UTC499
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC500
  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 UTC501
  return R(x0, y0, x1 - x0, y1 - y0);
05668b0d4753atagen2026-10-16 05:50 UTC502
}
05668b0d4753atagen2026-10-16 05:50 UTC503
function planBlocks(from, to, groups) {
05668b0d4753atagen2026-10-16 05:50 UTC504
  const bf = {}, bt = {};
05668b0d4753atagen2026-10-16 05:50 UTC505
  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 UTC506
  const bp = plan(bf, bt);
05668b0d4753atagen2026-10-16 05:50 UTC507
  const last = bp.keys.length - 1;
05668b0d4753atagen2026-10-16 05:50 UTC508
  const keys = bp.keys.map((K, ki) => {
05668b0d4753atagen2026-10-16 05:50 UTC509
    if (ki === 0) return from;
05668b0d4753atagen2026-10-16 05:50 UTC510
    if (ki === last) return to;
05668b0d4753atagen2026-10-16 05:50 UTC511
    const L = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC512
    for (const b in groups) for (const id of groups[b]) {
05668b0d4753atagen2026-10-16 05:50 UTC513
      const r = from[id], box = bf[b], k = K[b];
05668b0d4753atagen2026-10-16 05:50 UTC514
      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 UTC515
      const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC516
      L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h);
05668b0d4753atagen2026-10-16 05:50 UTC517
    }
05668b0d4753atagen2026-10-16 05:50 UTC518
    return L;
05668b0d4753atagen2026-10-16 05:50 UTC519
  });
05668b0d4753atagen2026-10-16 05:50 UTC520
  return { kind: bp.kind + ' on blocks', keys, durs: bp.durs };
05668b0d4753atagen2026-10-16 05:50 UTC521
}
05668b0d4753atagen2026-10-16 05:50 UTC522
05668b0d4753atagen2026-10-16 05:50 UTC523
function bezier(x1, y1, x2, y2) {
05668b0d4753atagen2026-10-16 05:50 UTC524
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
05668b0d4753atagen2026-10-16 05:50 UTC525
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
05668b0d4753atagen2026-10-16 05:50 UTC526
  const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u;
05668b0d4753atagen2026-10-16 05:50 UTC527
  const dX = u => (3 * ax * u + 2 * bx) * u + cx;
05668b0d4753atagen2026-10-16 05:50 UTC528
  return t => {
05668b0d4753atagen2026-10-16 05:50 UTC529
    if (t <= 0) return 0;
05668b0d4753atagen2026-10-16 05:50 UTC530
    if (t >= 1) return 1;
05668b0d4753atagen2026-10-16 05:50 UTC531
    let u = t;
05668b0d4753atagen2026-10-16 05:50 UTC532
    for (let i = 0; i < 8; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC533
      const x = X(u) - t;
05668b0d4753atagen2026-10-16 05:50 UTC534
      if (Math.abs(x) < 1e-6) return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC535
      const d = dX(u);
05668b0d4753atagen2026-10-16 05:50 UTC536
      if (Math.abs(d) < 1e-6) break;
05668b0d4753atagen2026-10-16 05:50 UTC537
      u -= x / d;
05668b0d4753atagen2026-10-16 05:50 UTC538
    }
05668b0d4753atagen2026-10-16 05:50 UTC539
    let lo = 0, hi = 1; u = t;
05668b0d4753atagen2026-10-16 05:50 UTC540
    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 UTC541
    return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC542
  };
05668b0d4753atagen2026-10-16 05:50 UTC543
}
05668b0d4753atagen2026-10-16 05:50 UTC544
const BASE = bezier(0.45, 0, 0.2, 1);
05668b0d4753atagen2026-10-16 05:50 UTC545
const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h];
05668b0d4753atagen2026-10-16 05:50 UTC546
</script>
05668b0d4753atagen2026-10-16 05:50 UTC547
05668b0d4753atagen2026-10-16 05:50 UTC548
<script>
944fc4782ac3atagen2026-10-16 08:09 UTC549
const PAL = { T: '#9aa3b5', A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' };
05668b0d4753atagen2026-10-16 05:50 UTC550
const demos = [];
05668b0d4753atagen2026-10-16 05:50 UTC551
05668b0d4753atagen2026-10-16 05:50 UTC552
class Demo {
05668b0d4753atagen2026-10-16 05:50 UTC553
  constructor(id, W, H, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC554
    this.W = W; this.H = H;
05668b0d4753atagen2026-10-16 05:50 UTC555
    this.outputs = opts.outputs || [R(0, 0, W, H)];
cd7085e54c88atagen2026-10-16 06:58 UTC556
    this.floating = new Set(opts.floating || []);
05668b0d4753atagen2026-10-16 05:50 UTC557
    const el = document.getElementById(id);
05668b0d4753atagen2026-10-16 05:50 UTC558
    this.canvas = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC559
    const dpr = window.devicePixelRatio || 1;
05668b0d4753atagen2026-10-16 05:50 UTC560
    this.canvas.width = W * dpr; this.canvas.height = H * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC561
    this.canvas.style.maxWidth = W + 'px';
05668b0d4753atagen2026-10-16 05:50 UTC562
    this.ctx = this.canvas.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC563
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC564
    el.appendChild(this.canvas);
05668b0d4753atagen2026-10-16 05:50 UTC565
    if (opts.graph) {
05668b0d4753atagen2026-10-16 05:50 UTC566
      this.graph = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC567
      this.graph.width = W * dpr; this.graph.height = 90 * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC568
      this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px';
05668b0d4753atagen2026-10-16 05:50 UTC569
      this.gctx = this.graph.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC570
      this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC571
      el.appendChild(this.graph);
05668b0d4753atagen2026-10-16 05:50 UTC572
      this.hist = [];
05668b0d4753atagen2026-10-16 05:50 UTC573
    }
05668b0d4753atagen2026-10-16 05:50 UTC574
    this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls);
05668b0d4753atagen2026-10-16 05:50 UTC575
    this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status);
05668b0d4753atagen2026-10-16 05:50 UTC576
    this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle';
05668b0d4753atagen2026-10-16 05:50 UTC577
    demos.push(this);
05668b0d4753atagen2026-10-16 05:50 UTC578
  }
05668b0d4753atagen2026-10-16 05:50 UTC579
  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 UTC580
  check(label, init, fn) {
05668b0d4753atagen2026-10-16 05:50 UTC581
    const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init;
05668b0d4753atagen2026-10-16 05:50 UTC582
    c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c;
05668b0d4753atagen2026-10-16 05:50 UTC583
  }
05668b0d4753atagen2026-10-16 05:50 UTC584
  set(L) { this.layout = clone(L); this.anim = null; }
05668b0d4753atagen2026-10-16 05:50 UTC585
  go(target, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC586
    const now = performance.now();
05668b0d4753atagen2026-10-16 05:50 UTC587
    const cur = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC588
    const from = {};
05668b0d4753atagen2026-10-16 05:50 UTC589
    for (const id in target) from[id] = cur[id] || target[id];
cd7085e54c88atagen2026-10-16 06:58 UTC590
    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 UTC591
    const curves = p.durs.map(() => BASE);
05668b0d4753atagen2026-10-16 05:50 UTC592
    const interrupted = !!this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC593
    let matched = false;
05668b0d4753atagen2026-10-16 05:50 UTC594
    if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) {
05668b0d4753atagen2026-10-16 05:50 UTC595
      const K1 = p.keys[1];
05668b0d4753atagen2026-10-16 05:50 UTC596
      let best = null, bs = 0;
05668b0d4753atagen2026-10-16 05:50 UTC597
      for (const id in K1) {
05668b0d4753atagen2026-10-16 05:50 UTC598
        const v = this.vel[id]; if (!v) continue;
05668b0d4753atagen2026-10-16 05:50 UTC599
        const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]);
05668b0d4753atagen2026-10-16 05:50 UTC600
        const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue;
05668b0d4753atagen2026-10-16 05:50 UTC601
        const sp = Math.hypot(...v);
05668b0d4753atagen2026-10-16 05:50 UTC602
        if (sp > bs) { bs = sp; best = { v, D, dd }; }
05668b0d4753atagen2026-10-16 05:50 UTC603
      }
05668b0d4753atagen2026-10-16 05:50 UTC604
      if (best) {
05668b0d4753atagen2026-10-16 05:50 UTC605
        const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd;
05668b0d4753atagen2026-10-16 05:50 UTC606
        const slope = s * p.durs[0];
05668b0d4753atagen2026-10-16 05:50 UTC607
        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 UTC608
      }
05668b0d4753atagen2026-10-16 05:50 UTC609
    }
05668b0d4753atagen2026-10-16 05:50 UTC610
    this.anim = { keys: p.keys, durs: p.durs, curves, start: now };
05668b0d4753atagen2026-10-16 05:50 UTC611
    this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : '');
05668b0d4753atagen2026-10-16 05:50 UTC612
    this.overlapSeen = false;
05668b0d4753atagen2026-10-16 05:50 UTC613
  }
05668b0d4753atagen2026-10-16 05:50 UTC614
  sampleAt(t) {
05668b0d4753atagen2026-10-16 05:50 UTC615
    if (!this.anim) return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC616
    let e = t - this.anim.start;
05668b0d4753atagen2026-10-16 05:50 UTC617
    const { keys, durs, curves } = this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC618
    for (let i = 0; i < durs.length; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC619
      const d = durs[i];
05668b0d4753atagen2026-10-16 05:50 UTC620
      if (e < d) {
05668b0d4753atagen2026-10-16 05:50 UTC621
        this.phase = i;
05668b0d4753atagen2026-10-16 05:50 UTC622
        const s = curves[i](e / d);
05668b0d4753atagen2026-10-16 05:50 UTC623
        const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC624
        for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s);
05668b0d4753atagen2026-10-16 05:50 UTC625
        return L;
05668b0d4753atagen2026-10-16 05:50 UTC626
      }
05668b0d4753atagen2026-10-16 05:50 UTC627
      e -= d;
05668b0d4753atagen2026-10-16 05:50 UTC628
    }
05668b0d4753atagen2026-10-16 05:50 UTC629
    this.layout = clone(keys[keys.length - 1]);
05668b0d4753atagen2026-10-16 05:50 UTC630
    this.anim = null;
05668b0d4753atagen2026-10-16 05:50 UTC631
    return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC632
  }
05668b0d4753atagen2026-10-16 05:50 UTC633
  frame(now) {
05668b0d4753atagen2026-10-16 05:50 UTC634
    const L = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC635
    if (this.prev) {
05668b0d4753atagen2026-10-16 05:50 UTC636
      const dt = now - this.prev.t;
05668b0d4753atagen2026-10-16 05:50 UTC637
      if (dt > 0) for (const id in L) if (this.prev.L[id]) {
05668b0d4753atagen2026-10-16 05:50 UTC638
        const a = edges(this.prev.L[id]), b = edges(L[id]);
05668b0d4753atagen2026-10-16 05:50 UTC639
        this.vel[id] = b.map((v, i) => (v - a[i]) / dt);
05668b0d4753atagen2026-10-16 05:50 UTC640
      }
05668b0d4753atagen2026-10-16 05:50 UTC641
    }
05668b0d4753atagen2026-10-16 05:50 UTC642
    this.prev = { t: now, L };
05668b0d4753atagen2026-10-16 05:50 UTC643
    this.draw(L);
05668b0d4753atagen2026-10-16 05:50 UTC644
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC645
    if (overlaps.length) this.overlapSeen = true;
05668b0d4753atagen2026-10-16 05:50 UTC646
    const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : '';
05668b0d4753atagen2026-10-16 05:50 UTC647
    this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : '');
05668b0d4753atagen2026-10-16 05:50 UTC648
    if (this.graph) this.drawGraph(now);
05668b0d4753atagen2026-10-16 05:50 UTC649
  }
05668b0d4753atagen2026-10-16 05:50 UTC650
  findOverlaps(L) {
cd7085e54c88atagen2026-10-16 06:58 UTC651
    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 UTC652
    const out = [];
05668b0d4753atagen2026-10-16 05:50 UTC653
    for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC654
      const a = L[ids[i]], b = L[ids[j]];
05668b0d4753atagen2026-10-16 05:50 UTC655
      const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
05668b0d4753atagen2026-10-16 05:50 UTC656
      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 UTC657
      if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0));
05668b0d4753atagen2026-10-16 05:50 UTC658
    }
05668b0d4753atagen2026-10-16 05:50 UTC659
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC660
  }
05668b0d4753atagen2026-10-16 05:50 UTC661
  draw(L) {
05668b0d4753atagen2026-10-16 05:50 UTC662
    const c = this.ctx;
05668b0d4753atagen2026-10-16 05:50 UTC663
    c.clearRect(0, 0, this.W, this.H);
05668b0d4753atagen2026-10-16 05:50 UTC664
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC665
    for (const o of this.outputs) {
05668b0d4753atagen2026-10-16 05:50 UTC666
      c.save();
05668b0d4753atagen2026-10-16 05:50 UTC667
      c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h);
05668b0d4753atagen2026-10-16 05:50 UTC668
      c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip();
cd7085e54c88atagen2026-10-16 06:58 UTC669
      for (const id of Object.keys(L).sort((a, b) => this.floating.has(a) - this.floating.has(b))) {
05668b0d4753atagen2026-10-16 05:50 UTC670
        const r = L[id];
05668b0d4753atagen2026-10-16 05:50 UTC671
        if (r.w < 0.5 || r.h < 0.5) continue;
05668b0d4753atagen2026-10-16 05:50 UTC672
        const rad = Math.min(8, r.w / 2, r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC673
        c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad);
05668b0d4753atagen2026-10-16 05:50 UTC674
        c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill();
05668b0d4753atagen2026-10-16 05:50 UTC675
        c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC676
        if (r.w > 22 && r.h > 22) {
05668b0d4753atagen2026-10-16 05:50 UTC677
          c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle';
05668b0d4753atagen2026-10-16 05:50 UTC678
          c.fillText(id, r.x + r.w / 2, r.y + r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC679
        }
05668b0d4753atagen2026-10-16 05:50 UTC680
      }
05668b0d4753atagen2026-10-16 05:50 UTC681
      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); }
944fc4782ac3atagen2026-10-16 08:09 UTC682
      if (this.overlay) for (const r of this.overlay()) { c.setLineDash([6, 4]); c.strokeStyle = '#8a909c'; c.lineWidth = 1.5; c.strokeRect(r.x, r.y, r.w, r.h); c.setLineDash([]); c.fillStyle = '#8a909c'; c.font = '13px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(r.id + ' (hidden)', r.x + r.w / 2, r.y + r.h / 2); }
05668b0d4753atagen2026-10-16 05:50 UTC683
      c.restore();
05668b0d4753atagen2026-10-16 05:50 UTC684
    }
05668b0d4753atagen2026-10-16 05:50 UTC685
  }
05668b0d4753atagen2026-10-16 05:50 UTC686
  drawGraph(now) {
05668b0d4753atagen2026-10-16 05:50 UTC687
    const v = this.vel.A;
05668b0d4753atagen2026-10-16 05:50 UTC688
    const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0;
05668b0d4753atagen2026-10-16 05:50 UTC689
    this.hist.push([now, sp]);
05668b0d4753atagen2026-10-16 05:50 UTC690
    while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift();
05668b0d4753atagen2026-10-16 05:50 UTC691
    const c = this.gctx, W = this.W, H = 90;
05668b0d4753atagen2026-10-16 05:50 UTC692
    c.clearRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC693
    c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC694
    const max = Math.max(400, ...this.hist.map(h => h[1]));
05668b0d4753atagen2026-10-16 05:50 UTC695
    c.beginPath();
05668b0d4753atagen2026-10-16 05:50 UTC696
    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 UTC697
    c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC698
    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 UTC699
  }
05668b0d4753atagen2026-10-16 05:50 UTC700
}
05668b0d4753atagen2026-10-16 05:50 UTC701
05668b0d4753atagen2026-10-16 05:50 UTC702
const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G);
05668b0d4753atagen2026-10-16 05:50 UTC703
05668b0d4753atagen2026-10-16 05:50 UTC704
{ // d1 swap
05668b0d4753atagen2026-10-16 05:50 UTC705
  const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC706
  let swapped = false;
05668b0d4753atagen2026-10-16 05:50 UTC707
  const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']);
05668b0d4753atagen2026-10-16 05:50 UTC708
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC709
  d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC710
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC711
}
05668b0d4753atagen2026-10-16 05:50 UTC712
05668b0d4753atagen2026-10-16 05:50 UTC713
{ // d2 one phase
05668b0d4753atagen2026-10-16 05:50 UTC714
  const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC715
  const s = { wide: false, d: false, e: false };
cd7085e54c88atagen2026-10-16 06:58 UTC716
  const modes = ['line on old boundary', 'point on old boundary', 'target centre'];
cd7085e54c88atagen2026-10-16 06:58 UTC717
  let mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC718
  const ratios = ids => ids.map(id => (id === 'A' && s.wide) ? 2 : 1);
05668b0d4753atagen2026-10-16 05:50 UTC719
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC720
    const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C'];
cd7085e54c88atagen2026-10-16 06:58 UTC721
    const out = splitX(I, ids, ratios(ids));
cd7085e54c88atagen2026-10-16 06:58 UTC722
    if (!s.d) {
cd7085e54c88atagen2026-10-16 06:58 UTC723
      const bx = (out.B.x + out.B.w + out.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC724
      const T = splitX(I, ['A', 'B', 'D', 'C'], ratios(['A', 'B', 'D', 'C'])).D;
cd7085e54c88atagen2026-10-16 06:58 UTC725
      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 UTC726
    }
05668b0d4753atagen2026-10-16 05:50 UTC727
    const b = out.B;
05668b0d4753atagen2026-10-16 05:50 UTC728
    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 UTC729
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC730
  };
05668b0d4753atagen2026-10-16 05:50 UTC731
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC732
  d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC733
  d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC734
  d.button('Split B with E', () => { s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC735
  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 UTC736
  const mb = d.button('seed: ' + modes[0], () => {
cd7085e54c88atagen2026-10-16 06:58 UTC737
    mode = (mode + 1) % modes.length; mb.textContent = 'seed: ' + modes[mode];
cd7085e54c88atagen2026-10-16 06:58 UTC738
    if (!s.d && !d.anim) d.set(L());
cd7085e54c88atagen2026-10-16 06:58 UTC739
  });
05668b0d4753atagen2026-10-16 05:50 UTC740
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC741
}
05668b0d4753atagen2026-10-16 05:50 UTC742
function tagLayout(I, view2, order) {
05668b0d4753atagen2026-10-16 05:50 UTC743
  const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC744
  if (view2) {
05668b0d4753atagen2026-10-16 05:50 UTC745
    const rows = splitY(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC746
    Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC747
  } else {
05668b0d4753atagen2026-10-16 05:50 UTC748
    Object.assign(L, splitX(I, ['A', 'B', 'C']));
05668b0d4753atagen2026-10-16 05:50 UTC749
    const y = order[0] === 't2' ? I.y : I.y + I.h;
05668b0d4753atagen2026-10-16 05:50 UTC750
    Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC751
  }
05668b0d4753atagen2026-10-16 05:50 UTC752
  return L;
05668b0d4753atagen2026-10-16 05:50 UTC753
}
05668b0d4753atagen2026-10-16 05:50 UTC754
05668b0d4753atagen2026-10-16 05:50 UTC755
{ // d3 tag toggle
05668b0d4753atagen2026-10-16 05:50 UTC756
  const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC757
  let view2 = false;
05668b0d4753atagen2026-10-16 05:50 UTC758
  d.set(tagLayout(I, view2, ['t1', 't2']));
05668b0d4753atagen2026-10-16 05:50 UTC759
  d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); });
05668b0d4753atagen2026-10-16 05:50 UTC760
}
05668b0d4753atagen2026-10-16 05:50 UTC761
05668b0d4753atagen2026-10-16 05:50 UTC762
{ // d3b tag reorder
05668b0d4753atagen2026-10-16 05:50 UTC763
  const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC764
  let order = ['t1', 't2'], blocks = true;
05668b0d4753atagen2026-10-16 05:50 UTC765
  d.set(tagLayout(I, true, order));
05668b0d4753atagen2026-10-16 05:50 UTC766
  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 UTC767
  d.check('rigid blocks', true, v => blocks = v);
05668b0d4753atagen2026-10-16 05:50 UTC768
}
05668b0d4753atagen2026-10-16 05:50 UTC769
05668b0d4753atagen2026-10-16 05:50 UTC770
{ // d4a orientation flip
05668b0d4753atagen2026-10-16 05:50 UTC771
  const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC772
  let rows = false;
05668b0d4753atagen2026-10-16 05:50 UTC773
  const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']);
05668b0d4753atagen2026-10-16 05:50 UTC774
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC775
  d.button('Flip orientation', () => { rows = !rows; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC776
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC777
}
05668b0d4753atagen2026-10-16 05:50 UTC778
05668b0d4753atagen2026-10-16 05:50 UTC779
{ // d4b grow both axes
05668b0d4753atagen2026-10-16 05:50 UTC780
  const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC781
  let wide = false;
05668b0d4753atagen2026-10-16 05:50 UTC782
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC783
    const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]);
05668b0d4753atagen2026-10-16 05:50 UTC784
    return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) };
05668b0d4753atagen2026-10-16 05:50 UTC785
  };
05668b0d4753atagen2026-10-16 05:50 UTC786
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC787
  d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC788
}
05668b0d4753atagen2026-10-16 05:50 UTC789
05668b0d4753atagen2026-10-16 05:50 UTC790
{ // d5 lanes
05668b0d4753atagen2026-10-16 05:50 UTC791
  const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC792
  let order = ['A', 'B', 'C', 'D'];
05668b0d4753atagen2026-10-16 05:50 UTC793
  const L = () => splitX(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC794
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC795
  d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC796
  d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC797
  d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC798
  d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC799
}
05668b0d4753atagen2026-10-16 05:50 UTC800
05668b0d4753atagen2026-10-16 05:50 UTC801
{ // d6 interrupts
05668b0d4753atagen2026-10-16 05:50 UTC802
  const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
aa790e7e33a0atagen2026-10-16 08:03 UTC803
  const names = ["A B C", "C A B", "A wide", "B C A", "close B"];
05668b0d4753atagen2026-10-16 05:50 UTC804
  const targets = [
05668b0d4753atagen2026-10-16 05:50 UTC805
    () => splitX(I, ['A', 'B', 'C']),
05668b0d4753atagen2026-10-16 05:50 UTC806
    () => splitX(I, ['C', 'A', 'B']),
05668b0d4753atagen2026-10-16 05:50 UTC807
    () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]),
05668b0d4753atagen2026-10-16 05:50 UTC808
    () => splitX(I, ['B', 'C', 'A']),
05668b0d4753atagen2026-10-16 05:50 UTC809
    () => ({ ...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 UTC810
  ];
05668b0d4753atagen2026-10-16 05:50 UTC811
  const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
05668b0d4753atagen2026-10-16 05:50 UTC812
  let idx = 0;
05668b0d4753atagen2026-10-16 05:50 UTC813
  d.set(withB(targets[0]()));
aa790e7e33a0atagen2026-10-16 08:03 UTC814
  const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); d.lastKind += ` · target "${names[idx]}"`; };
05668b0d4753atagen2026-10-16 05:50 UTC815
  d.button('Next target', next);
05668b0d4753atagen2026-10-16 05:50 UTC816
  d.button('Storm (8 rapid interrupts)', () => {
05668b0d4753atagen2026-10-16 05:50 UTC817
    let n = 0;
05668b0d4753atagen2026-10-16 05:50 UTC818
    const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); };
05668b0d4753atagen2026-10-16 05:50 UTC819
    fire();
05668b0d4753atagen2026-10-16 05:50 UTC820
  });
05668b0d4753atagen2026-10-16 05:50 UTC821
  d.check('match velocity', true, v => d.vmatch = v);
05668b0d4753atagen2026-10-16 05:50 UTC822
}
05668b0d4753atagen2026-10-16 05:50 UTC823
05668b0d4753atagen2026-10-16 05:50 UTC824
{ // d7 across outputs
05668b0d4753atagen2026-10-16 05:50 UTC825
  const W = 680, H = 260;
05668b0d4753atagen2026-10-16 05:50 UTC826
  const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H);
05668b0d4753atagen2026-10-16 05:50 UTC827
  const d = new Demo('d7', W, H, { outputs: [o1, o2] });
05668b0d4753atagen2026-10-16 05:50 UTC828
  const I1 = inner(320, H, 0), I2 = inner(320, H, 360);
05668b0d4753atagen2026-10-16 05:50 UTC829
  let s1 = ['A', 'B'], s2 = ['C'];
05668b0d4753atagen2026-10-16 05:50 UTC830
  const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) });
05668b0d4753atagen2026-10-16 05:50 UTC831
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC832
  d.button('Move B across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC833
    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 UTC834
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC835
  });
05668b0d4753atagen2026-10-16 05:50 UTC836
  d.button('Move A across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC837
    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 UTC838
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC839
  });
05668b0d4753atagen2026-10-16 05:50 UTC840
  d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC841
}
05668b0d4753atagen2026-10-16 05:50 UTC842
05668b0d4753atagen2026-10-16 05:50 UTC843
function layoutTree(node, rect, visible, out) {
05668b0d4753atagen2026-10-16 05:50 UTC844
  if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; }
05668b0d4753atagen2026-10-16 05:50 UTC845
  const horiz = node.dir === 'row';
05668b0d4753atagen2026-10-16 05:50 UTC846
  const shown = node.kids.map(k => anyVisible(k, visible));
05668b0d4753atagen2026-10-16 05:50 UTC847
  const m = shown.filter(Boolean).length;
05668b0d4753atagen2026-10-16 05:50 UTC848
  const len = horiz ? rect.w : rect.h;
05668b0d4753atagen2026-10-16 05:50 UTC849
  const avail = len - G * Math.max(0, m - 1);
05668b0d4753atagen2026-10-16 05:50 UTC850
  let pos = horiz ? rect.x : rect.y, seen = 0;
05668b0d4753atagen2026-10-16 05:50 UTC851
  node.kids.forEach((k, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC852
    if (!shown[i]) {
05668b0d4753atagen2026-10-16 05:50 UTC853
      const p = seen ? pos - G / 2 : pos;
05668b0d4753atagen2026-10-16 05:50 UTC854
      const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC855
      layoutTree(k, r, () => false, out);
05668b0d4753atagen2026-10-16 05:50 UTC856
      return;
05668b0d4753atagen2026-10-16 05:50 UTC857
    }
05668b0d4753atagen2026-10-16 05:50 UTC858
    const size = avail / m;
05668b0d4753atagen2026-10-16 05:50 UTC859
    const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size);
05668b0d4753atagen2026-10-16 05:50 UTC860
    layoutTree(k, r, visible, out);
05668b0d4753atagen2026-10-16 05:50 UTC861
    pos += size + G; seen++;
05668b0d4753atagen2026-10-16 05:50 UTC862
  });
05668b0d4753atagen2026-10-16 05:50 UTC863
}
05668b0d4753atagen2026-10-16 05:50 UTC864
function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); }
05668b0d4753atagen2026-10-16 05:50 UTC865
05668b0d4753atagen2026-10-16 05:50 UTC866
{ // d8 one tree, tags filter
05668b0d4753atagen2026-10-16 05:50 UTC867
  const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC868
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
05668b0d4753atagen2026-10-16 05:50 UTC869
  const view = { 1: true, 2: true };
05668b0d4753atagen2026-10-16 05:50 UTC870
  let mixed = false;
05668b0d4753atagen2026-10-16 05:50 UTC871
  const tree = () => mixed
05668b0d4753atagen2026-10-16 05:50 UTC872
    ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] }
05668b0d4753atagen2026-10-16 05:50 UTC873
    : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] };
05668b0d4753atagen2026-10-16 05:50 UTC874
  const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; };
05668b0d4753atagen2026-10-16 05:50 UTC875
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC876
  d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC877
  d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC878
  d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC879
}
05668b0d4753atagen2026-10-16 05:50 UTC880
cd7085e54c88atagen2026-10-16 06:58 UTC881
function stackLayouts(I) {
cd7085e54c88atagen2026-10-16 06:58 UTC882
  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 UTC883
  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 UTC884
  return {
cd7085e54c88atagen2026-10-16 06:58 UTC885
    'A | B | C': () => splitX(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC886
    'A | (B / C)': () => col(splitX(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC887
    '(A / C) | B': () => col(splitX(I, ['L', 'B']), 'L', ['A', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC888
    '(A / B) | C': () => col(splitX(I, ['L', 'C']), 'L', ['A', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC889
    'B | C | A': () => splitX(I, ['B', 'C', 'A']),
cd7085e54c88atagen2026-10-16 06:58 UTC890
    'A / B / C': () => splitY(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC891
    'A / (B | C)': () => row(splitY(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC892
    '(A / B) | (C / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'B']), 'R', ['C', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC893
    'A | (C / B / D)': () => col(splitX(I, ['A', 'R']), 'R', ['C', 'B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC894
    '(A / D) | (C / B)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'D']), 'R', ['C', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC895
    '(A / B / C) | D': () => col(splitX(I, ['L', 'D']), 'L', ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC896
    '(A / C) | (B / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'C']), 'R', ['B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC897
  };
cd7085e54c88atagen2026-10-16 06:58 UTC898
}
cd7085e54c88atagen2026-10-16 06:58 UTC899
cd7085e54c88atagen2026-10-16 06:58 UTC900
for (const [id, names] of [
cd7085e54c88atagen2026-10-16 06:58 UTC901
  ['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 UTC902
  ['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 UTC903
]) {
cd7085e54c88atagen2026-10-16 06:58 UTC904
  const W = 640, H = 300, d = new Demo(id, W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC905
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC906
  for (const n of names) d.button(n, () => d.go(S[n]()));
cd7085e54c88atagen2026-10-16 06:58 UTC907
}
cd7085e54c88atagen2026-10-16 06:58 UTC908
cd7085e54c88atagen2026-10-16 06:58 UTC909
{ // d9c grid
cd7085e54c88atagen2026-10-16 06:58 UTC910
  const W = 640, H = 300, d = new Demo('d9c', W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC911
  let at = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC912
  const names = ['(A / D) | (C / B)', '(A / B / C) | D'];
cd7085e54c88atagen2026-10-16 06:58 UTC913
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC914
  d.button('(A / D) | (C / B)  ⇄  (A / B / C) | D', () => { at = 1 - at; d.go(S[names[at]]()); });
cd7085e54c88atagen2026-10-16 06:58 UTC915
}
cd7085e54c88atagen2026-10-16 06:58 UTC916
cd7085e54c88atagen2026-10-16 06:58 UTC917
{ // d10 floating to tiled
cd7085e54c88atagen2026-10-16 06:58 UTC918
  const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H);
cd7085e54c88atagen2026-10-16 06:58 UTC919
  const FL = R(220, 40, 220, 130);
944fc4782ac3atagen2026-10-16 08:09 UTC920
  const modes = ['direct glide', 'slot opens last', 'slot opens first'];
aa790e7e33a0atagen2026-10-16 08:03 UTC921
  let tiled = false, mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC922
  const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']);
cd7085e54c88atagen2026-10-16 06:58 UTC923
  const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL });
cd7085e54c88atagen2026-10-16 06:58 UTC924
  const ortho = () => STYLE === 'orthogonal';
cd7085e54c88atagen2026-10-16 06:58 UTC925
  const assemble = (n, tiledAt, fAt, kind) => {
cd7085e54c88atagen2026-10-16 06:58 UTC926
    const keys = [];
cd7085e54c88atagen2026-10-16 06:58 UTC927
    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 UTC928
    return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) };
cd7085e54c88atagen2026-10-16 06:58 UTC929
  };
aa790e7e33a0atagen2026-10-16 08:03 UTC930
  const slotPlan = (from, to, opening) => {
aa790e7e33a0atagen2026-10-16 08:03 UTC931
    const fromBx = (from.B.x + from.B.w + from.C.x) / 2, toBx = (to.B.x + to.B.w + to.C.x) / 2;
aa790e7e33a0atagen2026-10-16 08:03 UTC932
    return opening
aa790e7e33a0atagen2026-10-16 08:03 UTC933
      ? plan({ A: from.A, B: from.B, C: from.C, P: R(fromBx, I.y, 0, I.h) }, { A: to.A, B: to.B, C: to.C, P: to.F })
aa790e7e33a0atagen2026-10-16 08:03 UTC934
      : plan({ A: from.A, B: from.B, C: from.C, P: from.F }, { A: to.A, B: to.B, C: to.C, P: R(toBx, I.y, 0, I.h) });
aa790e7e33a0atagen2026-10-16 08:03 UTC935
  };
aa790e7e33a0atagen2026-10-16 08:03 UTC936
  const at = (tp, i) => tp.keys[Math.max(0, Math.min(i, tp.durs.length))];
cd7085e54c88atagen2026-10-16 06:58 UTC937
  const floatIn = from => {
aa790e7e33a0atagen2026-10-16 08:03 UTC938
    const to = tiledL(), slot = to.F, tp = slotPlan(from, to, true), k = tp.durs.length, f = from.F;
aa790e7e33a0atagen2026-10-16 08:03 UTC939
    if (modes[mode] === 'direct glide') {
aa790e7e33a0atagen2026-10-16 08:03 UTC940
      const n = Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC941
      return assemble(n, i => at(tp, i), i => i === 0 ? f : slot, `glide in, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC942
    }
cd7085e54c88atagen2026-10-16 06:58 UTC943
    const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC944
    const path = ortho() ? [f, R(cx, f.y, f.w, f.h), R(cx, cy, f.w, f.h)] : [f, R(cx, cy, f.w, f.h)];
aa790e7e33a0atagen2026-10-16 08:03 UTC945
    const travel = path.length - 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC946
    if (modes[mode] === 'slot opens last') {
aa790e7e33a0atagen2026-10-16 08:03 UTC947
      const n = travel + Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC948
      return assemble(n, i => at(tp, i - (n - k)), i => i === n ? slot : path[Math.min(i, travel)], `float in, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC949
    }
aa790e7e33a0atagen2026-10-16 08:03 UTC950
    const n = Math.max(travel, k) + 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC951
    return assemble(n, i => at(tp, i), i => i === n ? slot : path[Math.min(i, travel)], `float in, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC952
  };
cd7085e54c88atagen2026-10-16 06:58 UTC953
  const floatOut = from => {
aa790e7e33a0atagen2026-10-16 08:03 UTC954
    const to = floatL(), slot = from.F, tp = slotPlan(from, to, false), k = tp.durs.length;
aa790e7e33a0atagen2026-10-16 08:03 UTC955
    if (modes[mode] === 'direct glide') {
aa790e7e33a0atagen2026-10-16 08:03 UTC956
      const n = Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC957
      return assemble(n, i => at(tp, i), i => i === 0 ? slot : FL, `glide out, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC958
    }
cd7085e54c88atagen2026-10-16 06:58 UTC959
    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 UTC960
    const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL];
aa790e7e33a0atagen2026-10-16 08:03 UTC961
    const travel = path.length - 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC962
    if (modes[mode] === 'slot opens last') {
aa790e7e33a0atagen2026-10-16 08:03 UTC963
      const n = Math.max(travel, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC964
      return assemble(n, i => at(tp, i), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC965
    }
aa790e7e33a0atagen2026-10-16 08:03 UTC966
    const n = Math.max(travel, k + 1);
aa790e7e33a0atagen2026-10-16 08:03 UTC967
    return assemble(n, i => at(tp, i - 1), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC968
  };
cd7085e54c88atagen2026-10-16 06:58 UTC969
  d.set(floatL());
cd7085e54c88atagen2026-10-16 06:58 UTC970
  d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); });
aa790e7e33a0atagen2026-10-16 08:03 UTC971
  const mb = d.button('mode: ' + modes[0], () => { mode = (mode + 1) % modes.length; mb.textContent = 'mode: ' + modes[mode]; });
cd7085e54c88atagen2026-10-16 06:58 UTC972
}
cd7085e54c88atagen2026-10-16 06:58 UTC973
944fc4782ac3atagen2026-10-16 08:09 UTC974
{ // d11 one tree, hidden slots
944fc4782ac3atagen2026-10-16 08:09 UTC975
  const W = 640, H = 320, d = new Demo('d11', W, H), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC976
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
944fc4782ac3atagen2026-10-16 08:09 UTC977
  const view = { 1: true, 2: true };
944fc4782ac3atagen2026-10-16 08:09 UTC978
  let tree, ghosts = false;
944fc4782ac3atagen2026-10-16 08:09 UTC979
  const reset = () => { tree = { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'D', 'B'] }, { dir: 'row', kids: ['C', 'E'] }] }; view[2] = true; };
944fc4782ac3atagen2026-10-16 08:09 UTC980
  reset();
944fc4782ac3atagen2026-10-16 08:09 UTC981
  const L = () => { const o = {}; layoutTree(tree, I, id => view[tagOf[id]], o); return o; };
944fc4782ac3atagen2026-10-16 08:09 UTC982
  const parentOf = (node, id, par = null) => {
944fc4782ac3atagen2026-10-16 08:09 UTC983
    if (typeof node === 'string') return node === id ? par : null;
944fc4782ac3atagen2026-10-16 08:09 UTC984
    for (const k of node.kids) { const r = parentOf(k, id, node); if (r) return r; }
944fc4782ac3atagen2026-10-16 08:09 UTC985
    return null;
944fc4782ac3atagen2026-10-16 08:09 UTC986
  };
944fc4782ac3atagen2026-10-16 08:09 UTC987
  const collapse = node => {
944fc4782ac3atagen2026-10-16 08:09 UTC988
    if (typeof node === 'string') return node;
944fc4782ac3atagen2026-10-16 08:09 UTC989
    node.kids = node.kids.map(collapse).filter(k => typeof k === 'string' || k.kids.length);
944fc4782ac3atagen2026-10-16 08:09 UTC990
    return node.kids.length === 1 ? node.kids[0] : node;
944fc4782ac3atagen2026-10-16 08:09 UTC991
  };
944fc4782ac3atagen2026-10-16 08:09 UTC992
  const swap = (a, b) => { const pa = parentOf(tree, a), pb = parentOf(tree, b); const ia = pa.kids.indexOf(a), ib = pb.kids.indexOf(b); pa.kids[ia] = b; pb.kids[ib] = a; };
944fc4782ac3atagen2026-10-16 08:09 UTC993
  const moveAfter = (id, target) => {
944fc4782ac3atagen2026-10-16 08:09 UTC994
    const p = parentOf(tree, id); p.kids.splice(p.kids.indexOf(id), 1);
944fc4782ac3atagen2026-10-16 08:09 UTC995
    tree = collapse(tree);
944fc4782ac3atagen2026-10-16 08:09 UTC996
    const pt = parentOf(tree, target); pt.kids.splice(pt.kids.indexOf(target) + 1, 0, id);
944fc4782ac3atagen2026-10-16 08:09 UTC997
  };
944fc4782ac3atagen2026-10-16 08:09 UTC998
  d.overlay = () => {
944fc4782ac3atagen2026-10-16 08:09 UTC999
    if (!ghosts) return [];
944fc4782ac3atagen2026-10-16 08:09 UTC1000
    const full = {}; layoutTree(tree, I, () => true, full);
944fc4782ac3atagen2026-10-16 08:09 UTC1001
    return Object.keys(full).filter(id => !view[tagOf[id]]).map(id => ({ ...full[id], id }));
944fc4782ac3atagen2026-10-16 08:09 UTC1002
  };
944fc4782ac3atagen2026-10-16 08:09 UTC1003
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1004
  d.button('Hide / show tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1005
  d.button('Swap A and B', () => { swap('A', 'B'); d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1006
  d.button('Move C next to B', () => { if (parentOf(tree, 'C') !== parentOf(tree, 'B')) { moveAfter('C', 'B'); d.go(L()); } });
944fc4782ac3atagen2026-10-16 08:09 UTC1007
  d.button('Reset', () => { reset(); d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1008
  d.check('show hidden slots', false, v => ghosts = v);
944fc4782ac3atagen2026-10-16 08:09 UTC1009
}
944fc4782ac3atagen2026-10-16 08:09 UTC1010
944fc4782ac3atagen2026-10-16 08:09 UTC1011
{ // d12 lane extent
944fc4782ac3atagen2026-10-16 08:09 UTC1012
  const W = 640, H = 320, d = new Demo('d12', W, H), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC1013
  let order = ['A', 'B', 'C'], whole = false;
944fc4782ac3atagen2026-10-16 08:09 UTC1014
  const L = () => { const r = splitY(I, ['top', 'bot'], [7, 3]); return { T: r.top, ...splitX(r.bot, order) }; };
944fc4782ac3atagen2026-10-16 08:09 UTC1015
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1016
  d.button('Reverse bottom row', () => {
944fc4782ac3atagen2026-10-16 08:09 UTC1017
    order = [...order].reverse();
944fc4782ac3atagen2026-10-16 08:09 UTC1018
    const tgt = L();
944fc4782ac3atagen2026-10-16 08:09 UTC1019
    d.go(tgt, { planFn: from => { LANE_REGION = whole ? I : null; try { return plan(from, tgt); } finally { LANE_REGION = null; } } });
944fc4782ac3atagen2026-10-16 08:09 UTC1020
  });
944fc4782ac3atagen2026-10-16 08:09 UTC1021
  const mb = d.button('lanes span: involved band', () => { whole = !whole; mb.textContent = 'lanes span: ' + (whole ? 'whole output' : 'involved band'); });
944fc4782ac3atagen2026-10-16 08:09 UTC1022
}
944fc4782ac3atagen2026-10-16 08:09 UTC1023
944fc4782ac3atagen2026-10-16 08:09 UTC1024
{ // d13 fullscreen
944fc4782ac3atagen2026-10-16 08:09 UTC1025
  const W = 640, H = 300, d = new Demo('d13', W, H, { floating: ['B'] }), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC1026
  let full = false;
944fc4782ac3atagen2026-10-16 08:09 UTC1027
  const L = () => { const o = splitX(I, ['A', 'B', 'C']); if (full) o.B = R(0, 0, W, H); return o; };
944fc4782ac3atagen2026-10-16 08:09 UTC1028
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1029
  d.button('Fullscreen B', () => {
944fc4782ac3atagen2026-10-16 08:09 UTC1030
    full = !full;
944fc4782ac3atagen2026-10-16 08:09 UTC1031
    const tgt = L();
944fc4782ac3atagen2026-10-16 08:09 UTC1032
    d.go(tgt, { planFn: from => ({ kind: full ? 'glide to fullscreen' : 'glide back to slot', keys: [from, tgt], durs: [PHASE_MS] }) });
944fc4782ac3atagen2026-10-16 08:09 UTC1033
  });
944fc4782ac3atagen2026-10-16 08:09 UTC1034
}
944fc4782ac3atagen2026-10-16 08:09 UTC1035
944fc4782ac3atagen2026-10-16 08:09 UTC1036
{ // d14 expand
944fc4782ac3atagen2026-10-16 08:09 UTC1037
  const W = 640, H = 300, d = new Demo('d14', W, H), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC1038
  let col = false, root = false;
944fc4782ac3atagen2026-10-16 08:09 UTC1039
  const L = () => {
944fc4782ac3atagen2026-10-16 08:09 UTC1040
    const cols = splitX(I, ['A', 'R']);
944fc4782ac3atagen2026-10-16 08:09 UTC1041
    const area = root ? I : cols.R;
944fc4782ac3atagen2026-10-16 08:09 UTC1042
    const o = root ? { A: R(I.x, I.y, 0, I.h) } : { A: cols.A };
944fc4782ac3atagen2026-10-16 08:09 UTC1043
    if (col) Object.assign(o, { B: R(area.x, area.y, area.w, 0), C: area, D: R(area.x, area.y + area.h, area.w, 0) });
944fc4782ac3atagen2026-10-16 08:09 UTC1044
    else Object.assign(o, splitY(area, ['B', 'C', 'D']));
944fc4782ac3atagen2026-10-16 08:09 UTC1045
    return o;
944fc4782ac3atagen2026-10-16 08:09 UTC1046
  };
944fc4782ac3atagen2026-10-16 08:09 UTC1047
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1048
  d.button('Expand C in its column', () => { col = !col; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1049
  d.button('Expand the column to the output', () => { root = !root; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1050
  d.button('Expand C all the way', () => { const on = !(col && root); col = on; root = on; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1051
}
944fc4782ac3atagen2026-10-16 08:09 UTC1052
05668b0d4753atagen2026-10-16 05:50 UTC1053
document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };
05668b0d4753atagen2026-10-16 05:50 UTC1054
document.getElementById('style').onchange = e => { STYLE = e.target.value; };
05668b0d4753atagen2026-10-16 05:50 UTC1055
const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); };
05668b0d4753atagen2026-10-16 05:50 UTC1056
requestAnimationFrame(loop);
05668b0d4753atagen2026-10-16 05:50 UTC1057
</script>
05668b0d4753atagen2026-10-16 05:50 UTC1058
</body>
05668b0d4753atagen2026-10-16 05:50 UTC1059
</html>