atagen

monolith

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

Blame: design/planner.html

05668b0d4753atagen2026-10-16 05:50 UTC1
<!doctype html>
05668b0d4753atagen2026-10-16 05:50 UTC2
<html lang="en">
05668b0d4753atagen2026-10-16 05:50 UTC3
<head>
05668b0d4753atagen2026-10-16 05:50 UTC4
<meta charset="utf-8">
05668b0d4753atagen2026-10-16 05:50 UTC5
<title>monolith planner design, draft 1</title>
05668b0d4753atagen2026-10-16 05:50 UTC6
<style>
05668b0d4753atagen2026-10-16 05:50 UTC7
  :root { --bg:#111317; --panel:#181b21; --fg:#d8dbe2; --dim:#8a909c; --accent:#e0774a; --bad:#ff4d5e; }
05668b0d4753atagen2026-10-16 05:50 UTC8
  * { box-sizing:border-box; }
05668b0d4753atagen2026-10-16 05:50 UTC9
  body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.6 system-ui, sans-serif; }
05668b0d4753atagen2026-10-16 05:50 UTC10
  main { max-width:920px; margin:0 auto; padding:32px 20px 120px; }
05668b0d4753atagen2026-10-16 05:50 UTC11
  h1 { font-size:28px; margin:0 0 4px; }
