atagen

monolith

public
Star0
Watch0
17d5669ab74b89f77787299c0313e001083b75d6
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>
061b977757d2atagen2026-10-16 08:22 UTC40
<h1>monolith planner, draft 3</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>
061b977757d2atagen2026-10-16 08:22 UTC243
<p>Nothing. Lanes span the involved band, and the hidden-slot rules stand.</p>
944fc4782ac3atagen2026-10-16 08:09 UTC244
05668b0d4753atagen2026-10-16 05:50 UTC245
</main>
05668b0d4753atagen2026-10-16 05:50 UTC246
05668b0d4753atagen2026-10-16 05:50 UTC247
<script id="planner">
05668b0d4753atagen2026-10-16 05:50 UTC248
const G = 10;
05668b0d4753atagen2026-10-16 05:50 UTC249
const EPS = 0.5;
05668b0d4753atagen2026-10-16 05:50 UTC250
let PHASE_MS = 450;
cd7085e54c88atagen2026-10-16 06:58 UTC251
let STYLE = 'orthogonal';
944fc4782ac3atagen2026-10-16 08:09 UTC252
let LANE_REGION = null;
05668b0d4753atagen2026-10-16 05:50 UTC253
05668b0d4753atagen2026-10-16 05:50 UTC254
const R = (x, y, w, h) => ({ x, y, w, h });
05668b0d4753atagen2026-10-16 05:50 UTC255
const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; };
05668b0d4753atagen2026-10-16 05:50 UTC256
const eq = (a, b) => Math.abs(a - b) < 0.05;
05668b0d4753atagen2026-10-16 05:50 UTC257
const lerp = (a, b, s) => a + (b - a) * s;
05668b0d4753atagen2026-10-16 05:50 UTC258
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 UTC259
05668b0d4753atagen2026-10-16 05:50 UTC260
function splitX(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC261
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC262
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC263
  const avail = rect.w - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC264
  let x = rect.x;
05668b0d4753atagen2026-10-16 05:50 UTC265
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC266
  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 UTC267
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC268
}
05668b0d4753atagen2026-10-16 05:50 UTC269
function splitY(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC270
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC271
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC272
  const avail = rect.h - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC273
  let y = rect.y;
05668b0d4753atagen2026-10-16 05:50 UTC274
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC275
  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 UTC276
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC277
}
05668b0d4753atagen2026-10-16 05:50 UTC278
05668b0d4753atagen2026-10-16 05:50 UTC279
function relBits(a, b) {
05668b0d4753atagen2026-10-16 05:50 UTC280
  let s = 0;
05668b0d4753atagen2026-10-16 05:50 UTC281
  if (a.x + a.w <= b.x + EPS) s |= 1;
05668b0d4753atagen2026-10-16 05:50 UTC282
  if (b.x + b.w <= a.x + EPS) s |= 2;
05668b0d4753atagen2026-10-16 05:50 UTC283
  if (a.y + a.h <= b.y + EPS) s |= 4;
05668b0d4753atagen2026-10-16 05:50 UTC284
  if (b.y + b.h <= a.y + EPS) s |= 8;
05668b0d4753atagen2026-10-16 05:50 UTC285
  return s;
05668b0d4753atagen2026-10-16 05:50 UTC286
}
05668b0d4753atagen2026-10-16 05:50 UTC287
function phaseOk(K1, K2, list) {
05668b0d4753atagen2026-10-16 05:50 UTC288
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC289
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC290
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC291
      if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false;
05668b0d4753atagen2026-10-16 05:50 UTC292
    }
05668b0d4753atagen2026-10-16 05:50 UTC293
  return true;
05668b0d4753atagen2026-10-16 05:50 UTC294
}
05668b0d4753atagen2026-10-16 05:50 UTC295
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 UTC296
function pureOk(keys) {
cd7085e54c88atagen2026-10-16 06:58 UTC297
  if (STYLE !== 'orthogonal') return true;
cd7085e54c88atagen2026-10-16 06:58 UTC298
  for (let k = 0; k + 1 < keys.length; k++)
cd7085e54c88atagen2026-10-16 06:58 UTC299
    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 UTC300
  return true;
cd7085e54c88atagen2026-10-16 06:58 UTC301
}
05668b0d4753atagen2026-10-16 05:50 UTC302
440b67836dd5atagen2026-10-16 07:57 UTC303
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 UTC304
05668b0d4753atagen2026-10-16 05:50 UTC305
function plan(from, to, opts = {}) {
440b67836dd5atagen2026-10-16 07:57 UTC306
  if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [PHASE_MS] };
440b67836dd5atagen2026-10-16 07:57 UTC307
  if (signature(from) <= signature(to)) return planOneWay(from, to);
440b67836dd5atagen2026-10-16 07:57 UTC308
  const p = planOneWay(to, from);
440b67836dd5atagen2026-10-16 07:57 UTC309
  return { kind: p.kind, keys: [...p.keys].reverse(), durs: [...p.durs].reverse() };
440b67836dd5atagen2026-10-16 07:57 UTC310
}
440b67836dd5atagen2026-10-16 07:57 UTC311
440b67836dd5atagen2026-10-16 07:57 UTC312
function planOneWay(from, to) {
05668b0d4753atagen2026-10-16 05:50 UTC313
  const list = Object.keys(to);
05668b0d4753atagen2026-10-16 05:50 UTC314
  const T = PHASE_MS;
05668b0d4753atagen2026-10-16 05:50 UTC315
  const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; });
05668b0d4753atagen2026-10-16 05:50 UTC316
  if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] };
05668b0d4753atagen2026-10-16 05:50 UTC317
  const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); });
05668b0d4753atagen2026-10-16 05:50 UTC318
  if (phaseOk(from, to, list) && (STYLE === 'direct' || pure))
05668b0d4753atagen2026-10-16 05:50 UTC319
    return { kind: '1 phase', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC320
  const two = axisOrder(from, to, list, movers);
05668b0d4753atagen2026-10-16 05:50 UTC321
  if (two) return two;
05668b0d4753atagen2026-10-16 05:50 UTC322
  for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; }
cd7085e54c88atagen2026-10-16 06:58 UTC323
  const g = grid(from, to, list, movers);
cd7085e54c88atagen2026-10-16 06:58 UTC324
  if (g) return g;
05668b0d4753atagen2026-10-16 05:50 UTC325
  return pinch(from, to, movers);