05668b0d4753atagen2026-10-16 05:50 UTC12
  h2 { font-size:20px; margin:48px 0 8px; border-bottom:1px solid #2a2e37; padding-bottom:4px; }
05668b0d4753atagen2026-10-16 05:50 UTC13
  h3 { font-size:16px; margin:24px 0 6px; }
05668b0d4753atagen2026-10-16 05:50 UTC14
  p, li { color:var(--fg); }
05668b0d4753atagen2026-10-16 05:50 UTC15
  .sub { color:var(--dim); margin:0 0 24px; }
05668b0d4753atagen2026-10-16 05:50 UTC16
  code { background:#22262e; padding:1px 5px; border-radius:4px; font-size:13px; }
05668b0d4753atagen2026-10-16 05:50 UTC17
  pre { background:#1a1d23; padding:14px; border-radius:8px; overflow-x:auto; font-size:13px; line-height:1.45; }
05668b0d4753atagen2026-10-16 05:50 UTC18
  table { border-collapse:collapse; width:100%; font-size:14px; margin:12px 0; }
05668b0d4753atagen2026-10-16 05:50 UTC19
  th, td { border:1px solid #2a2e37; padding:6px 8px; text-align:left; vertical-align:top; }
05668b0d4753atagen2026-10-16 05:50 UTC20
  th { background:#1d2027; }
05668b0d4753atagen2026-10-16 05:50 UTC21
  .demo { background:var(--panel); border:1px solid #2a2e37; border-radius:10px; padding:12px; margin:16px 0; }
05668b0d4753atagen2026-10-16 05:50 UTC22
  .demo canvas { display:block; width:100%; height:auto; border-radius:6px; background:#0d0f12; }
05668b0d4753atagen2026-10-16 05:50 UTC23
  .controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
05668b0d4753atagen2026-10-16 05:50 UTC24
  button { background:#2a2f39; color:var(--fg); border:1px solid #3a404c; border-radius:6px; padding:5px 12px; cursor:pointer; font:inherit; font-size:14px; }
05668b0d4753atagen2026-10-16 05:50 UTC25
  button:hover { background:#343a46; }
05668b0d4753atagen2026-10-16 05:50 UTC26
  label { color:var(--dim); font-size:14px; display:flex; gap:6px; align-items:center; }
05668b0d4753atagen2026-10-16 05:50 UTC27
  .status { font:13px/1.4 ui-monospace, monospace; color:var(--dim); margin-top:8px; min-height:1.4em; }
05668b0d4753atagen2026-10-16 05:50 UTC28
  .status .bad { color:var(--bad); }
05668b0d4753atagen2026-10-16 05:50 UTC29
  .global { position:sticky; top:0; z-index:5; background:rgba(17,19,23,0.94); backdrop-filter:blur(4px); border-bottom:1px solid #2a2e37; padding:8px 20px; display:flex; gap:20px; justify-content:center; flex-wrap:wrap; }
05668b0d4753atagen2026-10-16 05:50 UTC30
  .q { background:#1d2027; border-left:3px solid var(--accent); padding:8px 12px; margin:10px 0; border-radius:0 6px 6px 0; }
05668b0d4753atagen2026-10-16 05:50 UTC31
  .note { color:var(--dim); font-size:14px; }
05668b0d4753atagen2026-10-16 05:50 UTC32
</style>
05668b0d4753atagen2026-10-16 05:50 UTC33
</head>
05668b0d4753atagen2026-10-16 05:50 UTC34
<body>
05668b0d4753atagen2026-10-16 05:50 UTC35
<div class="global">
05668b0d4753atagen2026-10-16 05:50 UTC36
  <label>phase duration <input id="phaseMs" type="range" min="150" max="1500" step="50" value="450"> <span id="phaseMsOut">450 ms</span></label>
cd7085e54c88atagen2026-10-16 06:58 UTC37
  <label>style <select id="style"><option value="orthogonal">orthogonal (default, one axis per window per phase)</option><option value="direct">direct (diagonal when safe)</option></select></label>
05668b0d4753atagen2026-10-16 05:50 UTC38
</div>
05668b0d4753atagen2026-10-16 05:50 UTC39
<main>
cd7085e54c88atagen2026-10-16 06:58 UTC40
<h1>monolith planner, draft 2</h1>
cd7085e54c88atagen2026-10-16 06:58 UTC41
<p class="sub">Design for the multiphase layout animation planner (milestone M1). Every demo runs a small JavaScript port of the proposed algorithm, so what you see is what the design does. Red means two tiled windows overlap. Orthogonal is the default style, and the selector at the top switches every demo to direct.</p>
05668b0d4753atagen2026-10-16 05:50 UTC42
05668b0d4753atagen2026-10-16 05:50 UTC43
<h2>1. What the planner does</h2>
05668b0d4753atagen2026-10-16 05:50 UTC44
<p>The layout engine produces a target rectangle for every window. The planner turns "where windows are on screen now" and "where they should be" into a short sequence of keyframe layouts. Each step between two keyframes is a <em>phase</em>. Every window in a phase moves on the same curve and the phase takes exactly <code>t</code>, the configured duration, so a plan lasts <code>phases × t</code>.</p>
05668b0d4753atagen2026-10-16 05:50 UTC45
<p>Rules carried over from wry <code>master</code>.</p>
05668b0d4753atagen2026-10-16 05:50 UTC46
<ul>
05668b0d4753atagen2026-10-16 05:50 UTC47
  <li>Tiled windows never overlap while they move.</li>
05668b0d4753atagen2026-10-16 05:50 UTC48
  <li>Phases are synchronised. Everything in a phase starts and stops together.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC49
  <li>In orthogonal style, the default, a window moves or resizes along one axis per phase.</li>
05668b0d4753atagen2026-10-16 05:50 UTC50
  <li>An interrupt throws the plan away and replans from what is on screen. Plans never stack.</li>
05668b0d4753atagen2026-10-16 05:50 UTC51
</ul>
05668b0d4753atagen2026-10-16 05:50 UTC52
<p>New requirements wry never met.</p>
05668b0d4753atagen2026-10-16 05:50 UTC53
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC54
  <li>Moves between outputs, and large groups such as viewing or hiding a tag.</li>
05668b0d4753atagen2026-10-16 05:50 UTC55
  <li>Best-effort velocity continuity when an interrupt lands mid-phase.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC56
  <li>A hard bound on plan length, and a construction that never needs a fallback.</li>
05668b0d4753atagen2026-10-16 05:50 UTC57
</ul>
05668b0d4753atagen2026-10-16 05:50 UTC58
05668b0d4753atagen2026-10-16 05:50 UTC59
<h2>2. The problem</h2>
05668b0d4753atagen2026-10-16 05:50 UTC60
<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 UTC61
<div class="demo" id="d1"></div>
05668b0d4753atagen2026-10-16 05:50 UTC62
05668b0d4753atagen2026-10-16 05:50 UTC63
<h2>3. The core rule, shared separators</h2>
05668b0d4753atagen2026-10-16 05:50 UTC64
<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 UTC65
<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 UTC66
<p>Looking at a single pair gives a lower bound on phases.</p>
05668b0d4753atagen2026-10-16 05:50 UTC67
<table>
05668b0d4753atagen2026-10-16 05:50 UTC68
  <tr><th>pair goes from</th><th>to</th><th>minimum phases</th><th>why</th></tr>
05668b0d4753atagen2026-10-16 05:50 UTC69
  <tr><td>left of</td><td>left of (any sizes)</td><td>1</td><td>relation shared</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC70
  <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 UTC71
  <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 UTC72
</table>
cd7085e54c88atagen2026-10-16 06:58 UTC73
<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 UTC74
<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 UTC75
cd7085e54c88atagen2026-10-16 06:58 UTC76
<h2>4. One phase, and seeds</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC77
<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 UTC78
<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 UTC79
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC80
  <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 UTC81
  <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 UTC82
  <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 UTC83
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC84
<p>Cycle the seed mode below and insert D to compare.</p>
05668b0d4753atagen2026-10-16 05:50 UTC85
<div class="demo" id="d2"></div>
05668b0d4753atagen2026-10-16 05:50 UTC86
<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 UTC87
<div class="demo" id="d3"></div>
05668b0d4753atagen2026-10-16 05:50 UTC88
05668b0d4753atagen2026-10-16 05:50 UTC89
<h2>5. Two phases, axis order</h2>
05668b0d4753atagen2026-10-16 05:50 UTC90
<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 UTC91
<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>
05668b0d4753atagen2026-10-16 05:50 UTC92
<div class="demo" id="d4a"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC93
<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 UTC94
<div class="demo" id="d4b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC95
05668b0d4753atagen2026-10-16 05:50 UTC96
<h2>6. Three phases, lanes</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC97
<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 UTC98
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC99
  <li>Two windows may share a lane only if they never pass each other.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC100
  <li>Windows stacked along the strip axis must keep their stacking order in the lanes, at both ends.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC101
  <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 UTC102
  <li>The strips span only the band the involved windows occupy, not the whole output.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC103
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC104
<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 UTC105
<div class="demo" id="d5"></div>
05668b0d4753atagen2026-10-16 05:50 UTC106
cd7085e54c88atagen2026-10-16 06:58 UTC107
<h2>7. Four phases, the grid</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC108
<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 UTC109
<ol>
cd7085e54c88atagen2026-10-16 06:58 UTC110
  <li>Every involved window shrinks vertically into its own horizontal strip, ordered by where it starts.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC111
  <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 UTC112
  <li>It moves vertically to its final height. Its column keeps it clear of everything.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC113
  <li>It expands horizontally to its final width.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC114
</ol>
cd7085e54c88atagen2026-10-16 06:58 UTC115
<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 UTC116
<div class="demo" id="d9c"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC117
cd7085e54c88atagen2026-10-16 06:58 UTC118
<h2>8. Joining and leaving stacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC119
<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 UTC120
<div class="demo" id="d9a"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC121
<div class="demo" id="d9b"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC122
cd7085e54c88atagen2026-10-16 06:58 UTC123
<h2>9. Rigid blocks and movement commands</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC124
<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 UTC125
<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 UTC126
<div class="demo" id="d3b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC127
cd7085e54c88atagen2026-10-16 06:58 UTC128
<h2>10. Interrupts and velocity</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC129
<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 UTC130
<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 UTC131
<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 UTC132
<div class="demo" id="d6"></div>
05668b0d4753atagen2026-10-16 05:50 UTC133
cd7085e54c88atagen2026-10-16 06:58 UTC134
<h2>11. Across outputs</h2>
05668b0d4753atagen2026-10-16 05:50 UTC135
<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 UTC136
<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 UTC137
<div class="demo" id="d7"></div>
05668b0d4753atagen2026-10-16 05:50 UTC138
cd7085e54c88atagen2026-10-16 06:58 UTC139
<h2>12. Floating to tiled</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC140
<p>A floating window joining the tiled layout is phased, unlike a spawn. The tiled windows open a slot for it, concurrently and following the same rules as an insertion. Meanwhile the floating window travels above them, one axis per phase, until it sits over the slot. In the last phase it falls into place. Lifting a tiled window back out runs the same steps in reverse. The floating window is drawn above the tiled layer and is exempt from the overlap rule while it travels.</p>
cd7085e54c88atagen2026-10-16 06:58 UTC141
<div class="demo" id="d10"></div>
cd7085e54c88atagen2026-10-16 06:58 UTC142
cd7085e54c88atagen2026-10-16 06:58 UTC143
<h2>13. Fallbacks</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC144
<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 UTC145
cd7085e54c88atagen2026-10-16 06:58 UTC146
<h2>14. Algorithm</h2>
05668b0d4753atagen2026-10-16 05:50 UTC147
<pre>plan(from, to):
05668b0d4753atagen2026-10-16 05:50 UTC148
  groups = motion groups (windows whose swept areas can touch, transitively)
05668b0d4753atagen2026-10-16 05:50 UTC149
  for each group:
05668b0d4753atagen2026-10-16 05:50 UTC150
    if every pair shares a relation between from and to
05668b0d4753atagen2026-10-16 05:50 UTC151
       and (style is direct or every window changes only one axis):
05668b0d4753atagen2026-10-16 05:50 UTC152
      return [from, to]                                    # 1 phase
05668b0d4753atagen2026-10-16 05:50 UTC153
    if 2-SAT over axis order finds K1:                     # mixed rects
05668b0d4753atagen2026-10-16 05:50 UTC154
      return [from, K1, to]                                # 2 phases
05668b0d4753atagen2026-10-16 05:50 UTC155
    for lane_axis in [y, x]:                               # strips, then columns
05668b0d4753atagen2026-10-16 05:50 UTC156
      for set in [windows in reversals, all movers, all]:
05668b0d4753atagen2026-10-16 05:50 UTC157
        for k in 2..|set|:
05668b0d4753atagen2026-10-16 05:50 UTC158
          if lanes assignable with k lanes and all three phases validate:
05668b0d4753atagen2026-10-16 05:50 UTC159
            return [from, K1, K2, to]                      # 3 phases
cd7085e54c88atagen2026-10-16 06:58 UTC160
    for set in [windows in reversals, all movers, all]:
cd7085e54c88atagen2026-10-16 06:58 UTC161
      if the grid validates for set:                       # always true for all
cd7085e54c88atagen2026-10-16 06:58 UTC162
        return [from, K1, K2, K3, to]                      # 4 phases
cd7085e54c88atagen2026-10-16 06:58 UTC163
    return fallback(from, to)                              # unreachable in theory
05668b0d4753atagen2026-10-16 05:50 UTC164
05668b0d4753atagen2026-10-16 05:50 UTC165
  rigid blocks: run plan() on block rectangles first, then map windows into them
cd7085e54c88atagen2026-10-16 06:58 UTC166
  floating:     slot opens via plan() with a seed, floater travels x, then y, then lands
05668b0d4753atagen2026-10-16 05:50 UTC167
  interrupts:   from = rectangles sampled at the current presentation time
05668b0d4753atagen2026-10-16 05:50 UTC168
  velocity:     first-phase curve per group, slope from the fastest mover,
05668b0d4753atagen2026-10-16 05:50 UTC169
                applied only if moving the same way, control point height ≤ 1
05668b0d4753atagen2026-10-16 05:50 UTC170
  validation:   exact pairwise check over the curve actually used</pre>
05668b0d4753atagen2026-10-16 05:50 UTC171
<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 UTC172
<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 UTC173
cd7085e54c88atagen2026-10-16 06:58 UTC174
<h2>15. Compared with wry master</h2>
05668b0d4753atagen2026-10-16 05:50 UTC175
<table>
05668b0d4753atagen2026-10-16 05:50 UTC176
  <tr><th>wry master</th><th>this design</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC177
  <tr><td>SingleAction, MixedSinglePhase</td><td>1 phase</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC178
  <tr><td>SpaceThenOrthogonalGrowth, OrientationChange, HierarchyOrderedScales</td><td>2 phases, one 2-SAT problem instead of three strategies</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC179
  <tr><td>SwapLanes</td><td>3 phases, general lane assignment for any permutation</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC180
  <tr><td>no equivalent</td><td>4 phases, the grid, for simultaneous reversals on both axes</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC181
  <tr><td>ReversedForwardPlan</td><td>not needed, the construction is symmetric</td></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC182
  <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 UTC183
  <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 UTC184
  <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 UTC185
  <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 UTC186
  <tr><td>Cross-output moves skip animation</td><td>planned in global coordinates, clipped per output</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC187
</table>
05668b0d4753atagen2026-10-16 05:50 UTC188
cd7085e54c88atagen2026-10-16 06:58 UTC189
<h2>16. Interspersing tags (open, changes the layout model)</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC190
<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 UTC191
<table>
05668b0d4753atagen2026-10-16 05:50 UTC192
  <tr><th>option</th><th>meaning</th><th>verdict</th></tr>
cd7085e54c88atagen2026-10-16 06:58 UTC193
  <tr><td>Forbid it</td><td>tags stay in their rows</td><td>limiting</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC194
  <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 UTC195
  <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 UTC196
  <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 UTC197
  <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 UTC198
</table>
cd7085e54c88atagen2026-10-16 06:58 UTC199
<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 UTC200
<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 UTC201
<div class="demo" id="d8"></div>
05668b0d4753atagen2026-10-16 05:50 UTC202
cd7085e54c88atagen2026-10-16 06:58 UTC203
<h2>17. Decisions</h2>
cd7085e54c88atagen2026-10-16 06:58 UTC204
<h3>Settled</h3>
cd7085e54c88atagen2026-10-16 06:58 UTC205
<ul>
cd7085e54c88atagen2026-10-16 06:58 UTC206
  <li>Orthogonal is the default and the flagship look. Direct is a toggle.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC207
  <li>Window moves and block moves both exist as commands. Tag reordering moves rigid blocks.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC208
  <li>One curve per motion group, bounded by phase time rather than amount of motion.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC209
  <li>Floating to tiled is phased, travel one axis per phase, then land.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC210
  <li>The planner must never need a fallback. Pinch and lerp stay as safety nets to try.</li>
cd7085e54c88atagen2026-10-16 06:58 UTC211
</ul>
cd7085e54c88atagen2026-10-16 06:58 UTC212
<h3>Open</h3>
cd7085e54c88atagen2026-10-16 06:58 UTC213
<div class="q"><strong>Seed shape.</strong> Line on the old boundary, point on the old boundary, or target centre (section 4). Line is always one phase. Point looks more like your "grow from centre" but costs a phase in orthogonal style.</div>
cd7085e54c88atagen2026-10-16 06:58 UTC214
<div class="q"><strong>Landing in orthogonal style.</strong> The final "falls into place" step resizes both width and height at once. Is that an accepted exception, or must it also split into two phases?</div>
cd7085e54c88atagen2026-10-16 06:58 UTC215
<div class="q"><strong>Lane extent.</strong> Strips span only the band of the involved windows. Spanning the whole output gives thicker lanes for small windows, but disturbs more of the screen.</div>
cd7085e54c88atagen2026-10-16 06:58 UTC216
<div class="q"><strong>The layout model (section 16).</strong> One tree per output with tags as a filter, or something else. You raised the case of windows being closed and moved while others are hidden. In the one-tree model, hidden windows keep their slot, closing removes only that leaf, and moves are made relative to visible neighbours, so hidden siblings stay attached to whichever visible neighbour they were next to. That rule is what needs discussing.</div>
05668b0d4753atagen2026-10-16 05:50 UTC217
</main>
05668b0d4753atagen2026-10-16 05:50 UTC218
05668b0d4753atagen2026-10-16 05:50 UTC219
<script id="planner">
05668b0d4753atagen2026-10-16 05:50 UTC220
const G = 10;
05668b0d4753atagen2026-10-16 05:50 UTC221
const EPS = 0.5;
05668b0d4753atagen2026-10-16 05:50 UTC222
let PHASE_MS = 450;
cd7085e54c88atagen2026-10-16 06:58 UTC223
let STYLE = 'orthogonal';
05668b0d4753atagen2026-10-16 05:50 UTC224
05668b0d4753atagen2026-10-16 05:50 UTC225
const R = (x, y, w, h) => ({ x, y, w, h });
05668b0d4753atagen2026-10-16 05:50 UTC226
const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; };
05668b0d4753atagen2026-10-16 05:50 UTC227
const eq = (a, b) => Math.abs(a - b) < 0.05;
05668b0d4753atagen2026-10-16 05:50 UTC228
const lerp = (a, b, s) => a + (b - a) * s;
05668b0d4753atagen2026-10-16 05:50 UTC229
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 UTC230
05668b0d4753atagen2026-10-16 05:50 UTC231
function splitX(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC232
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC233
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC234
  const avail = rect.w - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC235
  let x = rect.x;
05668b0d4753atagen2026-10-16 05:50 UTC236
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC237
  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 UTC238
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC239
}
05668b0d4753atagen2026-10-16 05:50 UTC240
function splitY(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC241
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC242
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC243
  const avail = rect.h - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC244
  let y = rect.y;
05668b0d4753atagen2026-10-16 05:50 UTC245
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC246
  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 UTC247
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC248
}
05668b0d4753atagen2026-10-16 05:50 UTC249
05668b0d4753atagen2026-10-16 05:50 UTC250
function relBits(a, b) {
05668b0d4753atagen2026-10-16 05:50 UTC251
  let s = 0;
05668b0d4753atagen2026-10-16 05:50 UTC252
  if (a.x + a.w <= b.x + EPS) s |= 1;
05668b0d4753atagen2026-10-16 05:50 UTC253
  if (b.x + b.w <= a.x + EPS) s |= 2;
05668b0d4753atagen2026-10-16 05:50 UTC254
  if (a.y + a.h <= b.y + EPS) s |= 4;
05668b0d4753atagen2026-10-16 05:50 UTC255
  if (b.y + b.h <= a.y + EPS) s |= 8;
05668b0d4753atagen2026-10-16 05:50 UTC256
  return s;
05668b0d4753atagen2026-10-16 05:50 UTC257
}
05668b0d4753atagen2026-10-16 05:50 UTC258
function phaseOk(K1, K2, list) {
05668b0d4753atagen2026-10-16 05:50 UTC259
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC260
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC261
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC262
      if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false;
05668b0d4753atagen2026-10-16 05:50 UTC263
    }
05668b0d4753atagen2026-10-16 05:50 UTC264
  return true;
05668b0d4753atagen2026-10-16 05:50 UTC265
}
05668b0d4753atagen2026-10-16 05:50 UTC266
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 UTC267
function pureOk(keys) {
cd7085e54c88atagen2026-10-16 06:58 UTC268
  if (STYLE !== 'orthogonal') return true;
cd7085e54c88atagen2026-10-16 06:58 UTC269
  for (let k = 0; k + 1 < keys.length; k++)
cd7085e54c88atagen2026-10-16 06:58 UTC270
    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 UTC271
  return true;
cd7085e54c88atagen2026-10-16 06:58 UTC272
}
05668b0d4753atagen2026-10-16 05:50 UTC273
05668b0d4753atagen2026-10-16 05:50 UTC274
function plan(from, to, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC275
  const list = Object.keys(to);
05668b0d4753atagen2026-10-16 05:50 UTC276
  const T = PHASE_MS;
05668b0d4753atagen2026-10-16 05:50 UTC277
  if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC278
  const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; });
05668b0d4753atagen2026-10-16 05:50 UTC279
  if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] };
05668b0d4753atagen2026-10-16 05:50 UTC280
  const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); });
05668b0d4753atagen2026-10-16 05:50 UTC281
  if (phaseOk(from, to, list) && (STYLE === 'direct' || pure))
05668b0d4753atagen2026-10-16 05:50 UTC282
    return { kind: '1 phase', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC283
  const two = axisOrder(from, to, list, movers);
05668b0d4753atagen2026-10-16 05:50 UTC284
  if (two) return two;
05668b0d4753atagen2026-10-16 05:50 UTC285
  for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; }
cd7085e54c88atagen2026-10-16 06:58 UTC286
  const g = grid(from, to, list, movers);
cd7085e54c88atagen2026-10-16 06:58 UTC287
  if (g) return g;
05668b0d4753atagen2026-10-16 05:50 UTC288
  return pinch(from, to, movers);
05668b0d4753atagen2026-10-16 05:50 UTC289
}
05668b0d4753atagen2026-10-16 05:50 UTC290
cd7085e54c88atagen2026-10-16 06:58 UTC291
function conflictSet(from, to, list) {
cd7085e54c88atagen2026-10-16 06:58 UTC292
  const out = new Set();
cd7085e54c88atagen2026-10-16 06:58 UTC293
  for (let i = 0; i < list.length; i++)
cd7085e54c88atagen2026-10-16 06:58 UTC294
    for (let j = i + 1; j < list.length; j++) {
cd7085e54c88atagen2026-10-16 06:58 UTC295
      const a = list[i], b = list[j];
cd7085e54c88atagen2026-10-16 06:58 UTC296
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { out.add(a); out.add(b); }
cd7085e54c88atagen2026-10-16 06:58 UTC297
    }
cd7085e54c88atagen2026-10-16 06:58 UTC298
  return [...out];
cd7085e54c88atagen2026-10-16 06:58 UTC299
}
cd7085e54c88atagen2026-10-16 06:58 UTC300
cd7085e54c88atagen2026-10-16 06:58 UTC301
function grid(from, to, list, movers) {
cd7085e54c88atagen2026-10-16 06:58 UTC302
  const cx = r => r.x + r.w / 2, cy = r => r.y + r.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC303
  for (const set of [conflictSet(from, to, list), movers, list]) {
cd7085e54c88atagen2026-10-16 06:58 UTC304
    const n = set.length;
cd7085e54c88atagen2026-10-16 06:58 UTC305
    if (n < 2) continue;
cd7085e54c88atagen2026-10-16 06:58 UTC306
    let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
cd7085e54c88atagen2026-10-16 06:58 UTC307
    for (const id of set) for (const r of [from[id], to[id]]) {
cd7085e54c88atagen2026-10-16 06:58 UTC308
      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 UTC309
    }
cd7085e54c88atagen2026-10-16 06:58 UTC310
    const sh = (y1 - y0 - (n - 1) * G) / n, sw = (x1 - x0 - (n - 1) * G) / n;
cd7085e54c88atagen2026-10-16 06:58 UTC311
    const rowOf = {}, colOf = {};
cd7085e54c88atagen2026-10-16 06:58 UTC312
    [...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 UTC313
    [...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 UTC314
    const inSet = new Set(set);
cd7085e54c88atagen2026-10-16 06:58 UTC315
    for (const phase of [3, 2, 4, 1]) {
cd7085e54c88atagen2026-10-16 06:58 UTC316
      const K = [{}, {}, {}];
cd7085e54c88atagen2026-10-16 06:58 UTC317
      for (const id of list) {
cd7085e54c88atagen2026-10-16 06:58 UTC318
        const f = from[id], t = to[id];
cd7085e54c88atagen2026-10-16 06:58 UTC319
        if (inSet.has(id)) {
cd7085e54c88atagen2026-10-16 06:58 UTC320
          K[0][id] = R(f.x, rowOf[id], f.w, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC321
          K[1][id] = R(colOf[id], rowOf[id], sw, sh);
cd7085e54c88atagen2026-10-16 06:58 UTC322
          K[2][id] = R(colOf[id], t.y, sw, t.h);
cd7085e54c88atagen2026-10-16 06:58 UTC323
        } else for (let k = 0; k < 3; k++) K[k][id] = k + 1 >= phase ? t : f;
cd7085e54c88atagen2026-10-16 06:58 UTC324
      }
cd7085e54c88atagen2026-10-16 06:58 UTC325
      const keys = [from, ...K, to];
cd7085e54c88atagen2026-10-16 06:58 UTC326
      let ok = true;
cd7085e54c88atagen2026-10-16 06:58 UTC327
      for (let k = 0; k < 4 && ok; k++) ok = phaseOk(keys[k], keys[k + 1], list);
cd7085e54c88atagen2026-10-16 06:58 UTC328
      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 UTC329
    }
cd7085e54c88atagen2026-10-16 06:58 UTC330
  }
cd7085e54c88atagen2026-10-16 06:58 UTC331
  return null;
cd7085e54c88atagen2026-10-16 06:58 UTC332
}
cd7085e54c88atagen2026-10-16 06:58 UTC333
05668b0d4753atagen2026-10-16 05:50 UTC334
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 UTC335
05668b0d4753atagen2026-10-16 05:50 UTC336
function axisOrder(from, to, list, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC337
  const n = movers.length;
05668b0d4753atagen2026-10-16 05:50 UTC338
  if (n > 14) return null;
05668b0d4753atagen2026-10-16 05:50 UTC339
  let best = null, bestScore = Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC340
  for (let mask = 0; mask < (1 << n); mask++) {
05668b0d4753atagen2026-10-16 05:50 UTC341
    const K1 = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC342
    let score = 0;
05668b0d4753atagen2026-10-16 05:50 UTC343
    movers.forEach((id, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC344
      const xf = !!(mask & (1 << i));
05668b0d4753atagen2026-10-16 05:50 UTC345
      const c = chg(from[id], to[id]);
05668b0d4753atagen2026-10-16 05:50 UTC346
      K1[id] = mixed(from[id], to[id], xf);
05668b0d4753atagen2026-10-16 05:50 UTC347
      if (c.x && !c.y && !xf) score++;
05668b0d4753atagen2026-10-16 05:50 UTC348
      if (c.y && !c.x && xf) score++;
05668b0d4753atagen2026-10-16 05:50 UTC349
    });
05668b0d4753atagen2026-10-16 05:50 UTC350
    if (score >= bestScore) continue;
05668b0d4753atagen2026-10-16 05:50 UTC351
    if (phaseOk(from, K1, list) && phaseOk(K1, to, list)) { best = K1; bestScore = score; if (score === 0) break; }
05668b0d4753atagen2026-10-16 05:50 UTC352
  }
05668b0d4753atagen2026-10-16 05:50 UTC353
  return best ? { kind: '2 phases (axis order)', keys: [from, best, to], durs: [PHASE_MS, PHASE_MS] } : null;
05668b0d4753atagen2026-10-16 05:50 UTC354
}
05668b0d4753atagen2026-10-16 05:50 UTC355
05668b0d4753atagen2026-10-16 05:50 UTC356
function lanes(from, to, list, movers, ax) {
05668b0d4753atagen2026-10-16 05:50 UTC357
  const size = ax === 'y' ? 'h' : 'w';
05668b0d4753atagen2026-10-16 05:50 UTC358
  const bitsT = ax === 'y' ? 3 : 12;
05668b0d4753atagen2026-10-16 05:50 UTC359
  const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2;
05668b0d4753atagen2026-10-16 05:50 UTC360
  const conflict = new Set();
05668b0d4753atagen2026-10-16 05:50 UTC361
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC362
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC363
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC364
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); }
05668b0d4753atagen2026-10-16 05:50 UTC365
    }
05668b0d4753atagen2026-10-16 05:50 UTC366
  const sets = [[...conflict], movers, list];
05668b0d4753atagen2026-10-16 05:50 UTC367
  for (const set of sets) {
05668b0d4753atagen2026-10-16 05:50 UTC368
    const n = set.length;
05668b0d4753atagen2026-10-16 05:50 UTC369
    if (n < 2) continue;
05668b0d4753atagen2026-10-16 05:50 UTC370
    let lo = Infinity, hi = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC371
    for (const id of set) for (const r of [from[id], to[id]]) { lo = Math.min(lo, r[ax]); hi = Math.max(hi, r[ax] + r[size]); }
05668b0d4753atagen2026-10-16 05:50 UTC372
    const diff = [], lt = [];
05668b0d4753atagen2026-10-16 05:50 UTC373
    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 UTC374
    for (let i = 0; i < n; i++)
05668b0d4753atagen2026-10-16 05:50 UTC375
      for (let j = 0; j < n; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC376
        if (i === j) continue;
05668b0d4753atagen2026-10-16 05:50 UTC377
        const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]);
05668b0d4753atagen2026-10-16 05:50 UTC378
        if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC379
        for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC380
      }
05668b0d4753atagen2026-10-16 05:50 UTC381
    for (let k = 2; k <= n; k++) {
05668b0d4753atagen2026-10-16 05:50 UTC382
      const s = (hi - lo - (k - 1) * G) / k;
05668b0d4753atagen2026-10-16 05:50 UTC383
      if (s < 6) break;
05668b0d4753atagen2026-10-16 05:50 UTC384
      const lane = new Array(n).fill(-1);
05668b0d4753atagen2026-10-16 05:50 UTC385
      const assign = i => {
05668b0d4753atagen2026-10-16 05:50 UTC386
        if (i === n) return true;
05668b0d4753atagen2026-10-16 05:50 UTC387
        for (let l = 0; l < k; l++) {
05668b0d4753atagen2026-10-16 05:50 UTC388
          let ok = true;
05668b0d4753atagen2026-10-16 05:50 UTC389
          for (let j = 0; j < i && ok; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC390
            if (diff[i][j] && lane[j] === l) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC391
            if (lt[i][j] && !(l < lane[j])) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC392
            if (lt[j][i] && !(lane[j] < l)) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC393
          }
05668b0d4753atagen2026-10-16 05:50 UTC394
          if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; }
05668b0d4753atagen2026-10-16 05:50 UTC395
        }
05668b0d4753atagen2026-10-16 05:50 UTC396
        return false;
05668b0d4753atagen2026-10-16 05:50 UTC397
      };
05668b0d4753atagen2026-10-16 05:50 UTC398
      if (!assign(0)) continue;
05668b0d4753atagen2026-10-16 05:50 UTC399
      const strip = l => lo + l * (s + G);
05668b0d4753atagen2026-10-16 05:50 UTC400
      const inSet = new Set(set);
05668b0d4753atagen2026-10-16 05:50 UTC401
      for (const phase of [2, 1, 3]) {
05668b0d4753atagen2026-10-16 05:50 UTC402
        const K1 = {}, K2 = {};
05668b0d4753atagen2026-10-16 05:50 UTC403
        for (const id of list) {
05668b0d4753atagen2026-10-16 05:50 UTC404
          const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC405
          if (inSet.has(id)) {
05668b0d4753atagen2026-10-16 05:50 UTC406
            const l = lane[set.indexOf(id)];
05668b0d4753atagen2026-10-16 05:50 UTC407
            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 UTC408
            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 UTC409
          } else if (phase === 1) { K1[id] = t; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC410
          else if (phase === 2) { K1[id] = f; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC411
          else { K1[id] = f; K2[id] = f; }
05668b0d4753atagen2026-10-16 05:50 UTC412
        }
cd7085e54c88atagen2026-10-16 06:58 UTC413
        if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list) && pureOk([from, K1, K2, to]))
05668b0d4753atagen2026-10-16 05:50 UTC414
          return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC415
      }
05668b0d4753atagen2026-10-16 05:50 UTC416
    }
05668b0d4753atagen2026-10-16 05:50 UTC417
  }
05668b0d4753atagen2026-10-16 05:50 UTC418
  return null;
05668b0d4753atagen2026-10-16 05:50 UTC419
}
05668b0d4753atagen2026-10-16 05:50 UTC420
05668b0d4753atagen2026-10-16 05:50 UTC421
function pinch(from, to, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC422
  const K1 = { ...from }, K2 = { ...to };
05668b0d4753atagen2026-10-16 05:50 UTC423
  for (const id of movers) {
05668b0d4753atagen2026-10-16 05:50 UTC424
    const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC425
    K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC426
    K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC427
  }
05668b0d4753atagen2026-10-16 05:50 UTC428
  return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC429
}
05668b0d4753atagen2026-10-16 05:50 UTC430
05668b0d4753atagen2026-10-16 05:50 UTC431
function bbox(rects) {
05668b0d4753atagen2026-10-16 05:50 UTC432
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC433
  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 UTC434
  return R(x0, y0, x1 - x0, y1 - y0);
05668b0d4753atagen2026-10-16 05:50 UTC435
}
05668b0d4753atagen2026-10-16 05:50 UTC436
function planBlocks(from, to, groups) {
05668b0d4753atagen2026-10-16 05:50 UTC437
  const bf = {}, bt = {};
05668b0d4753atagen2026-10-16 05:50 UTC438
  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 UTC439
  const bp = plan(bf, bt);
05668b0d4753atagen2026-10-16 05:50 UTC440
  const last = bp.keys.length - 1;
05668b0d4753atagen2026-10-16 05:50 UTC441
  const keys = bp.keys.map((K, ki) => {
05668b0d4753atagen2026-10-16 05:50 UTC442
    if (ki === 0) return from;
05668b0d4753atagen2026-10-16 05:50 UTC443
    if (ki === last) return to;
05668b0d4753atagen2026-10-16 05:50 UTC444
    const L = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC445
    for (const b in groups) for (const id of groups[b]) {
05668b0d4753atagen2026-10-16 05:50 UTC446
      const r = from[id], box = bf[b], k = K[b];
05668b0d4753atagen2026-10-16 05:50 UTC447
      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 UTC448
      const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC449
      L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h);
05668b0d4753atagen2026-10-16 05:50 UTC450
    }
05668b0d4753atagen2026-10-16 05:50 UTC451
    return L;
05668b0d4753atagen2026-10-16 05:50 UTC452
  });