05668b0d4753atagen2026-10-16 05:50 UTC326
}
05668b0d4753atagen2026-10-16 05:50 UTC327
cd7085e54c88atagen2026-10-16 06:58 UTC328
function conflictSet(from, to, list) {
cd7085e54c88atagen2026-10-16 06:58 UTC329
  const out = new Set();
cd7085e54c88atagen2026-10-16 06:58 UTC330
  for (let i = 0; i < list.length; i++)
cd7085e54c88atagen2026-10-16 06:58 UTC331
    for (let j = i + 1; j < list.length; j++) {
cd7085e54c88atagen2026-10-16 06:58 UTC332
      const a = list[i], b = list[j];
cd7085e54c88atagen2026-10-16 06:58 UTC333
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { out.add(a); out.add(b); }
cd7085e54c88atagen2026-10-16 06:58 UTC334
    }
cd7085e54c88atagen2026-10-16 06:58 UTC335
  return [...out];
cd7085e54c88atagen2026-10-16 06:58 UTC336
}
cd7085e54c88atagen2026-10-16 06:58 UTC337
cd7085e54c88atagen2026-10-16 06:58 UTC338
function grid(from, to, list, movers) {
cd7085e54c88atagen2026-10-16 06:58 UTC339
  const cx = r => r.x + r.w / 2, cy = r => r.y + r.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC340
  for (const set of [conflictSet(from, to, list), movers, list]) {
cd7085e54c88atagen2026-10-16 06:58 UTC341
    const n = set.length;
cd7085e54c88atagen2026-10-16 06:58 UTC342
    if (n < 2) continue;
cd7085e54c88atagen2026-10-16 06:58 UTC343
    let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
cd7085e54c88atagen2026-10-16 06:58 UTC344
    for (const id of set) for (const r of [from[id], to[id]]) {
cd7085e54c88atagen2026-10-16 06:58 UTC345
      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 UTC346
    }
cd7085e54c88atagen2026-10-16 06:58 UTC347
    const sh = (y1 - y0 - (n - 1) * G) / n, sw = (x1 - x0 - (n - 1) * G) / n;
cd7085e54c88atagen2026-10-16 06:58 UTC348
    const rowOf = {}, colOf = {};
cd7085e54c88atagen2026-10-16 06:58 UTC349
    [...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 UTC350
    [...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 UTC351
    const inSet = new Set(set);
cd7085e54c88atagen2026-10-16 06:58 UTC352
    for (const phase of [3, 2, 4, 1]) {
cd7085e54c88atagen2026-10-16 06:58 UTC353
      const K = [{}, {}, {}];
cd7085e54c88atagen2026-10-16 06:58 UTC354
      for (const id of list) {
cd7085e54c88atagen2026-10-16 06:58 UTC355
        const f = from[id], t = to[id];
cd7085e54c88atagen2026-10-16 06:58 UTC356
        if (inSet.has(id)) {
cd7085e54c88atagen2026-10-16 06:58 UTC357
          K[0][id] = R(f.x, rowOf[id], f.w, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC358
          K[1][id] = R(colOf[id], rowOf[id], sw, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC359
          K[2][id] = R(colOf[id], t.y, sw, t.h);
cd7085e54c88atagen2026-10-16 06:58 UTC360
        } else for (let k = 0; k < 3; k++) K[k][id] = k + 1 >= phase ? t : f;
cd7085e54c88atagen2026-10-16 06:58 UTC361
      }
cd7085e54c88atagen2026-10-16 06:58 UTC362
      const keys = [from, ...K, to];
cd7085e54c88atagen2026-10-16 06:58 UTC363
      let ok = true;
cd7085e54c88atagen2026-10-16 06:58 UTC364
      for (let k = 0; k < 4 && ok; k++) ok = phaseOk(keys[k], keys[k + 1], list);
cd7085e54c88atagen2026-10-16 06:58 UTC365
      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 UTC366
    }
cd7085e54c88atagen2026-10-16 06:58 UTC367
  }
cd7085e54c88atagen2026-10-16 06:58 UTC368
  return null;
cd7085e54c88atagen2026-10-16 06:58 UTC369
}
cd7085e54c88atagen2026-10-16 06:58 UTC370
05668b0d4753atagen2026-10-16 05:50 UTC371
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 UTC372
440b67836dd5atagen2026-10-16 07:57 UTC373
function adjBits(a, b) {
440b67836dd5atagen2026-10-16 07:57 UTC374
  const ovX = a.x < b.x + b.w + EPS && b.x < a.x + a.w + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC375
  const ovY = a.y < b.y + b.h + EPS && b.y < a.y + a.h + EPS;
440b67836dd5atagen2026-10-16 07:57 UTC376
  let s = 0;
440b67836dd5atagen2026-10-16 07:57 UTC377
  if (ovY && Math.abs(b.x - (a.x + a.w)) <= G + EPS) s |= 1;
440b67836dd5atagen2026-10-16 07:57 UTC378
  if (ovY && Math.abs(a.x - (b.x + b.w)) <= G + EPS) s |= 2;
440b67836dd5atagen2026-10-16 07:57 UTC379
  if (ovX && Math.abs(b.y - (a.y + a.h)) <= G + EPS) s |= 4;
440b67836dd5atagen2026-10-16 07:57 UTC380
  if (ovX && Math.abs(a.y - (b.y + b.h)) <= G + EPS) s |= 8;
440b67836dd5atagen2026-10-16 07:57 UTC381
  return s;
440b67836dd5atagen2026-10-16 07:57 UTC382
}
440b67836dd5atagen2026-10-16 07:57 UTC383
function coupledEdges(from, to, list) {
440b67836dd5atagen2026-10-16 07:57 UTC384
  const out = [];
440b67836dd5atagen2026-10-16 07:57 UTC385
  for (let i = 0; i < list.length; i++)
440b67836dd5atagen2026-10-16 07:57 UTC386
    for (let j = i + 1; j < list.length; j++) {
440b67836dd5atagen2026-10-16 07:57 UTC387
      const a = list[i], b = list[j], s = adjBits(from[a], from[b]) & adjBits(to[a], to[b]);
440b67836dd5atagen2026-10-16 07:57 UTC388
      if (s) out.push([a, b, s]);
440b67836dd5atagen2026-10-16 07:57 UTC389
    }
440b67836dd5atagen2026-10-16 07:57 UTC390
  return out;
440b67836dd5atagen2026-10-16 07:57 UTC391
}
440b67836dd5atagen2026-10-16 07:57 UTC392
function holes(K, coupled) {
440b67836dd5atagen2026-10-16 07:57 UTC393
  let n = 0;
440b67836dd5atagen2026-10-16 07:57 UTC394
  for (const [a, b, s] of coupled) if ((adjBits(K[a], K[b]) & s) !== s) n++;
440b67836dd5atagen2026-10-16 07:57 UTC395
  return n;
440b67836dd5atagen2026-10-16 07:57 UTC396
}
440b67836dd5atagen2026-10-16 07:57 UTC397
05668b0d4753atagen2026-10-16 05:50 UTC398
function axisOrder(from, to, list, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC399
  const n = movers.length;
05668b0d4753atagen2026-10-16 05:50 UTC400
  if (n > 14) return null;
440b67836dd5atagen2026-10-16 07:57 UTC401
  const coupled = coupledEdges(from, to, list);
05668b0d4753atagen2026-10-16 05:50 UTC402
  let best = null, bestScore = Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC403
  for (let mask = 0; mask < (1 << n); mask++) {
05668b0d4753atagen2026-10-16 05:50 UTC404
    const K1 = { ...from };
440b67836dd5atagen2026-10-16 07:57 UTC405
    let late = 0;
05668b0d4753atagen2026-10-16 05:50 UTC406
    movers.forEach((id, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC407
      const xf = !!(mask & (1 << i));
05668b0d4753atagen2026-10-16 05:50 UTC408
      const c = chg(from[id], to[id]);
05668b0d4753atagen2026-10-16 05:50 UTC409
      K1[id] = mixed(from[id], to[id], xf);
440b67836dd5atagen2026-10-16 07:57 UTC410
      if (c.x && !c.y && !xf) late++;
440b67836dd5atagen2026-10-16 07:57 UTC411
      if (c.y && !c.x && xf) late++;
05668b0d4753atagen2026-10-16 05:50 UTC412
    });
440b67836dd5atagen2026-10-16 07:57 UTC413
    if (late >= bestScore) continue;
440b67836dd5atagen2026-10-16 07:57 UTC414
    if (!phaseOk(from, K1, list) || !phaseOk(K1, to, list)) continue;
440b67836dd5atagen2026-10-16 07:57 UTC415
    const score = holes(K1, coupled) * 1000 + late;
440b67836dd5atagen2026-10-16 07:57 UTC416
    if (score < bestScore) { best = K1; bestScore = score; if (score === 0) break; }
05668b0d4753atagen2026-10-16 05:50 UTC417
  }
440b67836dd5atagen2026-10-16 07:57 UTC418
  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 UTC419
}
05668b0d4753atagen2026-10-16 05:50 UTC420
05668b0d4753atagen2026-10-16 05:50 UTC421
function lanes(from, to, list, movers, ax) {
05668b0d4753atagen2026-10-16 05:50 UTC422
  const size = ax === 'y' ? 'h' : 'w';
05668b0d4753atagen2026-10-16 05:50 UTC423
  const bitsT = ax === 'y' ? 3 : 12;
05668b0d4753atagen2026-10-16 05:50 UTC424
  const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2;
05668b0d4753atagen2026-10-16 05:50 UTC425
  const conflict = new Set();
05668b0d4753atagen2026-10-16 05:50 UTC426
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC427
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC428
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC429
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); }
05668b0d4753atagen2026-10-16 05:50 UTC430
    }
05668b0d4753atagen2026-10-16 05:50 UTC431
  const sets = [[...conflict], movers, list];
05668b0d4753atagen2026-10-16 05:50 UTC432
  for (const set of sets) {
05668b0d4753atagen2026-10-16 05:50 UTC433
    const n = set.length;
05668b0d4753atagen2026-10-16 05:50 UTC434
    if (n < 2) continue;
05668b0d4753atagen2026-10-16 05:50 UTC435
    let lo = Infinity, hi = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC436
    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 UTC437
    if (LANE_REGION) { lo = LANE_REGION[ax]; hi = LANE_REGION[ax] + LANE_REGION[size]; }
05668b0d4753atagen2026-10-16 05:50 UTC438
    const diff = [], lt = [];
05668b0d4753atagen2026-10-16 05:50 UTC439
    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 UTC440
    for (let i = 0; i < n; i++)
05668b0d4753atagen2026-10-16 05:50 UTC441
      for (let j = 0; j < n; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC442
        if (i === j) continue;
05668b0d4753atagen2026-10-16 05:50 UTC443
        const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]);
05668b0d4753atagen2026-10-16 05:50 UTC444
        if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC445
        for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC446
      }
05668b0d4753atagen2026-10-16 05:50 UTC447
    for (let k = 2; k <= n; k++) {
05668b0d4753atagen2026-10-16 05:50 UTC448
      const s = (hi - lo - (k - 1) * G) / k;
05668b0d4753atagen2026-10-16 05:50 UTC449
      if (s < 6) break;
05668b0d4753atagen2026-10-16 05:50 UTC450
      const lane = new Array(n).fill(-1);
05668b0d4753atagen2026-10-16 05:50 UTC451
      const assign = i => {
05668b0d4753atagen2026-10-16 05:50 UTC452
        if (i === n) return true;
05668b0d4753atagen2026-10-16 05:50 UTC453
        for (let l = 0; l < k; l++) {
05668b0d4753atagen2026-10-16 05:50 UTC454
          let ok = true;
05668b0d4753atagen2026-10-16 05:50 UTC455
          for (let j = 0; j < i && ok; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC456
            if (diff[i][j] && lane[j] === l) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC457
            if (lt[i][j] && !(l < lane[j])) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC458
            if (lt[j][i] && !(lane[j] < l)) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC459
          }
05668b0d4753atagen2026-10-16 05:50 UTC460
          if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; }
05668b0d4753atagen2026-10-16 05:50 UTC461
        }
05668b0d4753atagen2026-10-16 05:50 UTC462
        return false;
05668b0d4753atagen2026-10-16 05:50 UTC463
      };
05668b0d4753atagen2026-10-16 05:50 UTC464
      if (!assign(0)) continue;
05668b0d4753atagen2026-10-16 05:50 UTC465
      const strip = l => lo + l * (s + G);
05668b0d4753atagen2026-10-16 05:50 UTC466
      const inSet = new Set(set);
05668b0d4753atagen2026-10-16 05:50 UTC467
      for (const phase of [2, 1, 3]) {
05668b0d4753atagen2026-10-16 05:50 UTC468
        const K1 = {}, K2 = {};
05668b0d4753atagen2026-10-16 05:50 UTC469
        for (const id of list) {
05668b0d4753atagen2026-10-16 05:50 UTC470
          const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC471
          if (inSet.has(id)) {
05668b0d4753atagen2026-10-16 05:50 UTC472
            const l = lane[set.indexOf(id)];
05668b0d4753atagen2026-10-16 05:50 UTC473
            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 UTC474
            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 UTC475
          } else if (phase === 1) { K1[id] = t; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC476
          else if (phase === 2) { K1[id] = f; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC477
          else { K1[id] = f; K2[id] = f; }
05668b0d4753atagen2026-10-16 05:50 UTC478
        }
cd7085e54c88atagen2026-10-16 06:58 UTC479
        if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list) && pureOk([from, K1, K2, to]))
05668b0d4753atagen2026-10-16 05:50 UTC480
          return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC481
      }
05668b0d4753atagen2026-10-16 05:50 UTC482
    }