05668b0d4753atagen2026-10-16 05:50 UTC453
  return { kind: bp.kind + ' on blocks', keys, durs: bp.durs };
05668b0d4753atagen2026-10-16 05:50 UTC454
}
05668b0d4753atagen2026-10-16 05:50 UTC455
05668b0d4753atagen2026-10-16 05:50 UTC456
function bezier(x1, y1, x2, y2) {
05668b0d4753atagen2026-10-16 05:50 UTC457
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
05668b0d4753atagen2026-10-16 05:50 UTC458
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
05668b0d4753atagen2026-10-16 05:50 UTC459
  const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u;
05668b0d4753atagen2026-10-16 05:50 UTC460
  const dX = u => (3 * ax * u + 2 * bx) * u + cx;
05668b0d4753atagen2026-10-16 05:50 UTC461
  return t => {
05668b0d4753atagen2026-10-16 05:50 UTC462
    if (t <= 0) return 0;
05668b0d4753atagen2026-10-16 05:50 UTC463
    if (t >= 1) return 1;
05668b0d4753atagen2026-10-16 05:50 UTC464
    let u = t;
05668b0d4753atagen2026-10-16 05:50 UTC465
    for (let i = 0; i < 8; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC466
      const x = X(u) - t;
05668b0d4753atagen2026-10-16 05:50 UTC467
      if (Math.abs(x) < 1e-6) return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC468
      const d = dX(u);
05668b0d4753atagen2026-10-16 05:50 UTC469
      if (Math.abs(d) < 1e-6) break;
05668b0d4753atagen2026-10-16 05:50 UTC470
      u -= x / d;
05668b0d4753atagen2026-10-16 05:50 UTC471
    }
05668b0d4753atagen2026-10-16 05:50 UTC472
    let lo = 0, hi = 1; u = t;
05668b0d4753atagen2026-10-16 05:50 UTC473
    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 UTC474
    return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC475
  };
05668b0d4753atagen2026-10-16 05:50 UTC476
}
05668b0d4753atagen2026-10-16 05:50 UTC477
const BASE = bezier(0.45, 0, 0.2, 1);
05668b0d4753atagen2026-10-16 05:50 UTC478
const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h];
05668b0d4753atagen2026-10-16 05:50 UTC479
</script>
05668b0d4753atagen2026-10-16 05:50 UTC480
05668b0d4753atagen2026-10-16 05:50 UTC481
<script>
05668b0d4753atagen2026-10-16 05:50 UTC482
const PAL = { A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' };
05668b0d4753atagen2026-10-16 05:50 UTC483
const demos = [];
05668b0d4753atagen2026-10-16 05:50 UTC484
05668b0d4753atagen2026-10-16 05:50 UTC485
class Demo {
05668b0d4753atagen2026-10-16 05:50 UTC486
  constructor(id, W, H, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC487
    this.W = W; this.H = H;
05668b0d4753atagen2026-10-16 05:50 UTC488
    this.outputs = opts.outputs || [R(0, 0, W, H)];
cd7085e54c88atagen2026-10-16 06:58 UTC489
    this.floating = new Set(opts.floating || []);
05668b0d4753atagen2026-10-16 05:50 UTC490
    const el = document.getElementById(id);
05668b0d4753atagen2026-10-16 05:50 UTC491
    this.canvas = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC492
    const dpr = window.devicePixelRatio || 1;
05668b0d4753atagen2026-10-16 05:50 UTC493
    this.canvas.width = W * dpr; this.canvas.height = H * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC494
    this.canvas.style.maxWidth = W + 'px';
05668b0d4753atagen2026-10-16 05:50 UTC495
    this.ctx = this.canvas.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC496
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC497
    el.appendChild(this.canvas);
05668b0d4753atagen2026-10-16 05:50 UTC498
    if (opts.graph) {
05668b0d4753atagen2026-10-16 05:50 UTC499
      this.graph = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC500
      this.graph.width = W * dpr; this.graph.height = 90 * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC501
      this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px';
05668b0d4753atagen2026-10-16 05:50 UTC502
      this.gctx = this.graph.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC503
      this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC504
      el.appendChild(this.graph);
05668b0d4753atagen2026-10-16 05:50 UTC505
      this.hist = [];
05668b0d4753atagen2026-10-16 05:50 UTC506
    }
05668b0d4753atagen2026-10-16 05:50 UTC507
    this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls);
05668b0d4753atagen2026-10-16 05:50 UTC508
    this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status);
05668b0d4753atagen2026-10-16 05:50 UTC509
    this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle';
05668b0d4753atagen2026-10-16 05:50 UTC510
    demos.push(this);
05668b0d4753atagen2026-10-16 05:50 UTC511
  }
05668b0d4753atagen2026-10-16 05:50 UTC512
  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 UTC513
  check(label, init, fn) {
05668b0d4753atagen2026-10-16 05:50 UTC514
    const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init;
05668b0d4753atagen2026-10-16 05:50 UTC515
    c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c;
05668b0d4753atagen2026-10-16 05:50 UTC516
  }
05668b0d4753atagen2026-10-16 05:50 UTC517
  set(L) { this.layout = clone(L); this.anim = null; }
05668b0d4753atagen2026-10-16 05:50 UTC518
  go(target, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC519
    const now = performance.now();
05668b0d4753atagen2026-10-16 05:50 UTC520
    const cur = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC521
    const from = {};
05668b0d4753atagen2026-10-16 05:50 UTC522
    for (const id in target) from[id] = cur[id] || target[id];
cd7085e54c88atagen2026-10-16 06:58 UTC523
    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 UTC524
    const curves = p.durs.map(() => BASE);
05668b0d4753atagen2026-10-16 05:50 UTC525
    const interrupted = !!this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC526
    let matched = false;
05668b0d4753atagen2026-10-16 05:50 UTC527
    if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) {
05668b0d4753atagen2026-10-16 05:50 UTC528
      const K1 = p.keys[1];
05668b0d4753atagen2026-10-16 05:50 UTC529
      let best = null, bs = 0;
05668b0d4753atagen2026-10-16 05:50 UTC530
      for (const id in K1) {
05668b0d4753atagen2026-10-16 05:50 UTC531
        const v = this.vel[id]; if (!v) continue;
05668b0d4753atagen2026-10-16 05:50 UTC532
        const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]);
05668b0d4753atagen2026-10-16 05:50 UTC533
        const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue;
05668b0d4753atagen2026-10-16 05:50 UTC534
        const sp = Math.hypot(...v);
05668b0d4753atagen2026-10-16 05:50 UTC535
        if (sp > bs) { bs = sp; best = { v, D, dd }; }
05668b0d4753atagen2026-10-16 05:50 UTC536
      }
05668b0d4753atagen2026-10-16 05:50 UTC537
      if (best) {
05668b0d4753atagen2026-10-16 05:50 UTC538
        const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd;
05668b0d4753atagen2026-10-16 05:50 UTC539
        const slope = s * p.durs[0];
05668b0d4753atagen2026-10-16 05:50 UTC540
        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 UTC541
      }
05668b0d4753atagen2026-10-16 05:50 UTC542
    }