05668b0d4753atagen2026-10-16 05:50 UTC483
  }
05668b0d4753atagen2026-10-16 05:50 UTC484
  return null;
05668b0d4753atagen2026-10-16 05:50 UTC485
}
05668b0d4753atagen2026-10-16 05:50 UTC486
05668b0d4753atagen2026-10-16 05:50 UTC487
function pinch(from, to, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC488
  const K1 = { ...from }, K2 = { ...to };
05668b0d4753atagen2026-10-16 05:50 UTC489
  for (const id of movers) {
05668b0d4753atagen2026-10-16 05:50 UTC490
    const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC491
    K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC492
    K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC493
  }
05668b0d4753atagen2026-10-16 05:50 UTC494
  return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC495
}
05668b0d4753atagen2026-10-16 05:50 UTC496
05668b0d4753atagen2026-10-16 05:50 UTC497
function bbox(rects) {
05668b0d4753atagen2026-10-16 05:50 UTC498
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC499
  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 UTC500
  return R(x0, y0, x1 - x0, y1 - y0);
05668b0d4753atagen2026-10-16 05:50 UTC501
}
05668b0d4753atagen2026-10-16 05:50 UTC502
function planBlocks(from, to, groups) {
05668b0d4753atagen2026-10-16 05:50 UTC503
  const bf = {}, bt = {};
05668b0d4753atagen2026-10-16 05:50 UTC504
  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 UTC505
  const bp = plan(bf, bt);
05668b0d4753atagen2026-10-16 05:50 UTC506
  const last = bp.keys.length - 1;
05668b0d4753atagen2026-10-16 05:50 UTC507
  const keys = bp.keys.map((K, ki) => {
05668b0d4753atagen2026-10-16 05:50 UTC508
    if (ki === 0) return from;
05668b0d4753atagen2026-10-16 05:50 UTC509
    if (ki === last) return to;
05668b0d4753atagen2026-10-16 05:50 UTC510
    const L = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC511
    for (const b in groups) for (const id of groups[b]) {
05668b0d4753atagen2026-10-16 05:50 UTC512
      const r = from[id], box = bf[b], k = K[b];
05668b0d4753atagen2026-10-16 05:50 UTC513
      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 UTC514
      const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC515
      L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h);
05668b0d4753atagen2026-10-16 05:50 UTC516
    }
05668b0d4753atagen2026-10-16 05:50 UTC517
    return L;
05668b0d4753atagen2026-10-16 05:50 UTC518
  });
05668b0d4753atagen2026-10-16 05:50 UTC519
  return { kind: bp.kind + ' on blocks', keys, durs: bp.durs };