05668b0d4753atagen2026-10-16 05:50 UTC543
    this.anim = { keys: p.keys, durs: p.durs, curves, start: now };
05668b0d4753atagen2026-10-16 05:50 UTC544
    this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : '');
05668b0d4753atagen2026-10-16 05:50 UTC545
    this.overlapSeen = false;
05668b0d4753atagen2026-10-16 05:50 UTC546
  }
05668b0d4753atagen2026-10-16 05:50 UTC547
  sampleAt(t) {
05668b0d4753atagen2026-10-16 05:50 UTC548
    if (!this.anim) return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC549
    let e = t - this.anim.start;
05668b0d4753atagen2026-10-16 05:50 UTC550
    const { keys, durs, curves } = this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC551
    for (let i = 0; i < durs.length; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC552
      const d = durs[i];
05668b0d4753atagen2026-10-16 05:50 UTC553
      if (e < d) {
05668b0d4753atagen2026-10-16 05:50 UTC554
        this.phase = i;
05668b0d4753atagen2026-10-16 05:50 UTC555
        const s = curves[i](e / d);
05668b0d4753atagen2026-10-16 05:50 UTC556
        const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC557
        for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s);
05668b0d4753atagen2026-10-16 05:50 UTC558
        return L;
05668b0d4753atagen2026-10-16 05:50 UTC559
      }
05668b0d4753atagen2026-10-16 05:50 UTC560
      e -= d;
05668b0d4753atagen2026-10-16 05:50 UTC561
    }
05668b0d4753atagen2026-10-16 05:50 UTC562
    this.layout = clone(keys[keys.length - 1]);
05668b0d4753atagen2026-10-16 05:50 UTC563
    this.anim = null;
05668b0d4753atagen2026-10-16 05:50 UTC564
    return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC565
  }
05668b0d4753atagen2026-10-16 05:50 UTC566
  frame(now) {
05668b0d4753atagen2026-10-16 05:50 UTC567
    const L = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC568
    if (this.prev) {
05668b0d4753atagen2026-10-16 05:50 UTC569
      const dt = now - this.prev.t;
05668b0d4753atagen2026-10-16 05:50 UTC570
      if (dt > 0) for (const id in L) if (this.prev.L[id]) {
05668b0d4753atagen2026-10-16 05:50 UTC571
        const a = edges(this.prev.L[id]), b = edges(L[id]);
05668b0d4753atagen2026-10-16 05:50 UTC572
        this.vel[id] = b.map((v, i) => (v - a[i]) / dt);
05668b0d4753atagen2026-10-16 05:50 UTC573
      }
05668b0d4753atagen2026-10-16 05:50 UTC574
    }
05668b0d4753atagen2026-10-16 05:50 UTC575
    this.prev = { t: now, L };
05668b0d4753atagen2026-10-16 05:50 UTC576
    this.draw(L);
05668b0d4753atagen2026-10-16 05:50 UTC577
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC578
    if (overlaps.length) this.overlapSeen = true;
05668b0d4753atagen2026-10-16 05:50 UTC579
    const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : '';
05668b0d4753atagen2026-10-16 05:50 UTC580
    this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : '');
05668b0d4753atagen2026-10-16 05:50 UTC581
    if (this.graph) this.drawGraph(now);
05668b0d4753atagen2026-10-16 05:50 UTC582
  }
05668b0d4753atagen2026-10-16 05:50 UTC583
  findOverlaps(L) {
cd7085e54c88atagen2026-10-16 06:58 UTC584
    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 UTC585
    const out = [];
05668b0d4753atagen2026-10-16 05:50 UTC586
    for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC587
      const a = L[ids[i]], b = L[ids[j]];
05668b0d4753atagen2026-10-16 05:50 UTC588
      const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
05668b0d4753atagen2026-10-16 05:50 UTC589
      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 UTC590
      if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0));
05668b0d4753atagen2026-10-16 05:50 UTC591
    }
05668b0d4753atagen2026-10-16 05:50 UTC592
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC593
  }
05668b0d4753atagen2026-10-16 05:50 UTC594
  draw(L) {
05668b0d4753atagen2026-10-16 05:50 UTC595
    const c = this.ctx;
05668b0d4753atagen2026-10-16 05:50 UTC596
    c.clearRect(0, 0, this.W, this.H);
05668b0d4753atagen2026-10-16 05:50 UTC597
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC598
    for (const o of this.outputs) {
05668b0d4753atagen2026-10-16 05:50 UTC599
      c.save();
05668b0d4753atagen2026-10-16 05:50 UTC600
      c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h);
05668b0d4753atagen2026-10-16 05:50 UTC601
      c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip();
cd7085e54c88atagen2026-10-16 06:58 UTC602
      for (const id of Object.keys(L).sort((a, b) => this.floating.has(a) - this.floating.has(b))) {
05668b0d4753atagen2026-10-16 05:50 UTC603
        const r = L[id];
05668b0d4753atagen2026-10-16 05:50 UTC604
        if (r.w < 0.5 || r.h < 0.5) continue;
05668b0d4753atagen2026-10-16 05:50 UTC605
        const rad = Math.min(8, r.w / 2, r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC606
        c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad);
05668b0d4753atagen2026-10-16 05:50 UTC607
        c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill();
05668b0d4753atagen2026-10-16 05:50 UTC608
        c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC609
        if (r.w > 22 && r.h > 22) {
05668b0d4753atagen2026-10-16 05:50 UTC610
          c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle';
05668b0d4753atagen2026-10-16 05:50 UTC611
          c.fillText(id, r.x + r.w / 2, r.y + r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC612
        }
05668b0d4753atagen2026-10-16 05:50 UTC613
      }
05668b0d4753atagen2026-10-16 05:50 UTC614
      for (const o2 of overlaps) { c.fillStyle = 'rgba(255,77,94,0.55)'; c.fillRect(o2.x, o2.y, o2.w, o2.h); c.strokeStyle = '#ff4d5e'; c.lineWidth = 2; c.strokeRect(o2.x, o2.y, o2.w, o2.h); }
05668b0d4753atagen2026-10-16 05:50 UTC615
      c.restore();
05668b0d4753atagen2026-10-16 05:50 UTC616
    }
05668b0d4753atagen2026-10-16 05:50 UTC617
  }
05668b0d4753atagen2026-10-16 05:50 UTC618
  drawGraph(now) {
05668b0d4753atagen2026-10-16 05:50 UTC619
    const v = this.vel.A;
05668b0d4753atagen2026-10-16 05:50 UTC620
    const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0;
05668b0d4753atagen2026-10-16 05:50 UTC621
    this.hist.push([now, sp]);
05668b0d4753atagen2026-10-16 05:50 UTC622
    while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift();
05668b0d4753atagen2026-10-16 05:50 UTC623
    const c = this.gctx, W = this.W, H = 90;
05668b0d4753atagen2026-10-16 05:50 UTC624
    c.clearRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC625
    c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC626
    const max = Math.max(400, ...this.hist.map(h => h[1]));
05668b0d4753atagen2026-10-16 05:50 UTC627
    c.beginPath();
05668b0d4753atagen2026-10-16 05:50 UTC628
    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 UTC629
    c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC630
    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 UTC631
  }
05668b0d4753atagen2026-10-16 05:50 UTC632
}
05668b0d4753atagen2026-10-16 05:50 UTC633
05668b0d4753atagen2026-10-16 05:50 UTC634
const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G);
05668b0d4753atagen2026-10-16 05:50 UTC635
05668b0d4753atagen2026-10-16 05:50 UTC636
{ // d1 swap
05668b0d4753atagen2026-10-16 05:50 UTC637
  const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC638
  let swapped = false;
05668b0d4753atagen2026-10-16 05:50 UTC639
  const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']);
05668b0d4753atagen2026-10-16 05:50 UTC640
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC641
  d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC642
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC643
}
05668b0d4753atagen2026-10-16 05:50 UTC644
05668b0d4753atagen2026-10-16 05:50 UTC645
{ // d2 one phase
05668b0d4753atagen2026-10-16 05:50 UTC646
  const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC647
  const s = { wide: false, d: false, e: false };
cd7085e54c88atagen2026-10-16 06:58 UTC648
  const modes = ['line on old boundary', 'point on old boundary', 'target centre'];
cd7085e54c88atagen2026-10-16 06:58 UTC649
  let mode = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC650
  const ratios = ids => ids.map(id => (id === 'A' && s.wide) ? 2 : 1);
05668b0d4753atagen2026-10-16 05:50 UTC651
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC652
    const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C'];
cd7085e54c88atagen2026-10-16 06:58 UTC653
    const out = splitX(I, ids, ratios(ids));
cd7085e54c88atagen2026-10-16 06:58 UTC654
    if (!s.d) {
cd7085e54c88atagen2026-10-16 06:58 UTC655
      const bx = (out.B.x + out.B.w + out.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC656
      const T = splitX(I, ['A', 'B', 'D', 'C'], ratios(['A', 'B', 'D', 'C'])).D;
cd7085e54c88atagen2026-10-16 06:58 UTC657
      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 UTC658
    }
05668b0d4753atagen2026-10-16 05:50 UTC659
    const b = out.B;
05668b0d4753atagen2026-10-16 05:50 UTC660
    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 UTC661
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC662
  };
05668b0d4753atagen2026-10-16 05:50 UTC663
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC664
  d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC665
  d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC666
  d.button('Split B with E', () => { s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC667
  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 UTC668
  const mb = d.button('seed: ' + modes[0], () => {
cd7085e54c88atagen2026-10-16 06:58 UTC669
    mode = (mode + 1) % modes.length; mb.textContent = 'seed: ' + modes[mode];
cd7085e54c88atagen2026-10-16 06:58 UTC670
    if (!s.d && !d.anim) d.set(L());
cd7085e54c88atagen2026-10-16 06:58 UTC671
  });
05668b0d4753atagen2026-10-16 05:50 UTC672
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC673
}
05668b0d4753atagen2026-10-16 05:50 UTC674
function tagLayout(I, view2, order) {
05668b0d4753atagen2026-10-16 05:50 UTC675
  const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC676
  if (view2) {
05668b0d4753atagen2026-10-16 05:50 UTC677
    const rows = splitY(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC678
    Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC679
  } else {
05668b0d4753atagen2026-10-16 05:50 UTC680
    Object.assign(L, splitX(I, ['A', 'B', 'C']));
05668b0d4753atagen2026-10-16 05:50 UTC681
    const y = order[0] === 't2' ? I.y : I.y + I.h;
05668b0d4753atagen2026-10-16 05:50 UTC682
    Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC683
  }
05668b0d4753atagen2026-10-16 05:50 UTC684
  return L;
05668b0d4753atagen2026-10-16 05:50 UTC685
}
05668b0d4753atagen2026-10-16 05:50 UTC686
05668b0d4753atagen2026-10-16 05:50 UTC687
{ // d3 tag toggle
05668b0d4753atagen2026-10-16 05:50 UTC688
  const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC689
  let view2 = false;
05668b0d4753atagen2026-10-16 05:50 UTC690
  d.set(tagLayout(I, view2, ['t1', 't2']));
05668b0d4753atagen2026-10-16 05:50 UTC691
  d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); });
05668b0d4753atagen2026-10-16 05:50 UTC692
}
05668b0d4753atagen2026-10-16 05:50 UTC693
05668b0d4753atagen2026-10-16 05:50 UTC694
{ // d3b tag reorder
05668b0d4753atagen2026-10-16 05:50 UTC695
  const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC696
  let order = ['t1', 't2'], blocks = true;
05668b0d4753atagen2026-10-16 05:50 UTC697
  d.set(tagLayout(I, true, order));
05668b0d4753atagen2026-10-16 05:50 UTC698
  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 UTC699
  d.check('rigid blocks', true, v => blocks = v);
05668b0d4753atagen2026-10-16 05:50 UTC700
}
05668b0d4753atagen2026-10-16 05:50 UTC701
05668b0d4753atagen2026-10-16 05:50 UTC702
{ // d4a orientation flip
05668b0d4753atagen2026-10-16 05:50 UTC703
  const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC704
  let rows = false;
05668b0d4753atagen2026-10-16 05:50 UTC705
  const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']);
05668b0d4753atagen2026-10-16 05:50 UTC706
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC707
  d.button('Flip orientation', () => { rows = !rows; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC708
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC709
}
05668b0d4753atagen2026-10-16 05:50 UTC710
05668b0d4753atagen2026-10-16 05:50 UTC711
{ // d4b grow both axes
05668b0d4753atagen2026-10-16 05:50 UTC712
  const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC713
  let wide = false;
05668b0d4753atagen2026-10-16 05:50 UTC714
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC715
    const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]);
05668b0d4753atagen2026-10-16 05:50 UTC716
    return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) };
05668b0d4753atagen2026-10-16 05:50 UTC717
  };
05668b0d4753atagen2026-10-16 05:50 UTC718
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC719
  d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC720
}
05668b0d4753atagen2026-10-16 05:50 UTC721
05668b0d4753atagen2026-10-16 05:50 UTC722
{ // d5 lanes
05668b0d4753atagen2026-10-16 05:50 UTC723
  const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC724
  let order = ['A', 'B', 'C', 'D'];
05668b0d4753atagen2026-10-16 05:50 UTC725
  const L = () => splitX(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC726
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC727
  d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC728
  d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC729
  d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC730
  d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC731
}
05668b0d4753atagen2026-10-16 05:50 UTC732
05668b0d4753atagen2026-10-16 05:50 UTC733
{ // d6 interrupts
05668b0d4753atagen2026-10-16 05:50 UTC734
  const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC735
  const targets = [
05668b0d4753atagen2026-10-16 05:50 UTC736
    () => splitX(I, ['A', 'B', 'C']),
05668b0d4753atagen2026-10-16 05:50 UTC737
    () => splitX(I, ['C', 'A', 'B']),
05668b0d4753atagen2026-10-16 05:50 UTC738
    () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]),
05668b0d4753atagen2026-10-16 05:50 UTC739
    () => splitX(I, ['B', 'C', 'A']),
05668b0d4753atagen2026-10-16 05:50 UTC740
    () => ({ ...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 UTC741
  ];
05668b0d4753atagen2026-10-16 05:50 UTC742
  const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
05668b0d4753atagen2026-10-16 05:50 UTC743
  let idx = 0;
05668b0d4753atagen2026-10-16 05:50 UTC744
  d.set(withB(targets[0]()));
05668b0d4753atagen2026-10-16 05:50 UTC745
  const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); };
05668b0d4753atagen2026-10-16 05:50 UTC746
  d.button('Next target', next);
05668b0d4753atagen2026-10-16 05:50 UTC747
  d.button('Storm (8 rapid interrupts)', () => {
05668b0d4753atagen2026-10-16 05:50 UTC748
    let n = 0;
05668b0d4753atagen2026-10-16 05:50 UTC749
    const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); };
05668b0d4753atagen2026-10-16 05:50 UTC750
    fire();
05668b0d4753atagen2026-10-16 05:50 UTC751
  });
05668b0d4753atagen2026-10-16 05:50 UTC752
  d.check('match velocity', true, v => d.vmatch = v);
05668b0d4753atagen2026-10-16 05:50 UTC753
}
05668b0d4753atagen2026-10-16 05:50 UTC754
05668b0d4753atagen2026-10-16 05:50 UTC755
{ // d7 across outputs
05668b0d4753atagen2026-10-16 05:50 UTC756
  const W = 680, H = 260;
05668b0d4753atagen2026-10-16 05:50 UTC757
  const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H);
05668b0d4753atagen2026-10-16 05:50 UTC758
  const d = new Demo('d7', W, H, { outputs: [o1, o2] });
05668b0d4753atagen2026-10-16 05:50 UTC759
  const I1 = inner(320, H, 0), I2 = inner(320, H, 360);
05668b0d4753atagen2026-10-16 05:50 UTC760
  let s1 = ['A', 'B'], s2 = ['C'];
05668b0d4753atagen2026-10-16 05:50 UTC761
  const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) });
05668b0d4753atagen2026-10-16 05:50 UTC762
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC763
  d.button('Move B across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC764
    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 UTC765
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC766
  });
05668b0d4753atagen2026-10-16 05:50 UTC767
  d.button('Move A across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC768
    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 UTC769
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC770
  });
05668b0d4753atagen2026-10-16 05:50 UTC771
  d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC772
}
05668b0d4753atagen2026-10-16 05:50 UTC773
05668b0d4753atagen2026-10-16 05:50 UTC774
function layoutTree(node, rect, visible, out) {
05668b0d4753atagen2026-10-16 05:50 UTC775
  if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; }
05668b0d4753atagen2026-10-16 05:50 UTC776
  const horiz = node.dir === 'row';
05668b0d4753atagen2026-10-16 05:50 UTC777
  const shown = node.kids.map(k => anyVisible(k, visible));
05668b0d4753atagen2026-10-16 05:50 UTC778
  const m = shown.filter(Boolean).length;
05668b0d4753atagen2026-10-16 05:50 UTC779
  const len = horiz ? rect.w : rect.h;
05668b0d4753atagen2026-10-16 05:50 UTC780
  const avail = len - G * Math.max(0, m - 1);
05668b0d4753atagen2026-10-16 05:50 UTC781
  let pos = horiz ? rect.x : rect.y, seen = 0;
05668b0d4753atagen2026-10-16 05:50 UTC782
  node.kids.forEach((k, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC783
    if (!shown[i]) {
05668b0d4753atagen2026-10-16 05:50 UTC784
      const p = seen ? pos - G / 2 : pos;
05668b0d4753atagen2026-10-16 05:50 UTC785
      const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC786
      layoutTree(k, r, () => false, out);
05668b0d4753atagen2026-10-16 05:50 UTC787
      return;
05668b0d4753atagen2026-10-16 05:50 UTC788
    }
05668b0d4753atagen2026-10-16 05:50 UTC789
    const size = avail / m;
05668b0d4753atagen2026-10-16 05:50 UTC790
    const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size);
05668b0d4753atagen2026-10-16 05:50 UTC791
    layoutTree(k, r, visible, out);
05668b0d4753atagen2026-10-16 05:50 UTC792
    pos += size + G; seen++;
05668b0d4753atagen2026-10-16 05:50 UTC793
  });