05668b0d4753atagen2026-10-16 05:50 UTC520
}
05668b0d4753atagen2026-10-16 05:50 UTC521
05668b0d4753atagen2026-10-16 05:50 UTC522
function bezier(x1, y1, x2, y2) {
05668b0d4753atagen2026-10-16 05:50 UTC523
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
05668b0d4753atagen2026-10-16 05:50 UTC524
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
05668b0d4753atagen2026-10-16 05:50 UTC525
  const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u;
05668b0d4753atagen2026-10-16 05:50 UTC526
  const dX = u => (3 * ax * u + 2 * bx) * u + cx;
05668b0d4753atagen2026-10-16 05:50 UTC527
  return t => {
05668b0d4753atagen2026-10-16 05:50 UTC528
    if (t <= 0) return 0;
05668b0d4753atagen2026-10-16 05:50 UTC529
    if (t >= 1) return 1;
05668b0d4753atagen2026-10-16 05:50 UTC530
    let u = t;
05668b0d4753atagen2026-10-16 05:50 UTC531
    for (let i = 0; i < 8; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC532
      const x = X(u) - t;
05668b0d4753atagen2026-10-16 05:50 UTC533
      if (Math.abs(x) < 1e-6) return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC534
      const d = dX(u);
05668b0d4753atagen2026-10-16 05:50 UTC535
      if (Math.abs(d) < 1e-6) break;
05668b0d4753atagen2026-10-16 05:50 UTC536
      u -= x / d;
05668b0d4753atagen2026-10-16 05:50 UTC537
    }
05668b0d4753atagen2026-10-16 05:50 UTC538
    let lo = 0, hi = 1; u = t;
05668b0d4753atagen2026-10-16 05:50 UTC539
    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 UTC540
    return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC541
  };
05668b0d4753atagen2026-10-16 05:50 UTC542
}
05668b0d4753atagen2026-10-16 05:50 UTC543
const BASE = bezier(0.45, 0, 0.2, 1);
05668b0d4753atagen2026-10-16 05:50 UTC544
const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h];
05668b0d4753atagen2026-10-16 05:50 UTC545
</script>
05668b0d4753atagen2026-10-16 05:50 UTC546
05668b0d4753atagen2026-10-16 05:50 UTC547
<script>
944fc4782ac3atagen2026-10-16 08:09 UTC548
const PAL = { T: '#9aa3b5', A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' };
05668b0d4753atagen2026-10-16 05:50 UTC549
const demos = [];
05668b0d4753atagen2026-10-16 05:50 UTC550
05668b0d4753atagen2026-10-16 05:50 UTC551
class Demo {
05668b0d4753atagen2026-10-16 05:50 UTC552
  constructor(id, W, H, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC553
    this.W = W; this.H = H;
05668b0d4753atagen2026-10-16 05:50 UTC554
    this.outputs = opts.outputs || [R(0, 0, W, H)];
cd7085e54c88atagen2026-10-16 06:58 UTC555
    this.floating = new Set(opts.floating || []);
05668b0d4753atagen2026-10-16 05:50 UTC556
    const el = document.getElementById(id);
05668b0d4753atagen2026-10-16 05:50 UTC557
    this.canvas = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC558
    const dpr = window.devicePixelRatio || 1;
05668b0d4753atagen2026-10-16 05:50 UTC559
    this.canvas.width = W * dpr; this.canvas.height = H * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC560
    this.canvas.style.maxWidth = W + 'px';
05668b0d4753atagen2026-10-16 05:50 UTC561
    this.ctx = this.canvas.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC562
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC563
    el.appendChild(this.canvas);
05668b0d4753atagen2026-10-16 05:50 UTC564
    if (opts.graph) {
05668b0d4753atagen2026-10-16 05:50 UTC565
      this.graph = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC566
      this.graph.width = W * dpr; this.graph.height = 90 * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC567
      this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px';
05668b0d4753atagen2026-10-16 05:50 UTC568
      this.gctx = this.graph.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC569
      this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC570
      el.appendChild(this.graph);
05668b0d4753atagen2026-10-16 05:50 UTC571
      this.hist = [];
05668b0d4753atagen2026-10-16 05:50 UTC572
    }
05668b0d4753atagen2026-10-16 05:50 UTC573
    this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls);
05668b0d4753atagen2026-10-16 05:50 UTC574
    this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status);
05668b0d4753atagen2026-10-16 05:50 UTC575
    this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle';
05668b0d4753atagen2026-10-16 05:50 UTC576
    demos.push(this);
05668b0d4753atagen2026-10-16 05:50 UTC577
  }
05668b0d4753atagen2026-10-16 05:50 UTC578
  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 UTC579
  check(label, init, fn) {
05668b0d4753atagen2026-10-16 05:50 UTC580
    const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init;
05668b0d4753atagen2026-10-16 05:50 UTC581
    c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c;
05668b0d4753atagen2026-10-16 05:50 UTC582
  }
05668b0d4753atagen2026-10-16 05:50 UTC583
  set(L) { this.layout = clone(L); this.anim = null; }
05668b0d4753atagen2026-10-16 05:50 UTC584
  go(target, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC585
    const now = performance.now();
05668b0d4753atagen2026-10-16 05:50 UTC586
    const cur = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC587
    const from = {};
05668b0d4753atagen2026-10-16 05:50 UTC588
    for (const id in target) from[id] = cur[id] || target[id];
cd7085e54c88atagen2026-10-16 06:58 UTC589
    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 UTC590
    const curves = p.durs.map(() => BASE);
05668b0d4753atagen2026-10-16 05:50 UTC591
    const interrupted = !!this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC592
    let matched = false;
05668b0d4753atagen2026-10-16 05:50 UTC593
    if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) {
05668b0d4753atagen2026-10-16 05:50 UTC594
      const K1 = p.keys[1];
05668b0d4753atagen2026-10-16 05:50 UTC595
      let best = null, bs = 0;
05668b0d4753atagen2026-10-16 05:50 UTC596
      for (const id in K1) {
05668b0d4753atagen2026-10-16 05:50 UTC597
        const v = this.vel[id]; if (!v) continue;
05668b0d4753atagen2026-10-16 05:50 UTC598
        const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]);
05668b0d4753atagen2026-10-16 05:50 UTC599
        const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue;
05668b0d4753atagen2026-10-16 05:50 UTC600
        const sp = Math.hypot(...v);
05668b0d4753atagen2026-10-16 05:50 UTC601
        if (sp > bs) { bs = sp; best = { v, D, dd }; }
05668b0d4753atagen2026-10-16 05:50 UTC602
      }
05668b0d4753atagen2026-10-16 05:50 UTC603
      if (best) {
05668b0d4753atagen2026-10-16 05:50 UTC604
        const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd;
05668b0d4753atagen2026-10-16 05:50 UTC605
        const slope = s * p.durs[0];
05668b0d4753atagen2026-10-16 05:50 UTC606
        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 UTC607
      }
05668b0d4753atagen2026-10-16 05:50 UTC608
    }
05668b0d4753atagen2026-10-16 05:50 UTC609
    this.anim = { keys: p.keys, durs: p.durs, curves, start: now };
05668b0d4753atagen2026-10-16 05:50 UTC610
    this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : '');
05668b0d4753atagen2026-10-16 05:50 UTC611
    this.overlapSeen = false;
05668b0d4753atagen2026-10-16 05:50 UTC612
  }
05668b0d4753atagen2026-10-16 05:50 UTC613
  sampleAt(t) {
05668b0d4753atagen2026-10-16 05:50 UTC614
    if (!this.anim) return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC615
    let e = t - this.anim.start;
05668b0d4753atagen2026-10-16 05:50 UTC616
    const { keys, durs, curves } = this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC617
    for (let i = 0; i < durs.length; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC618
      const d = durs[i];
05668b0d4753atagen2026-10-16 05:50 UTC619
      if (e < d) {
05668b0d4753atagen2026-10-16 05:50 UTC620
        this.phase = i;
05668b0d4753atagen2026-10-16 05:50 UTC621
        const s = curves[i](e / d);
05668b0d4753atagen2026-10-16 05:50 UTC622
        const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC623
        for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s);
05668b0d4753atagen2026-10-16 05:50 UTC624
        return L;
05668b0d4753atagen2026-10-16 05:50 UTC625
      }
05668b0d4753atagen2026-10-16 05:50 UTC626
      e -= d;
05668b0d4753atagen2026-10-16 05:50 UTC627
    }
05668b0d4753atagen2026-10-16 05:50 UTC628
    this.layout = clone(keys[keys.length - 1]);
05668b0d4753atagen2026-10-16 05:50 UTC629
    this.anim = null;
05668b0d4753atagen2026-10-16 05:50 UTC630
    return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC631
  }
05668b0d4753atagen2026-10-16 05:50 UTC632
  frame(now) {
05668b0d4753atagen2026-10-16 05:50 UTC633
    const L = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC634
    if (this.prev) {
05668b0d4753atagen2026-10-16 05:50 UTC635
      const dt = now - this.prev.t;
05668b0d4753atagen2026-10-16 05:50 UTC636
      if (dt > 0) for (const id in L) if (this.prev.L[id]) {
05668b0d4753atagen2026-10-16 05:50 UTC637
        const a = edges(this.prev.L[id]), b = edges(L[id]);
05668b0d4753atagen2026-10-16 05:50 UTC638
        this.vel[id] = b.map((v, i) => (v - a[i]) / dt);
05668b0d4753atagen2026-10-16 05:50 UTC639
      }
05668b0d4753atagen2026-10-16 05:50 UTC640
    }
05668b0d4753atagen2026-10-16 05:50 UTC641
    this.prev = { t: now, L };
05668b0d4753atagen2026-10-16 05:50 UTC642
    this.draw(L);
05668b0d4753atagen2026-10-16 05:50 UTC643
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC644
    if (overlaps.length) this.overlapSeen = true;
05668b0d4753atagen2026-10-16 05:50 UTC645
    const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : '';
05668b0d4753atagen2026-10-16 05:50 UTC646
    this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : '');
05668b0d4753atagen2026-10-16 05:50 UTC647
    if (this.graph) this.drawGraph(now);
05668b0d4753atagen2026-10-16 05:50 UTC648
  }
05668b0d4753atagen2026-10-16 05:50 UTC649
  findOverlaps(L) {
cd7085e54c88atagen2026-10-16 06:58 UTC650
    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 UTC651
    const out = [];
05668b0d4753atagen2026-10-16 05:50 UTC652
    for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC653
      const a = L[ids[i]], b = L[ids[j]];
05668b0d4753atagen2026-10-16 05:50 UTC654
      const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
05668b0d4753atagen2026-10-16 05:50 UTC655
      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 UTC656
      if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0));
05668b0d4753atagen2026-10-16 05:50 UTC657
    }
05668b0d4753atagen2026-10-16 05:50 UTC658
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC659
  }
05668b0d4753atagen2026-10-16 05:50 UTC660
  draw(L) {
05668b0d4753atagen2026-10-16 05:50 UTC661
    const c = this.ctx;
05668b0d4753atagen2026-10-16 05:50 UTC662
    c.clearRect(0, 0, this.W, this.H);
05668b0d4753atagen2026-10-16 05:50 UTC663
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC664
    for (const o of this.outputs) {
05668b0d4753atagen2026-10-16 05:50 UTC665
      c.save();
05668b0d4753atagen2026-10-16 05:50 UTC666
      c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h);
05668b0d4753atagen2026-10-16 05:50 UTC667
      c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip();
cd7085e54c88atagen2026-10-16 06:58 UTC668
      for (const id of Object.keys(L).sort((a, b) => this.floating.has(a) - this.floating.has(b))) {
05668b0d4753atagen2026-10-16 05:50 UTC669
        const r = L[id];
05668b0d4753atagen2026-10-16 05:50 UTC670
        if (r.w < 0.5 || r.h < 0.5) continue;
05668b0d4753atagen2026-10-16 05:50 UTC671
        const rad = Math.min(8, r.w / 2, r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC672
        c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad);
05668b0d4753atagen2026-10-16 05:50 UTC673
        c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill();
05668b0d4753atagen2026-10-16 05:50 UTC674
        c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC675
        if (r.w > 22 && r.h > 22) {
05668b0d4753atagen2026-10-16 05:50 UTC676
          c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle';
05668b0d4753atagen2026-10-16 05:50 UTC677
          c.fillText(id, r.x + r.w / 2, r.y + r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC678
        }
05668b0d4753atagen2026-10-16 05:50 UTC679
      }
05668b0d4753atagen2026-10-16 05:50 UTC680
      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 UTC681
      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 UTC682
      c.restore();
05668b0d4753atagen2026-10-16 05:50 UTC683
    }
05668b0d4753atagen2026-10-16 05:50 UTC684
  }
05668b0d4753atagen2026-10-16 05:50 UTC685
  drawGraph(now) {
05668b0d4753atagen2026-10-16 05:50 UTC686
    const v = this.vel.A;
05668b0d4753atagen2026-10-16 05:50 UTC687
    const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0;
05668b0d4753atagen2026-10-16 05:50 UTC688
    this.hist.push([now, sp]);
05668b0d4753atagen2026-10-16 05:50 UTC689
    while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift();
05668b0d4753atagen2026-10-16 05:50 UTC690
    const c = this.gctx, W = this.W, H = 90;
05668b0d4753atagen2026-10-16 05:50 UTC691
    c.clearRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC692
    c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC693
    const max = Math.max(400, ...this.hist.map(h => h[1]));
05668b0d4753atagen2026-10-16 05:50 UTC694
    c.beginPath();
05668b0d4753atagen2026-10-16 05:50 UTC695
    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 UTC696
    c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC697
    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 UTC698
  }
05668b0d4753atagen2026-10-16 05:50 UTC699
}
05668b0d4753atagen2026-10-16 05:50 UTC700
05668b0d4753atagen2026-10-16 05:50 UTC701
const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G);
05668b0d4753atagen2026-10-16 05:50 UTC702
05668b0d4753atagen2026-10-16 05:50 UTC703
{ // d1 swap
05668b0d4753atagen2026-10-16 05:50 UTC704
  const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC705
  let swapped = false;
05668b0d4753atagen2026-10-16 05:50 UTC706
  const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']);
05668b0d4753atagen2026-10-16 05:50 UTC707
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC708
  d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC709
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC710
}
05668b0d4753atagen2026-10-16 05:50 UTC711
05668b0d4753atagen2026-10-16 05:50 UTC712
{ // d2 one phase
05668b0d4753atagen2026-10-16 05:50 UTC713
  const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC714
  const s = { wide: false, d: false, e: false };
cd7085e54c88atagen2026-10-16 06:58 UTC715
  const modes = ['line on old boundary', 'point on old boundary', 'target centre'];
cd7085e54c88atagen2026-10-16 06:58 UTC716
  let mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC717
  const ratios = ids => ids.map(id => (id === 'A' && s.wide) ? 2 : 1);
05668b0d4753atagen2026-10-16 05:50 UTC718
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC719
    const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C'];
cd7085e54c88atagen2026-10-16 06:58 UTC720
    const out = splitX(I, ids, ratios(ids));
cd7085e54c88atagen2026-10-16 06:58 UTC721
    if (!s.d) {
cd7085e54c88atagen2026-10-16 06:58 UTC722
      const bx = (out.B.x + out.B.w + out.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC723
      const T = splitX(I, ['A', 'B', 'D', 'C'], ratios(['A', 'B', 'D', 'C'])).D;
cd7085e54c88atagen2026-10-16 06:58 UTC724
      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 UTC725
    }
05668b0d4753atagen2026-10-16 05:50 UTC726
    const b = out.B;
05668b0d4753atagen2026-10-16 05:50 UTC727
    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 UTC728
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC729
  };
05668b0d4753atagen2026-10-16 05:50 UTC730
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC731
  d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC732
  d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC733
  d.button('Split B with E', () => { s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC734
  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 UTC735
  const mb = d.button('seed: ' + modes[0], () => {
cd7085e54c88atagen2026-10-16 06:58 UTC736
    mode = (mode + 1) % modes.length; mb.textContent = 'seed: ' + modes[mode];
cd7085e54c88atagen2026-10-16 06:58 UTC737
    if (!s.d && !d.anim) d.set(L());
cd7085e54c88atagen2026-10-16 06:58 UTC738
  });
05668b0d4753atagen2026-10-16 05:50 UTC739
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC740
}
05668b0d4753atagen2026-10-16 05:50 UTC741
function tagLayout(I, view2, order) {
05668b0d4753atagen2026-10-16 05:50 UTC742
  const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC743
  if (view2) {
05668b0d4753atagen2026-10-16 05:50 UTC744
    const rows = splitY(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC745
    Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC746
  } else {
05668b0d4753atagen2026-10-16 05:50 UTC747
    Object.assign(L, splitX(I, ['A', 'B', 'C']));
05668b0d4753atagen2026-10-16 05:50 UTC748
    const y = order[0] === 't2' ? I.y : I.y + I.h;
05668b0d4753atagen2026-10-16 05:50 UTC749
    Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC750
  }
05668b0d4753atagen2026-10-16 05:50 UTC751
  return L;
05668b0d4753atagen2026-10-16 05:50 UTC752
}
05668b0d4753atagen2026-10-16 05:50 UTC753
05668b0d4753atagen2026-10-16 05:50 UTC754
{ // d3 tag toggle
05668b0d4753atagen2026-10-16 05:50 UTC755
  const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC756
  let view2 = false;
05668b0d4753atagen2026-10-16 05:50 UTC757
  d.set(tagLayout(I, view2, ['t1', 't2']));
05668b0d4753atagen2026-10-16 05:50 UTC758
  d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); });
05668b0d4753atagen2026-10-16 05:50 UTC759
}
05668b0d4753atagen2026-10-16 05:50 UTC760
05668b0d4753atagen2026-10-16 05:50 UTC761
{ // d3b tag reorder
05668b0d4753atagen2026-10-16 05:50 UTC762
  const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC763
  let order = ['t1', 't2'], blocks = true;
05668b0d4753atagen2026-10-16 05:50 UTC764
  d.set(tagLayout(I, true, order));
05668b0d4753atagen2026-10-16 05:50 UTC765
  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 UTC766
  d.check('rigid blocks', true, v => blocks = v);
05668b0d4753atagen2026-10-16 05:50 UTC767
}
05668b0d4753atagen2026-10-16 05:50 UTC768
05668b0d4753atagen2026-10-16 05:50 UTC769
{ // d4a orientation flip
05668b0d4753atagen2026-10-16 05:50 UTC770
  const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC771
  let rows = false;
05668b0d4753atagen2026-10-16 05:50 UTC772
  const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']);
05668b0d4753atagen2026-10-16 05:50 UTC773
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC774
  d.button('Flip orientation', () => { rows = !rows; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC775
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC776
}
05668b0d4753atagen2026-10-16 05:50 UTC777
05668b0d4753atagen2026-10-16 05:50 UTC778
{ // d4b grow both axes
05668b0d4753atagen2026-10-16 05:50 UTC779
  const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC780
  let wide = false;
05668b0d4753atagen2026-10-16 05:50 UTC781
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC782
    const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]);
05668b0d4753atagen2026-10-16 05:50 UTC783
    return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) };