05668b0d4753atagen2026-10-16 05:50 UTC794
}
05668b0d4753atagen2026-10-16 05:50 UTC795
function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); }
05668b0d4753atagen2026-10-16 05:50 UTC796
05668b0d4753atagen2026-10-16 05:50 UTC797
{ // d8 one tree, tags filter
05668b0d4753atagen2026-10-16 05:50 UTC798
  const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC799
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
05668b0d4753atagen2026-10-16 05:50 UTC800
  const view = { 1: true, 2: true };
05668b0d4753atagen2026-10-16 05:50 UTC801
  let mixed = false;
05668b0d4753atagen2026-10-16 05:50 UTC802
  const tree = () => mixed
05668b0d4753atagen2026-10-16 05:50 UTC803
    ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] }
05668b0d4753atagen2026-10-16 05:50 UTC804
    : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] };
05668b0d4753atagen2026-10-16 05:50 UTC805
  const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; };
05668b0d4753atagen2026-10-16 05:50 UTC806
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC807
  d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC808
  d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC809
  d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC810
}
05668b0d4753atagen2026-10-16 05:50 UTC811
cd7085e54c88atagen2026-10-16 06:58 UTC812
function stackLayouts(I) {
cd7085e54c88atagen2026-10-16 06:58 UTC813
  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 UTC814
  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 UTC815
  return {
cd7085e54c88atagen2026-10-16 06:58 UTC816
    'A | B | C': () => splitX(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC817
    'A | (B / C)': () => col(splitX(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC818
    '(A / C) | B': () => col(splitX(I, ['L', 'B']), 'L', ['A', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC819
    '(A / B) | C': () => col(splitX(I, ['L', 'C']), 'L', ['A', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC820
    'B | C | A': () => splitX(I, ['B', 'C', 'A']),
cd7085e54c88atagen2026-10-16 06:58 UTC821
    'A / B / C': () => splitY(I, ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC822
    'A / (B | C)': () => row(splitY(I, ['A', 'R']), 'R', ['B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC823
    '(A / B) | (C / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'B']), 'R', ['C', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC824
    'A | (C / B / D)': () => col(splitX(I, ['A', 'R']), 'R', ['C', 'B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC825
    '(A / D) | (C / B)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'D']), 'R', ['C', 'B']),
cd7085e54c88atagen2026-10-16 06:58 UTC826
    '(A / B / C) | D': () => col(splitX(I, ['L', 'D']), 'L', ['A', 'B', 'C']),
cd7085e54c88atagen2026-10-16 06:58 UTC827
    '(A / C) | (B / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'C']), 'R', ['B', 'D']),
cd7085e54c88atagen2026-10-16 06:58 UTC828
  };
cd7085e54c88atagen2026-10-16 06:58 UTC829
}
cd7085e54c88atagen2026-10-16 06:58 UTC830
cd7085e54c88atagen2026-10-16 06:58 UTC831
for (const [id, names] of [
cd7085e54c88atagen2026-10-16 06:58 UTC832
  ['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 UTC833
  ['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 UTC834
]) {
cd7085e54c88atagen2026-10-16 06:58 UTC835
  const W = 640, H = 300, d = new Demo(id, W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC836
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC837
  for (const n of names) d.button(n, () => d.go(S[n]()));
cd7085e54c88atagen2026-10-16 06:58 UTC838
}
cd7085e54c88atagen2026-10-16 06:58 UTC839
cd7085e54c88atagen2026-10-16 06:58 UTC840
{ // d9c grid
cd7085e54c88atagen2026-10-16 06:58 UTC841
  const W = 640, H = 300, d = new Demo('d9c', W, H), I = inner(W, H), S = stackLayouts(I);
cd7085e54c88atagen2026-10-16 06:58 UTC842
  let at = 0;
cd7085e54c88atagen2026-10-16 06:58 UTC843
  const names = ['(A / D) | (C / B)', '(A / B / C) | D'];
cd7085e54c88atagen2026-10-16 06:58 UTC844
  d.set(S[names[0]]());
cd7085e54c88atagen2026-10-16 06:58 UTC845
  d.button('(A / D) | (C / B)  ⇄  (A / B / C) | D', () => { at = 1 - at; d.go(S[names[at]]()); });
cd7085e54c88atagen2026-10-16 06:58 UTC846
}
cd7085e54c88atagen2026-10-16 06:58 UTC847
cd7085e54c88atagen2026-10-16 06:58 UTC848
{ // d10 floating to tiled
cd7085e54c88atagen2026-10-16 06:58 UTC849
  const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H);
cd7085e54c88atagen2026-10-16 06:58 UTC850
  const FL = R(220, 40, 220, 130);
cd7085e54c88atagen2026-10-16 06:58 UTC851
  let tiled = false;
cd7085e54c88atagen2026-10-16 06:58 UTC852
  const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']);
cd7085e54c88atagen2026-10-16 06:58 UTC853
  const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL });
cd7085e54c88atagen2026-10-16 06:58 UTC854
  const ortho = () => STYLE === 'orthogonal';
cd7085e54c88atagen2026-10-16 06:58 UTC855
  const assemble = (n, tiledAt, fAt, kind) => {
cd7085e54c88atagen2026-10-16 06:58 UTC856
    const keys = [];
cd7085e54c88atagen2026-10-16 06:58 UTC857
    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 UTC858
    return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) };
cd7085e54c88atagen2026-10-16 06:58 UTC859
  };
cd7085e54c88atagen2026-10-16 06:58 UTC860
  const floatIn = from => {
cd7085e54c88atagen2026-10-16 06:58 UTC861
    const slot = tiledL().F, to = tiledL();
cd7085e54c88atagen2026-10-16 06:58 UTC862
    const bx = (from.B.x + from.B.w + from.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC863
    const tp = plan({ A: from.A, B: from.B, C: from.C, P: R(bx, I.y, 0, I.h) }, { A: to.A, B: to.B, C: to.C, P: slot });
cd7085e54c88atagen2026-10-16 06:58 UTC864
    const k = tp.durs.length, f = from.F;
cd7085e54c88atagen2026-10-16 06:58 UTC865
    const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC866
    const path = ortho() ? [f, R(cx, f.y, f.w, f.h), R(cx, cy, f.w, f.h)] : [f, R(cx, cy, f.w, f.h)];
cd7085e54c88atagen2026-10-16 06:58 UTC867
    const n = Math.max(path.length - 1, k) + 1;
cd7085e54c88atagen2026-10-16 06:58 UTC868
    return assemble(n, i => tp.keys[Math.min(i, k)], i => i === n ? slot : path[Math.min(i, path.length - 1)], `float in, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC869
  };
cd7085e54c88atagen2026-10-16 06:58 UTC870
  const floatOut = from => {
cd7085e54c88atagen2026-10-16 06:58 UTC871
    const slot = from.F, to = floatL();
cd7085e54c88atagen2026-10-16 06:58 UTC872
    const bx = (to.B.x + to.B.w + to.C.x) / 2;
cd7085e54c88atagen2026-10-16 06:58 UTC873
    const tp = plan({ A: from.A, B: from.B, C: from.C, P: slot }, { A: to.A, B: to.B, C: to.C, P: R(bx, I.y, 0, I.h) });
cd7085e54c88atagen2026-10-16 06:58 UTC874
    const k = tp.durs.length;
cd7085e54c88atagen2026-10-16 06:58 UTC875
    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 UTC876
    const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL];
cd7085e54c88atagen2026-10-16 06:58 UTC877
    const n = Math.max(path.length - 1, k + 1);
cd7085e54c88atagen2026-10-16 06:58 UTC878
    return assemble(n, i => tp.keys[Math.max(0, Math.min(i - 1, k))], i => path[Math.min(i, path.length - 1)], `lift out, slot ${tp.kind}`);
cd7085e54c88atagen2026-10-16 06:58 UTC879
  };
cd7085e54c88atagen2026-10-16 06:58 UTC880
  d.set(floatL());
cd7085e54c88atagen2026-10-16 06:58 UTC881
  d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); });
cd7085e54c88atagen2026-10-16 06:58 UTC882
}
cd7085e54c88atagen2026-10-16 06:58 UTC883
05668b0d4753atagen2026-10-16 05:50 UTC884
document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };
05668b0d4753atagen2026-10-16 05:50 UTC885
document.getElementById('style').onchange = e => { STYLE = e.target.value; };
05668b0d4753atagen2026-10-16 05:50 UTC886
const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); };
05668b0d4753atagen2026-10-16 05:50 UTC887
requestAnimationFrame(loop);
05668b0d4753atagen2026-10-16 05:50 UTC888
</script>
05668b0d4753atagen2026-10-16 05:50 UTC889
</body>
05668b0d4753atagen2026-10-16 05:50 UTC890
</html>