05668b0d4753atagen2026-10-16 05:50 UTC784
  };
05668b0d4753atagen2026-10-16 05:50 UTC785
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC786
  d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC787
}
05668b0d4753atagen2026-10-16 05:50 UTC788
05668b0d4753atagen2026-10-16 05:50 UTC789
{ // d5 lanes
05668b0d4753atagen2026-10-16 05:50 UTC790
  const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC791
  let order = ['A', 'B', 'C', 'D'];
05668b0d4753atagen2026-10-16 05:50 UTC792
  const L = () => splitX(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC793
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC794
  d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC795
  d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC796
  d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC797
  d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC798
}
05668b0d4753atagen2026-10-16 05:50 UTC799
05668b0d4753atagen2026-10-16 05:50 UTC800
{ // d6 interrupts
05668b0d4753atagen2026-10-16 05:50 UTC801
  const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
aa790e7e33a0atagen2026-10-16 08:03 UTC802
  const names = ["A B C", "C A B", "A wide", "B C A", "close B"];
05668b0d4753atagen2026-10-16 05:50 UTC803
  const targets = [
05668b0d4753atagen2026-10-16 05:50 UTC804
    () => splitX(I, ['A', 'B', 'C']),
05668b0d4753atagen2026-10-16 05:50 UTC805
    () => splitX(I, ['C', 'A', 'B']),
05668b0d4753atagen2026-10-16 05:50 UTC806
    () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]),
05668b0d4753atagen2026-10-16 05:50 UTC807
    () => splitX(I, ['B', 'C', 'A']),
05668b0d4753atagen2026-10-16 05:50 UTC808
    () => ({ ...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 UTC809
  ];
05668b0d4753atagen2026-10-16 05:50 UTC810
  const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
05668b0d4753atagen2026-10-16 05:50 UTC811
  let idx = 0;
05668b0d4753atagen2026-10-16 05:50 UTC812
  d.set(withB(targets[0]()));
aa790e7e33a0atagen2026-10-16 08:03 UTC813
  const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); d.lastKind += ` · target "${names[idx]}"`; };
05668b0d4753atagen2026-10-16 05:50 UTC814
  d.button('Next target', next);
05668b0d4753atagen2026-10-16 05:50 UTC815
  d.button('Storm (8 rapid interrupts)', () => {
05668b0d4753atagen2026-10-16 05:50 UTC816
    let n = 0;
05668b0d4753atagen2026-10-16 05:50 UTC817
    const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); };
05668b0d4753atagen2026-10-16 05:50 UTC818
    fire();
05668b0d4753atagen2026-10-16 05:50 UTC819
  });
05668b0d4753atagen2026-10-16 05:50 UTC820
  d.check('match velocity', true, v => d.vmatch = v);
05668b0d4753atagen2026-10-16 05:50 UTC821
}
05668b0d4753atagen2026-10-16 05:50 UTC822
05668b0d4753atagen2026-10-16 05:50 UTC823
{ // d7 across outputs
05668b0d4753atagen2026-10-16 05:50 UTC824
  const W = 680, H = 260;
05668b0d4753atagen2026-10-16 05:50 UTC825
  const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H);
05668b0d4753atagen2026-10-16 05:50 UTC826
  const d = new Demo('d7', W, H, { outputs: [o1, o2] });
05668b0d4753atagen2026-10-16 05:50 UTC827
  const I1 = inner(320, H, 0), I2 = inner(320, H, 360);
05668b0d4753atagen2026-10-16 05:50 UTC828
  let s1 = ['A', 'B'], s2 = ['C'];
05668b0d4753atagen2026-10-16 05:50 UTC829
  const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) });
05668b0d4753atagen2026-10-16 05:50 UTC830
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC831
  d.button('Move B across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC832
    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 UTC833
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC834
  });
05668b0d4753atagen2026-10-16 05:50 UTC835
  d.button('Move A across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC836
    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 UTC837
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC838
  });
05668b0d4753atagen2026-10-16 05:50 UTC839
  d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC840
}
05668b0d4753atagen2026-10-16 05:50 UTC841
05668b0d4753atagen2026-10-16 05:50 UTC842
function layoutTree(node, rect, visible, out) {
05668b0d4753atagen2026-10-16 05:50 UTC843
  if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; }
05668b0d4753atagen2026-10-16 05:50 UTC844
  const horiz = node.dir === 'row';
05668b0d4753atagen2026-10-16 05:50 UTC845
  const shown = node.kids.map(k => anyVisible(k, visible));
05668b0d4753atagen2026-10-16 05:50 UTC846
  const m = shown.filter(Boolean).length;
05668b0d4753atagen2026-10-16 05:50 UTC847
  const len = horiz ? rect.w : rect.h;
05668b0d4753atagen2026-10-16 05:50 UTC848
  const avail = len - G * Math.max(0, m - 1);
05668b0d4753atagen2026-10-16 05:50 UTC849
  let pos = horiz ? rect.x : rect.y, seen = 0;
05668b0d4753atagen2026-10-16 05:50 UTC850
  node.kids.forEach((k, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC851
    if (!shown[i]) {
05668b0d4753atagen2026-10-16 05:50 UTC852
      const p = seen ? pos - G / 2 : pos;
05668b0d4753atagen2026-10-16 05:50 UTC853
      const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC854
      layoutTree(k, r, () => false, out);
05668b0d4753atagen2026-10-16 05:50 UTC855
      return;
05668b0d4753atagen2026-10-16 05:50 UTC856
    }
05668b0d4753atagen2026-10-16 05:50 UTC857
    const size = avail / m;
05668b0d4753atagen2026-10-16 05:50 UTC858
    const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size);
05668b0d4753atagen2026-10-16 05:50 UTC859
    layoutTree(k, r, visible, out);
05668b0d4753atagen2026-10-16 05:50 UTC860
    pos += size + G; seen++;
05668b0d4753atagen2026-10-16 05:50 UTC861
  });
05668b0d4753atagen2026-10-16 05:50 UTC862
}
05668b0d4753atagen2026-10-16 05:50 UTC863
function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); }
05668b0d4753atagen2026-10-16 05:50 UTC864
05668b0d4753atagen2026-10-16 05:50 UTC865
{ // d8 one tree, tags filter
05668b0d4753atagen2026-10-16 05:50 UTC866
  const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC867
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
05668b0d4753atagen2026-10-16 05:50 UTC868
  const view = { 1: true, 2: true };
05668b0d4753atagen2026-10-16 05:50 UTC869
  let mixed = false;
05668b0d4753atagen2026-10-16 05:50 UTC870
  const tree = () => mixed
05668b0d4753atagen2026-10-16 05:50 UTC871
    ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] }
05668b0d4753atagen2026-10-16 05:50 UTC872
    : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] };
05668b0d4753atagen2026-10-16 05:50 UTC873
  const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; };
05668b0d4753atagen2026-10-16 05:50 UTC874
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC875
  d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC876
  d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC877
  d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC878
}
05668b0d4753atagen2026-10-16 05:50 UTC879
cd7085e54c88atagen2026-10-16 06:58 UTC880
function stackLayouts(I) {
cd7085e54c88atagen2026-10-16 06:58 UTC881
  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 UTC882
  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 UTC883
  return {
cd7085e54c88atagen2026-10-16 06:58 UTC884
    'A | B | C': () => splitX(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC885
    'A | (B / C)': () => col(splitX(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC886
    '(A / C) | B': () => col(splitX(I, ['L', 'B']), 'L', ['A', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC887
    '(A / B) | C': () => col(splitX(I, ['L', 'C']), 'L', ['A', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC888
    'B | C | A': () => splitX(I, ['B', 'C', 'A']),
cd7085e54c88atagen2026-10-16 06:58 UTC889
    'A / B / C': () => splitY(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC890
    'A / (B | C)': () => row(splitY(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC891
    '(A / B) | (C / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'B']), 'R', ['C', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC892
    'A | (C / B / D)': () => col(splitX(I, ['A', 'R']), 'R', ['C', 'B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC893
    '(A / D) | (C / B)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'D']), 'R', ['C', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC894
    '(A / B / C) | D': () => col(splitX(I, ['L', 'D']), 'L', ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC895
    '(A / C) | (B / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'C']), 'R', ['B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC896
  };
cd7085e54c88atagen2026-10-16 06:58 UTC897
}
cd7085e54c88atagen2026-10-16 06:58 UTC898
cd7085e54c88atagen2026-10-16 06:58 UTC899
for (const [id, names] of [
cd7085e54c88atagen2026-10-16 06:58 UTC900
  ['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 UTC901
  ['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 UTC902
]) {
cd7085e54c88atagen2026-10-16 06:58 UTC903
  const W = 640, H = 300, d = new Demo(id, W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC904
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC905
  for (const n of names) d.button(n, () => d.go(S[n]()));
cd7085e54c88atagen2026-10-16 06:58 UTC906
}
cd7085e54c88atagen2026-10-16 06:58 UTC907
cd7085e54c88atagen2026-10-16 06:58 UTC908
{ // d9c grid
cd7085e54c88atagen2026-10-16 06:58 UTC909
  const W = 640, H = 300, d = new Demo('d9c', W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC910
  let at = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC911
  const names = ['(A / D) | (C / B)', '(A / B / C) | D'];
cd7085e54c88atagen2026-10-16 06:58 UTC912
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC913
  d.button('(A / D) | (C / B)  ⇄  (A / B / C) | D', () => { at = 1 - at; d.go(S[names[at]]()); });
cd7085e54c88atagen2026-10-16 06:58 UTC914
}
cd7085e54c88atagen2026-10-16 06:58 UTC915
cd7085e54c88atagen2026-10-16 06:58 UTC916
{ // d10 floating to tiled
cd7085e54c88atagen2026-10-16 06:58 UTC917
  const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H);
cd7085e54c88atagen2026-10-16 06:58 UTC918
  const FL = R(220, 40, 220, 130);
944fc4782ac3atagen2026-10-16 08:09 UTC919
  const modes = ['direct glide', 'slot opens last', 'slot opens first'];
aa790e7e33a0atagen2026-10-16 08:03 UTC920
  let tiled = false, mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC921
  const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']);
cd7085e54c88atagen2026-10-16 06:58 UTC922
  const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL });
cd7085e54c88atagen2026-10-16 06:58 UTC923
  const ortho = () => STYLE === 'orthogonal';
cd7085e54c88atagen2026-10-16 06:58 UTC924
  const assemble = (n, tiledAt, fAt, kind) => {
cd7085e54c88atagen2026-10-16 06:58 UTC925
    const keys = [];
cd7085e54c88atagen2026-10-16 06:58 UTC926
    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 UTC927
    return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) };
cd7085e54c88atagen2026-10-16 06:58 UTC928
  };
aa790e7e33a0atagen2026-10-16 08:03 UTC929
  const slotPlan = (from, to, opening) => {
aa790e7e33a0atagen2026-10-16 08:03 UTC930
    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 UTC931
    return opening
aa790e7e33a0atagen2026-10-16 08:03 UTC932
      ? 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 UTC933
      : 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 UTC934
  };
aa790e7e33a0atagen2026-10-16 08:03 UTC935
  const at = (tp, i) => tp.keys[Math.max(0, Math.min(i, tp.durs.length))];
cd7085e54c88atagen2026-10-16 06:58 UTC936
  const floatIn = from => {
aa790e7e33a0atagen2026-10-16 08:03 UTC937
    const to = tiledL(), slot = to.F, tp = slotPlan(from, to, true), k = tp.durs.length, f = from.F;
aa790e7e33a0atagen2026-10-16 08:03 UTC938
    if (modes[mode] === 'direct glide') {
aa790e7e33a0atagen2026-10-16 08:03 UTC939
      const n = Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC940
      return assemble(n, i => at(tp, i), i => i === 0 ? f : slot, `glide in, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC941
    }
cd7085e54c88atagen2026-10-16 06:58 UTC942
    const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC943
    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 UTC944
    const travel = path.length - 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC945
    if (modes[mode] === 'slot opens last') {
aa790e7e33a0atagen2026-10-16 08:03 UTC946
      const n = travel + Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC947
      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 UTC948
    }
aa790e7e33a0atagen2026-10-16 08:03 UTC949
    const n = Math.max(travel, k) + 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC950
    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 UTC951
  };
cd7085e54c88atagen2026-10-16 06:58 UTC952
  const floatOut = from => {
aa790e7e33a0atagen2026-10-16 08:03 UTC953
    const to = floatL(), slot = from.F, tp = slotPlan(from, to, false), k = tp.durs.length;
aa790e7e33a0atagen2026-10-16 08:03 UTC954
    if (modes[mode] === 'direct glide') {
aa790e7e33a0atagen2026-10-16 08:03 UTC955
      const n = Math.max(1, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC956
      return assemble(n, i => at(tp, i), i => i === 0 ? slot : FL, `glide out, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC957
    }
cd7085e54c88atagen2026-10-16 06:58 UTC958
    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 UTC959
    const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL];
aa790e7e33a0atagen2026-10-16 08:03 UTC960
    const travel = path.length - 1;
aa790e7e33a0atagen2026-10-16 08:03 UTC961
    if (modes[mode] === 'slot opens last') {
aa790e7e33a0atagen2026-10-16 08:03 UTC962
      const n = Math.max(travel, k);
aa790e7e33a0atagen2026-10-16 08:03 UTC963
      return assemble(n, i => at(tp, i), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
aa790e7e33a0atagen2026-10-16 08:03 UTC964
    }
aa790e7e33a0atagen2026-10-16 08:03 UTC965
    const n = Math.max(travel, k + 1);
aa790e7e33a0atagen2026-10-16 08:03 UTC966
    return assemble(n, i => at(tp, i - 1), i => path[Math.min(i, travel)], `lift out, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC967
  };
cd7085e54c88atagen2026-10-16 06:58 UTC968
  d.set(floatL());
cd7085e54c88atagen2026-10-16 06:58 UTC969
  d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); });
aa790e7e33a0atagen2026-10-16 08:03 UTC970
  const mb = d.button('mode: ' + modes[0], () => { mode = (mode + 1) % modes.length; mb.textContent = 'mode: ' + modes[mode]; });
cd7085e54c88atagen2026-10-16 06:58 UTC971
}
cd7085e54c88atagen2026-10-16 06:58 UTC972
944fc4782ac3atagen2026-10-16 08:09 UTC973
{ // d11 one tree, hidden slots
944fc4782ac3atagen2026-10-16 08:09 UTC974
  const W = 640, H = 320, d = new Demo('d11', W, H), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC975
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
944fc4782ac3atagen2026-10-16 08:09 UTC976
  const view = { 1: true, 2: true };
944fc4782ac3atagen2026-10-16 08:09 UTC977
  let tree, ghosts = false;
944fc4782ac3atagen2026-10-16 08:09 UTC978
  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 UTC979
  reset();
944fc4782ac3atagen2026-10-16 08:09 UTC980
  const L = () => { const o = {}; layoutTree(tree, I, id => view[tagOf[id]], o); return o; };
944fc4782ac3atagen2026-10-16 08:09 UTC981
  const parentOf = (node, id, par = null) => {
944fc4782ac3atagen2026-10-16 08:09 UTC982
    if (typeof node === 'string') return node === id ? par : null;
944fc4782ac3atagen2026-10-16 08:09 UTC983
    for (const k of node.kids) { const r = parentOf(k, id, node); if (r) return r; }
944fc4782ac3atagen2026-10-16 08:09 UTC984
    return null;
944fc4782ac3atagen2026-10-16 08:09 UTC985
  };
944fc4782ac3atagen2026-10-16 08:09 UTC986
  const collapse = node => {
944fc4782ac3atagen2026-10-16 08:09 UTC987
    if (typeof node === 'string') return node;
944fc4782ac3atagen2026-10-16 08:09 UTC988
    node.kids = node.kids.map(collapse).filter(k => typeof k === 'string' || k.kids.length);
944fc4782ac3atagen2026-10-16 08:09 UTC989
    return node.kids.length === 1 ? node.kids[0] : node;
944fc4782ac3atagen2026-10-16 08:09 UTC990
  };
944fc4782ac3atagen2026-10-16 08:09 UTC991
  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 UTC992
  const moveAfter = (id, target) => {
944fc4782ac3atagen2026-10-16 08:09 UTC993
    const p = parentOf(tree, id); p.kids.splice(p.kids.indexOf(id), 1);
944fc4782ac3atagen2026-10-16 08:09 UTC994
    tree = collapse(tree);
944fc4782ac3atagen2026-10-16 08:09 UTC995
    const pt = parentOf(tree, target); pt.kids.splice(pt.kids.indexOf(target) + 1, 0, id);
944fc4782ac3atagen2026-10-16 08:09 UTC996
  };
944fc4782ac3atagen2026-10-16 08:09 UTC997
  d.overlay = () => {
944fc4782ac3atagen2026-10-16 08:09 UTC998
    if (!ghosts) return [];
944fc4782ac3atagen2026-10-16 08:09 UTC999
    const full = {}; layoutTree(tree, I, () => true, full);
944fc4782ac3atagen2026-10-16 08:09 UTC1000
    return Object.keys(full).filter(id => !view[tagOf[id]]).map(id => ({ ...full[id], id }));
944fc4782ac3atagen2026-10-16 08:09 UTC1001
  };
944fc4782ac3atagen2026-10-16 08:09 UTC1002
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1003
  d.button('Hide / show tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1004
  d.button('Swap A and B', () => { swap('A', 'B'); d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1005
  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 UTC1006
  d.button('Reset', () => { reset(); d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1007
  d.check('show hidden slots', false, v => ghosts = v);
944fc4782ac3atagen2026-10-16 08:09 UTC1008
}
944fc4782ac3atagen2026-10-16 08:09 UTC1009
944fc4782ac3atagen2026-10-16 08:09 UTC1010
{ // d12 lane extent
944fc4782ac3atagen2026-10-16 08:09 UTC1011
  const W = 640, H = 320, d = new Demo('d12', W, H), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC1012
  let order = ['A', 'B', 'C'], whole = false;
944fc4782ac3atagen2026-10-16 08:09 UTC1013
  const L = () => { const r = splitY(I, ['top', 'bot'], [7, 3]); return { T: r.top, ...splitX(r.bot, order) }; };
944fc4782ac3atagen2026-10-16 08:09 UTC1014
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1015
  d.button('Reverse bottom row', () => {
944fc4782ac3atagen2026-10-16 08:09 UTC1016
    order = [...order].reverse();
944fc4782ac3atagen2026-10-16 08:09 UTC1017
    const tgt = L();
944fc4782ac3atagen2026-10-16 08:09 UTC1018
    d.go(tgt, { planFn: from => { LANE_REGION = whole ? I : null; try { return plan(from, tgt); } finally { LANE_REGION = null; } } });
944fc4782ac3atagen2026-10-16 08:09 UTC1019
  });
944fc4782ac3atagen2026-10-16 08:09 UTC1020
  const mb = d.button('lanes span: involved band', () => { whole = !whole; mb.textContent = 'lanes span: ' + (whole ? 'whole output' : 'involved band'); });
944fc4782ac3atagen2026-10-16 08:09 UTC1021
}
944fc4782ac3atagen2026-10-16 08:09 UTC1022
944fc4782ac3atagen2026-10-16 08:09 UTC1023
{ // d13 fullscreen
944fc4782ac3atagen2026-10-16 08:09 UTC1024
  const W = 640, H = 300, d = new Demo('d13', W, H, { floating: ['B'] }), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC1025
  let full = false;
944fc4782ac3atagen2026-10-16 08:09 UTC1026
  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 UTC1027
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1028
  d.button('Fullscreen B', () => {
944fc4782ac3atagen2026-10-16 08:09 UTC1029
    full = !full;
944fc4782ac3atagen2026-10-16 08:09 UTC1030
    const tgt = L();
944fc4782ac3atagen2026-10-16 08:09 UTC1031
    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 UTC1032
  });
944fc4782ac3atagen2026-10-16 08:09 UTC1033
}
944fc4782ac3atagen2026-10-16 08:09 UTC1034
944fc4782ac3atagen2026-10-16 08:09 UTC1035
{ // d14 expand
944fc4782ac3atagen2026-10-16 08:09 UTC1036
  const W = 640, H = 300, d = new Demo('d14', W, H), I = inner(W, H);
944fc4782ac3atagen2026-10-16 08:09 UTC1037
  let col = false, root = false;
944fc4782ac3atagen2026-10-16 08:09 UTC1038
  const L = () => {
944fc4782ac3atagen2026-10-16 08:09 UTC1039
    const cols = splitX(I, ['A', 'R']);
944fc4782ac3atagen2026-10-16 08:09 UTC1040
    const area = root ? I : cols.R;
944fc4782ac3atagen2026-10-16 08:09 UTC1041
    const o = root ? { A: R(I.x, I.y, 0, I.h) } : { A: cols.A };
944fc4782ac3atagen2026-10-16 08:09 UTC1042
    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 UTC1043
    else Object.assign(o, splitY(area, ['B', 'C', 'D']));
944fc4782ac3atagen2026-10-16 08:09 UTC1044
    return o;
944fc4782ac3atagen2026-10-16 08:09 UTC1045
  };
944fc4782ac3atagen2026-10-16 08:09 UTC1046
  d.set(L());
944fc4782ac3atagen2026-10-16 08:09 UTC1047
  d.button('Expand C in its column', () => { col = !col; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1048
  d.button('Expand the column to the output', () => { root = !root; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1049
  d.button('Expand C all the way', () => { const on = !(col && root); col = on; root = on; d.go(L()); });
944fc4782ac3atagen2026-10-16 08:09 UTC1050
}
944fc4782ac3atagen2026-10-16 08:09 UTC1051
05668b0d4753atagen2026-10-16 05:50 UTC1052
document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };
05668b0d4753atagen2026-10-16 05:50 UTC1053
document.getElementById('style').onchange = e => { STYLE = e.target.value; };
05668b0d4753atagen2026-10-16 05:50 UTC1054
const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); };
05668b0d4753atagen2026-10-16 05:50 UTC1055
requestAnimationFrame(loop);
05668b0d4753atagen2026-10-16 05:50 UTC1056
</script>
05668b0d4753atagen2026-10-16 05:50 UTC1057
</body>
05668b0d4753atagen2026-10-16 05:50 UTC1058
</html>