atagen

monolith

public
Star0
Watch0
05668b0d47539e4bfea530f662dd9fa046df74b0
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>
05668b0d4753atagen2026-10-16 05:50 UTC37
  <label>style <select id="style"><option value="direct">direct (diagonal when safe)</option><option value="orthogonal">orthogonal (one axis per window per phase)</option></select></label>
05668b0d4753atagen2026-10-16 05:50 UTC38
</div>
05668b0d4753atagen2026-10-16 05:50 UTC39
<main>
05668b0d4753atagen2026-10-16 05:50 UTC40
<h1>monolith planner, draft 1</h1>
05668b0d4753atagen2026-10-16 05:50 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 windows overlap.</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>
05668b0d4753atagen2026-10-16 05:50 UTC49
  <li>An interrupt throws the plan away and replans from what is on screen. Plans never stack.</li>
05668b0d4753atagen2026-10-16 05:50 UTC50
</ul>
05668b0d4753atagen2026-10-16 05:50 UTC51
<p>New requirements wry never met.</p>
05668b0d4753atagen2026-10-16 05:50 UTC52
<ul>
05668b0d4753atagen2026-10-16 05:50 UTC53
  <li>Diagonal moves, moves between outputs, and large groups such as viewing or hiding a tag.</li>
05668b0d4753atagen2026-10-16 05:50 UTC54
  <li>Best-effort velocity continuity when an interrupt lands mid-phase.</li>
05668b0d4753atagen2026-10-16 05:50 UTC55
  <li>A hard bound on plan length, so interrupts can never produce long animations.</li>
05668b0d4753atagen2026-10-16 05:50 UTC56
</ul>
05668b0d4753atagen2026-10-16 05:50 UTC57
05668b0d4753atagen2026-10-16 05:50 UTC58
<h2>2. The problem</h2>
05668b0d4753atagen2026-10-16 05:50 UTC59
<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 UTC60
<div class="demo" id="d1"></div>
05668b0d4753atagen2026-10-16 05:50 UTC61
05668b0d4753atagen2026-10-16 05:50 UTC62
<h2>3. The core rule, shared separators</h2>
05668b0d4753atagen2026-10-16 05:50 UTC63
<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>
05668b0d4753atagen2026-10-16 05:50 UTC64
<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> Diagonal motion is perfectly safe under this rule. wry needed single-axis steps because its solver checked overlap directly. This rule proves it instead.</p>
05668b0d4753atagen2026-10-16 05:50 UTC65
<p>Looking at a single pair gives a lower bound on phases.</p>
05668b0d4753atagen2026-10-16 05:50 UTC66
<table>
05668b0d4753atagen2026-10-16 05:50 UTC67
  <tr><th>pair goes from</th><th>to</th><th>minimum phases</th><th>why</th></tr>
05668b0d4753atagen2026-10-16 05:50 UTC68
  <tr><td>left of</td><td>left of (any sizes)</td><td>1</td><td>relation shared</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC69
  <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 UTC70
  <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 UTC71
</table>
05668b0d4753atagen2026-10-16 05:50 UTC72
<p>The plan length is the worst pair, so <strong>every plan is 1, 2 or 3 phases</strong>, with one fallback described in section 10. 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 constructs plans that satisfy it.</p>
05668b0d4753atagen2026-10-16 05:50 UTC73
05668b0d4753atagen2026-10-16 05:50 UTC74
<h2>4. One phase</h2>
05668b0d4753atagen2026-10-16 05:50 UTC75
<p>Anything that keeps the arrangement of windows intact is a single phase, including changes in both axes at once. That covers ratio changes, inserting and removing windows, splitting a column, and viewing or hiding a tag.</p>
05668b0d4753atagen2026-10-16 05:50 UTC76
<p><strong>Seeds.</strong> A window that appears starts as a zero-size rectangle sitting on the edge where space will open. A closing window ends as one. A seed on an edge is already separated from its neighbours, so it obeys the rule from the first frame. wry grew spawns from the centre of their target, which overlaps the neighbours early in the phase while they are still making room.</p>
05668b0d4753atagen2026-10-16 05:50 UTC77
<div class="demo" id="d2"></div>
05668b0d4753atagen2026-10-16 05:50 UTC78
<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 UTC79
<div class="demo" id="d3"></div>
05668b0d4753atagen2026-10-16 05:50 UTC80
05668b0d4753atagen2026-10-16 05:50 UTC81
<h2>5. Two phases, axis order</h2>
05668b0d4753atagen2026-10-16 05:50 UTC82
<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 UTC83
<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 UTC84
<div class="demo" id="d4a"></div>
05668b0d4753atagen2026-10-16 05:50 UTC85
<p>The style selector at the top matters here. Growing a window in both directions inside a grid is a valid single phase in <em>direct</em> style, because every pair keeps a relation. <em>Orthogonal</em> style forbids moving a window on both axes in one phase, which gives wry's look of making space first and then growing, at the cost of an extra phase.</p>
05668b0d4753atagen2026-10-16 05:50 UTC86
<div class="demo" id="d4b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC87
05668b0d4753atagen2026-10-16 05:50 UTC88
<h2>6. Three phases, lanes</h2>
05668b0d4753atagen2026-10-16 05:50 UTC89
<p>Reversals (left of becomes right of) need two middle keyframes. Windows shrink into <em>lanes</em> across the axis of travel, slide past each other, then expand. Two windows may share a lane only if they never pass each other. Windows stacked on top of each other must keep their stacking order in the lanes, at both ends. 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 full reversal of four windows needs 4.</p>
05668b0d4753atagen2026-10-16 05:50 UTC90
<p>Only windows involved in a reversal enter lanes. Everything else moves in whichever phase validates, preferring the slide phase.</p>
05668b0d4753atagen2026-10-16 05:50 UTC91
<div class="demo" id="d5"></div>
05668b0d4753atagen2026-10-16 05:50 UTC92
05668b0d4753atagen2026-10-16 05:50 UTC93
<h2>7. Tags as rigid blocks</h2>
05668b0d4753atagen2026-10-16 05:50 UTC94
<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 (5 thin strips in the demo below). Planned with each tag as a <em>rigid block</em>, it needs 2 lanes, and the windows ride inside their block. The block planner is the same algorithm one level up, followed by mapping each window into its block's keyframes.</p>
05668b0d4753atagen2026-10-16 05:50 UTC95
<div class="demo" id="d3b"></div>
05668b0d4753atagen2026-10-16 05:50 UTC96
05668b0d4753atagen2026-10-16 05:50 UTC97
<h2>8. Interrupts and velocity</h2>
05668b0d4753atagen2026-10-16 05:50 UTC98
<p>An interrupt samples the current on-screen rectangles and plans from there to the new target. The new plan has at most 3 phases, however many times you interrupt. Try the storm button, which fires a new target every few hundred milliseconds.</p>
05668b0d4753atagen2026-10-16 05:50 UTC99
<p><strong>Velocity.</strong> The base curve starts and ends at rest, so a window that is 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 first control point of the bezier so the initial slope matches. The slope only applies when the first phase continues in the direction things were already moving. Otherwise the window stops and turns, which reads as deliberate. The control point's height is clamped to 1, which mathematically rules out overshoot, since a cubic bezier never leaves the range of its control points.</p>
05668b0d4753atagen2026-10-16 05:50 UTC100
<p>One curve per <em>motion group</em>, never per window. Windows that share an edge must move that edge on the same curve or the edge tears open or overlaps. So the group gets a single curve, matched to its fastest mover. The graph shows window A's speed, and turning matching off shows the drop to zero at every interrupt.</p>
05668b0d4753atagen2026-10-16 05:50 UTC101
<div class="demo" id="d6"></div>
05668b0d4753atagen2026-10-16 05:50 UTC102
05668b0d4753atagen2026-10-16 05:50 UTC103
<h2>9. Across outputs</h2>
05668b0d4753atagen2026-10-16 05:50 UTC104
<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>
05668b0d4753atagen2026-10-16 05:50 UTC105
<p class="note">Not shown. Outputs at different scales draw the same window at different sizes. The planner works in logical coordinates and each output scales its part. If two outputs aren't adjacent in the global space, the window would travel through empty space, and the pinch fallback is probably better there.</p>
05668b0d4753atagen2026-10-16 05:50 UTC106
<div class="demo" id="d7"></div>
05668b0d4753atagen2026-10-16 05:50 UTC107
05668b0d4753atagen2026-10-16 05:50 UTC108
<h2>10. Fallback, pinch</h2>
05668b0d4753atagen2026-10-16 05:50 UTC109
<p>If no 1, 2 or 3 phase plan validates, the windows that can't be routed shrink to a point, jump invisibly, and grow at their target. That's two visible phases, never overlaps, and always works. I couldn't find a tiling transition in these demos that needs it, but the bound has to hold for every input, including arbitrary floating-to-tiled transitions and malformed layouts.</p>
05668b0d4753atagen2026-10-16 05:50 UTC110
05668b0d4753atagen2026-10-16 05:50 UTC111
<h2>11. Algorithm</h2>
05668b0d4753atagen2026-10-16 05:50 UTC112
<pre>plan(from, to):
05668b0d4753atagen2026-10-16 05:50 UTC113
  groups = motion groups (windows whose swept areas can touch, transitively)
05668b0d4753atagen2026-10-16 05:50 UTC114
  for each group:
05668b0d4753atagen2026-10-16 05:50 UTC115
    if every pair shares a relation between from and to
05668b0d4753atagen2026-10-16 05:50 UTC116
       and (style is direct or every window changes only one axis):
05668b0d4753atagen2026-10-16 05:50 UTC117
      return [from, to]                                    # 1 phase
05668b0d4753atagen2026-10-16 05:50 UTC118
    if 2-SAT over axis order finds K1:                     # mixed rects
05668b0d4753atagen2026-10-16 05:50 UTC119
      return [from, K1, to]                                # 2 phases
05668b0d4753atagen2026-10-16 05:50 UTC120
    for lane_axis in [y, x]:                               # strips, then columns
05668b0d4753atagen2026-10-16 05:50 UTC121
      for set in [windows in reversals, all movers, all]:
05668b0d4753atagen2026-10-16 05:50 UTC122
        for k in 2..|set|:
05668b0d4753atagen2026-10-16 05:50 UTC123
          if lanes assignable with k lanes and all three phases validate:
05668b0d4753atagen2026-10-16 05:50 UTC124
            return [from, K1, K2, to]                      # 3 phases
05668b0d4753atagen2026-10-16 05:50 UTC125
    return pinch(from, to)                                 # 2 visible phases
05668b0d4753atagen2026-10-16 05:50 UTC126
05668b0d4753atagen2026-10-16 05:50 UTC127
  rigid blocks: run plan() on block rectangles first, then map windows into them
05668b0d4753atagen2026-10-16 05:50 UTC128
  interrupts:   from = rectangles sampled at the current presentation time
05668b0d4753atagen2026-10-16 05:50 UTC129
  velocity:     first-phase curve per group, slope from the fastest mover,
05668b0d4753atagen2026-10-16 05:50 UTC130
                applied only if moving the same way, control point height ≤ 1
05668b0d4753atagen2026-10-16 05:50 UTC131
  validation:   exact pairwise check over the curve actually used</pre>
05668b0d4753atagen2026-10-16 05:50 UTC132
<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>
05668b0d4753atagen2026-10-16 05:50 UTC133
<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 ≤ 3 phases, and every interrupt for continuity of position.</p>
05668b0d4753atagen2026-10-16 05:50 UTC134
05668b0d4753atagen2026-10-16 05:50 UTC135
<h2>12. Compared with wry master</h2>
05668b0d4753atagen2026-10-16 05:50 UTC136
<table>
05668b0d4753atagen2026-10-16 05:50 UTC137
  <tr><th>wry master</th><th>this design</th></tr>
05668b0d4753atagen2026-10-16 05:50 UTC138
  <tr><td>SingleAction, MixedSinglePhase</td><td>1 phase, now including diagonal moves in direct style</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC139
  <tr><td>SpaceThenOrthogonalGrowth, OrientationChange, HierarchyOrderedScales</td><td>2 phases, one 2-SAT problem instead of three strategies</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC140
  <tr><td>SwapLanes</td><td>3 phases, general lane assignment for any permutation</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC141
  <tr><td>ReversedForwardPlan</td><td>not needed, the construction is symmetric</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC142
  <tr><td>Diagonal falls back to overlapping plain interpolation</td><td>diagonal is legal whenever relations are shared, pinch otherwise</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC143
  <tr><td>Bridged retarget can make plans longer than the original</td><td>every replan is ≤ 3 phases</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC144
  <tr><td>Per-phase duration, plans grow with strategy</td><td>per-phase duration kept, plan length bounded</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC145
  <tr><td>Spawns grow from the target centre</td><td>spawns grow from a seed on the opening edge</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC146
  <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 UTC147
  <tr><td>Cross-output moves skip animation</td><td>planned in global coordinates, clipped per output</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC148
</table>
05668b0d4753atagen2026-10-16 05:50 UTC149
05668b0d4753atagen2026-10-16 05:50 UTC150
<h2>13. Interspersing tags (new, changes the layout model)</h2>
05668b0d4753atagen2026-10-16 05:50 UTC151
<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 four answers.</p>
05668b0d4753atagen2026-10-16 05:50 UTC152
<table>
05668b0d4753atagen2026-10-16 05:50 UTC153
  <tr><th>option</th><th>meaning</th><th>verdict</th></tr>
05668b0d4753atagen2026-10-16 05:50 UTC154
  <tr><td>Forbid it</td><td>tags stay in their rows</td><td>limiting, as you said</td></tr>
05668b0d4753atagen2026-10-16 05:50 UTC155
  <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 UTC156
  <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 UTC157
  <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 UTC158
  <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 UTC159
</table>
05668b0d4753atagen2026-10-16 05:50 UTC160
<p>The last option is dwm's model (one client list, tags as a filter) with a tree in place of the list. Its advantages are one source of truth and no sync problem. Interspersing is just moving a window in the tree. 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 in the same tree. Switching views never rearranges anything.</p>
05668b0d4753atagen2026-10-16 05:50 UTC161
<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. That's consistent, but some will find it surprising. It also reverses an earlier decision. Layout state moves from per tag-on-output to per output.</p>
05668b0d4753atagen2026-10-16 05:50 UTC162
<div class="demo" id="d8"></div>
05668b0d4753atagen2026-10-16 05:50 UTC163
05668b0d4753atagen2026-10-16 05:50 UTC164
<h2>14. Decisions for you</h2>
05668b0d4753atagen2026-10-16 05:50 UTC165
<div class="q"><strong>1. Direct or orthogonal style?</strong> Direct gives fewer phases and diagonal motion when it's safe. Orthogonal is the wry look, where every window moves along one axis at a time. It could be a config option, but the default shapes how monolith feels.</div>
05668b0d4753atagen2026-10-16 05:50 UTC166
<div class="q"><strong>2. Rigid tag blocks?</strong> Should tags move as blocks when their order changes, and should the same apply to any container whose contents keep their arrangement? Blocks are calmer and need fewer lanes. Per-window planning is more uniform.</div>
05668b0d4753atagen2026-10-16 05:50 UTC167
<div class="q"><strong>3. Lane extent.</strong> Lanes currently span only the band the involved windows occupy. Spanning the whole output gives thicker lanes for small windows, but disturbs more of the screen.</div>
05668b0d4753atagen2026-10-16 05:50 UTC168
<div class="q"><strong>4. Seeds on edges instead of growing from the centre.</strong> Growing from the centre looks friendlier but overlaps the neighbours early on. Edge seeds never overlap. Centre growth drawn on top of everything is the alternative.</div>
05668b0d4753atagen2026-10-16 05:50 UTC169
<div class="q"><strong>5. Pinch as the last resort.</strong> The alternative is a crossfade at the target, which is softer but means two copies of the window are visible at once.</div>
05668b0d4753atagen2026-10-16 05:50 UTC170
<div class="q"><strong>6. One curve per motion group.</strong> This is forced by shared edges, but it means velocity matching follows the group's fastest mover. Slower windows in the group inherit its momentum. Acceptable?</div>
05668b0d4753atagen2026-10-16 05:50 UTC171
<div class="q"><strong>7. One tree per output with tags as a filter</strong>, replacing per-tag layouts and the row-stacking merge rule (section 13)? If yes, what happens to a window's tree position when it is retagged? It can keep its spot, or move into the target tag's own row.</div>
05668b0d4753atagen2026-10-16 05:50 UTC172
<div class="q"><strong>8. Floating windows</strong> are outside the no-overlap rule and move in one phase on top of the tiled layer. Should a floating-to-tiled transition join the plan as a seed, or keep floating above until it lands?</div>
05668b0d4753atagen2026-10-16 05:50 UTC173
</main>
05668b0d4753atagen2026-10-16 05:50 UTC174
05668b0d4753atagen2026-10-16 05:50 UTC175
<script id="planner">
05668b0d4753atagen2026-10-16 05:50 UTC176
const G = 10;
05668b0d4753atagen2026-10-16 05:50 UTC177
const EPS = 0.5;
05668b0d4753atagen2026-10-16 05:50 UTC178
let PHASE_MS = 450;
05668b0d4753atagen2026-10-16 05:50 UTC179
let STYLE = 'direct';
05668b0d4753atagen2026-10-16 05:50 UTC180
05668b0d4753atagen2026-10-16 05:50 UTC181
const R = (x, y, w, h) => ({ x, y, w, h });
05668b0d4753atagen2026-10-16 05:50 UTC182
const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; };
05668b0d4753atagen2026-10-16 05:50 UTC183
const eq = (a, b) => Math.abs(a - b) < 0.05;
05668b0d4753atagen2026-10-16 05:50 UTC184
const lerp = (a, b, s) => a + (b - a) * s;
05668b0d4753atagen2026-10-16 05:50 UTC185
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 UTC186
05668b0d4753atagen2026-10-16 05:50 UTC187
function splitX(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC188
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC189
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC190
  const avail = rect.w - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC191
  let x = rect.x;
05668b0d4753atagen2026-10-16 05:50 UTC192
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC193
  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 UTC194
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC195
}
05668b0d4753atagen2026-10-16 05:50 UTC196
function splitY(rect, ids, ratios) {
05668b0d4753atagen2026-10-16 05:50 UTC197
  ratios = ratios || ids.map(() => 1);
05668b0d4753atagen2026-10-16 05:50 UTC198
  const tot = ratios.reduce((a, b) => a + b, 0);
05668b0d4753atagen2026-10-16 05:50 UTC199
  const avail = rect.h - G * (ids.length - 1);
05668b0d4753atagen2026-10-16 05:50 UTC200
  let y = rect.y;
05668b0d4753atagen2026-10-16 05:50 UTC201
  const out = {};
05668b0d4753atagen2026-10-16 05:50 UTC202
  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 UTC203
  return out;
05668b0d4753atagen2026-10-16 05:50 UTC204
}
05668b0d4753atagen2026-10-16 05:50 UTC205
05668b0d4753atagen2026-10-16 05:50 UTC206
function relBits(a, b) {
05668b0d4753atagen2026-10-16 05:50 UTC207
  let s = 0;
05668b0d4753atagen2026-10-16 05:50 UTC208
  if (a.x + a.w <= b.x + EPS) s |= 1;
05668b0d4753atagen2026-10-16 05:50 UTC209
  if (b.x + b.w <= a.x + EPS) s |= 2;
05668b0d4753atagen2026-10-16 05:50 UTC210
  if (a.y + a.h <= b.y + EPS) s |= 4;
05668b0d4753atagen2026-10-16 05:50 UTC211
  if (b.y + b.h <= a.y + EPS) s |= 8;
05668b0d4753atagen2026-10-16 05:50 UTC212
  return s;
05668b0d4753atagen2026-10-16 05:50 UTC213
}
05668b0d4753atagen2026-10-16 05:50 UTC214
function phaseOk(K1, K2, list) {
05668b0d4753atagen2026-10-16 05:50 UTC215
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC216
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC217
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC218
      if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false;
05668b0d4753atagen2026-10-16 05:50 UTC219
    }
05668b0d4753atagen2026-10-16 05:50 UTC220
  return true;
05668b0d4753atagen2026-10-16 05:50 UTC221
}
05668b0d4753atagen2026-10-16 05:50 UTC222
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)) }; }
05668b0d4753atagen2026-10-16 05:50 UTC223
05668b0d4753atagen2026-10-16 05:50 UTC224
function plan(from, to, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC225
  const list = Object.keys(to);
05668b0d4753atagen2026-10-16 05:50 UTC226
  const T = PHASE_MS;
05668b0d4753atagen2026-10-16 05:50 UTC227
  if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC228
  const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; });
05668b0d4753atagen2026-10-16 05:50 UTC229
  if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] };
05668b0d4753atagen2026-10-16 05:50 UTC230
  const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); });
05668b0d4753atagen2026-10-16 05:50 UTC231
  if (phaseOk(from, to, list) && (STYLE === 'direct' || pure))
05668b0d4753atagen2026-10-16 05:50 UTC232
    return { kind: '1 phase', keys: [from, to], durs: [T] };
05668b0d4753atagen2026-10-16 05:50 UTC233
  const two = axisOrder(from, to, list, movers);
05668b0d4753atagen2026-10-16 05:50 UTC234
  if (two) return two;
05668b0d4753atagen2026-10-16 05:50 UTC235
  for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; }
05668b0d4753atagen2026-10-16 05:50 UTC236
  return pinch(from, to, movers);
05668b0d4753atagen2026-10-16 05:50 UTC237
}
05668b0d4753atagen2026-10-16 05:50 UTC238
05668b0d4753atagen2026-10-16 05:50 UTC239
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 UTC240
05668b0d4753atagen2026-10-16 05:50 UTC241
function axisOrder(from, to, list, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC242
  const n = movers.length;
05668b0d4753atagen2026-10-16 05:50 UTC243
  if (n > 14) return null;
05668b0d4753atagen2026-10-16 05:50 UTC244
  let best = null, bestScore = Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC245
  for (let mask = 0; mask < (1 << n); mask++) {
05668b0d4753atagen2026-10-16 05:50 UTC246
    const K1 = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC247
    let score = 0;
05668b0d4753atagen2026-10-16 05:50 UTC248
    movers.forEach((id, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC249
      const xf = !!(mask & (1 << i));
05668b0d4753atagen2026-10-16 05:50 UTC250
      const c = chg(from[id], to[id]);
05668b0d4753atagen2026-10-16 05:50 UTC251
      K1[id] = mixed(from[id], to[id], xf);
05668b0d4753atagen2026-10-16 05:50 UTC252
      if (c.x && !c.y && !xf) score++;
05668b0d4753atagen2026-10-16 05:50 UTC253
      if (c.y && !c.x && xf) score++;
05668b0d4753atagen2026-10-16 05:50 UTC254
    });
05668b0d4753atagen2026-10-16 05:50 UTC255
    if (score >= bestScore) continue;
05668b0d4753atagen2026-10-16 05:50 UTC256
    if (phaseOk(from, K1, list) && phaseOk(K1, to, list)) { best = K1; bestScore = score; if (score === 0) break; }
05668b0d4753atagen2026-10-16 05:50 UTC257
  }
05668b0d4753atagen2026-10-16 05:50 UTC258
  return best ? { kind: '2 phases (axis order)', keys: [from, best, to], durs: [PHASE_MS, PHASE_MS] } : null;
05668b0d4753atagen2026-10-16 05:50 UTC259
}
05668b0d4753atagen2026-10-16 05:50 UTC260
05668b0d4753atagen2026-10-16 05:50 UTC261
function lanes(from, to, list, movers, ax) {
05668b0d4753atagen2026-10-16 05:50 UTC262
  const size = ax === 'y' ? 'h' : 'w';
05668b0d4753atagen2026-10-16 05:50 UTC263
  const bitsT = ax === 'y' ? 3 : 12;
05668b0d4753atagen2026-10-16 05:50 UTC264
  const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2;
05668b0d4753atagen2026-10-16 05:50 UTC265
  const conflict = new Set();
05668b0d4753atagen2026-10-16 05:50 UTC266
  for (let i = 0; i < list.length; i++)
05668b0d4753atagen2026-10-16 05:50 UTC267
    for (let j = i + 1; j < list.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC268
      const a = list[i], b = list[j];
05668b0d4753atagen2026-10-16 05:50 UTC269
      if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); }
05668b0d4753atagen2026-10-16 05:50 UTC270
    }
05668b0d4753atagen2026-10-16 05:50 UTC271
  const sets = [[...conflict], movers, list];
05668b0d4753atagen2026-10-16 05:50 UTC272
  for (const set of sets) {
05668b0d4753atagen2026-10-16 05:50 UTC273
    const n = set.length;
05668b0d4753atagen2026-10-16 05:50 UTC274
    if (n < 2) continue;
05668b0d4753atagen2026-10-16 05:50 UTC275
    let lo = Infinity, hi = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC276
    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 UTC277
    const diff = [], lt = [];
05668b0d4753atagen2026-10-16 05:50 UTC278
    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 UTC279
    for (let i = 0; i < n; i++)
05668b0d4753atagen2026-10-16 05:50 UTC280
      for (let j = 0; j < n; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC281
        if (i === j) continue;
05668b0d4753atagen2026-10-16 05:50 UTC282
        const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]);
05668b0d4753atagen2026-10-16 05:50 UTC283
        if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC284
        for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true;
05668b0d4753atagen2026-10-16 05:50 UTC285
      }
05668b0d4753atagen2026-10-16 05:50 UTC286
    for (let k = 2; k <= n; k++) {
05668b0d4753atagen2026-10-16 05:50 UTC287
      const s = (hi - lo - (k - 1) * G) / k;
05668b0d4753atagen2026-10-16 05:50 UTC288
      if (s < 6) break;
05668b0d4753atagen2026-10-16 05:50 UTC289
      const lane = new Array(n).fill(-1);
05668b0d4753atagen2026-10-16 05:50 UTC290
      const assign = i => {
05668b0d4753atagen2026-10-16 05:50 UTC291
        if (i === n) return true;
05668b0d4753atagen2026-10-16 05:50 UTC292
        for (let l = 0; l < k; l++) {
05668b0d4753atagen2026-10-16 05:50 UTC293
          let ok = true;
05668b0d4753atagen2026-10-16 05:50 UTC294
          for (let j = 0; j < i && ok; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC295
            if (diff[i][j] && lane[j] === l) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC296
            if (lt[i][j] && !(l < lane[j])) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC297
            if (lt[j][i] && !(lane[j] < l)) ok = false;
05668b0d4753atagen2026-10-16 05:50 UTC298
          }
05668b0d4753atagen2026-10-16 05:50 UTC299
          if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; }
05668b0d4753atagen2026-10-16 05:50 UTC300
        }
05668b0d4753atagen2026-10-16 05:50 UTC301
        return false;
05668b0d4753atagen2026-10-16 05:50 UTC302
      };
05668b0d4753atagen2026-10-16 05:50 UTC303
      if (!assign(0)) continue;
05668b0d4753atagen2026-10-16 05:50 UTC304
      const strip = l => lo + l * (s + G);
05668b0d4753atagen2026-10-16 05:50 UTC305
      const inSet = new Set(set);
05668b0d4753atagen2026-10-16 05:50 UTC306
      for (const phase of [2, 1, 3]) {
05668b0d4753atagen2026-10-16 05:50 UTC307
        const K1 = {}, K2 = {};
05668b0d4753atagen2026-10-16 05:50 UTC308
        for (const id of list) {
05668b0d4753atagen2026-10-16 05:50 UTC309
          const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC310
          if (inSet.has(id)) {
05668b0d4753atagen2026-10-16 05:50 UTC311
            const l = lane[set.indexOf(id)];
05668b0d4753atagen2026-10-16 05:50 UTC312
            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 UTC313
            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 UTC314
          } else if (phase === 1) { K1[id] = t; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC315
          else if (phase === 2) { K1[id] = f; K2[id] = t; }
05668b0d4753atagen2026-10-16 05:50 UTC316
          else { K1[id] = f; K2[id] = f; }
05668b0d4753atagen2026-10-16 05:50 UTC317
        }
05668b0d4753atagen2026-10-16 05:50 UTC318
        if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list))
05668b0d4753atagen2026-10-16 05:50 UTC319
          return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC320
      }
05668b0d4753atagen2026-10-16 05:50 UTC321
    }
05668b0d4753atagen2026-10-16 05:50 UTC322
  }
05668b0d4753atagen2026-10-16 05:50 UTC323
  return null;
05668b0d4753atagen2026-10-16 05:50 UTC324
}
05668b0d4753atagen2026-10-16 05:50 UTC325
05668b0d4753atagen2026-10-16 05:50 UTC326
function pinch(from, to, movers) {
05668b0d4753atagen2026-10-16 05:50 UTC327
  const K1 = { ...from }, K2 = { ...to };
05668b0d4753atagen2026-10-16 05:50 UTC328
  for (const id of movers) {
05668b0d4753atagen2026-10-16 05:50 UTC329
    const f = from[id], t = to[id];
05668b0d4753atagen2026-10-16 05:50 UTC330
    K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC331
    K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC332
  }
05668b0d4753atagen2026-10-16 05:50 UTC333
  return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] };
05668b0d4753atagen2026-10-16 05:50 UTC334
}
05668b0d4753atagen2026-10-16 05:50 UTC335
05668b0d4753atagen2026-10-16 05:50 UTC336
function bbox(rects) {
05668b0d4753atagen2026-10-16 05:50 UTC337
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
05668b0d4753atagen2026-10-16 05:50 UTC338
  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 UTC339
  return R(x0, y0, x1 - x0, y1 - y0);
05668b0d4753atagen2026-10-16 05:50 UTC340
}
05668b0d4753atagen2026-10-16 05:50 UTC341
function planBlocks(from, to, groups) {
05668b0d4753atagen2026-10-16 05:50 UTC342
  const bf = {}, bt = {};
05668b0d4753atagen2026-10-16 05:50 UTC343
  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 UTC344
  const bp = plan(bf, bt);
05668b0d4753atagen2026-10-16 05:50 UTC345
  const last = bp.keys.length - 1;
05668b0d4753atagen2026-10-16 05:50 UTC346
  const keys = bp.keys.map((K, ki) => {
05668b0d4753atagen2026-10-16 05:50 UTC347
    if (ki === 0) return from;
05668b0d4753atagen2026-10-16 05:50 UTC348
    if (ki === last) return to;
05668b0d4753atagen2026-10-16 05:50 UTC349
    const L = { ...from };
05668b0d4753atagen2026-10-16 05:50 UTC350
    for (const b in groups) for (const id of groups[b]) {
05668b0d4753atagen2026-10-16 05:50 UTC351
      const r = from[id], box = bf[b], k = K[b];
05668b0d4753atagen2026-10-16 05:50 UTC352
      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 UTC353
      const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0;
05668b0d4753atagen2026-10-16 05:50 UTC354
      L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h);
05668b0d4753atagen2026-10-16 05:50 UTC355
    }
05668b0d4753atagen2026-10-16 05:50 UTC356
    return L;
05668b0d4753atagen2026-10-16 05:50 UTC357
  });
05668b0d4753atagen2026-10-16 05:50 UTC358
  return { kind: bp.kind + ' on blocks', keys, durs: bp.durs };
05668b0d4753atagen2026-10-16 05:50 UTC359
}
05668b0d4753atagen2026-10-16 05:50 UTC360
05668b0d4753atagen2026-10-16 05:50 UTC361
function bezier(x1, y1, x2, y2) {
05668b0d4753atagen2026-10-16 05:50 UTC362
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
05668b0d4753atagen2026-10-16 05:50 UTC363
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
05668b0d4753atagen2026-10-16 05:50 UTC364
  const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u;
05668b0d4753atagen2026-10-16 05:50 UTC365
  const dX = u => (3 * ax * u + 2 * bx) * u + cx;
05668b0d4753atagen2026-10-16 05:50 UTC366
  return t => {
05668b0d4753atagen2026-10-16 05:50 UTC367
    if (t <= 0) return 0;
05668b0d4753atagen2026-10-16 05:50 UTC368
    if (t >= 1) return 1;
05668b0d4753atagen2026-10-16 05:50 UTC369
    let u = t;
05668b0d4753atagen2026-10-16 05:50 UTC370
    for (let i = 0; i < 8; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC371
      const x = X(u) - t;
05668b0d4753atagen2026-10-16 05:50 UTC372
      if (Math.abs(x) < 1e-6) return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC373
      const d = dX(u);
05668b0d4753atagen2026-10-16 05:50 UTC374
      if (Math.abs(d) < 1e-6) break;
05668b0d4753atagen2026-10-16 05:50 UTC375
      u -= x / d;
05668b0d4753atagen2026-10-16 05:50 UTC376
    }
05668b0d4753atagen2026-10-16 05:50 UTC377
    let lo = 0, hi = 1; u = t;
05668b0d4753atagen2026-10-16 05:50 UTC378
    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 UTC379
    return Y(u);
05668b0d4753atagen2026-10-16 05:50 UTC380
  };
05668b0d4753atagen2026-10-16 05:50 UTC381
}
05668b0d4753atagen2026-10-16 05:50 UTC382
const BASE = bezier(0.45, 0, 0.2, 1);
05668b0d4753atagen2026-10-16 05:50 UTC383
const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h];
05668b0d4753atagen2026-10-16 05:50 UTC384
</script>
05668b0d4753atagen2026-10-16 05:50 UTC385
05668b0d4753atagen2026-10-16 05:50 UTC386
<script>
05668b0d4753atagen2026-10-16 05:50 UTC387
const PAL = { A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' };
05668b0d4753atagen2026-10-16 05:50 UTC388
const demos = [];
05668b0d4753atagen2026-10-16 05:50 UTC389
05668b0d4753atagen2026-10-16 05:50 UTC390
class Demo {
05668b0d4753atagen2026-10-16 05:50 UTC391
  constructor(id, W, H, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC392
    this.W = W; this.H = H;
05668b0d4753atagen2026-10-16 05:50 UTC393
    this.outputs = opts.outputs || [R(0, 0, W, H)];
05668b0d4753atagen2026-10-16 05:50 UTC394
    const el = document.getElementById(id);
05668b0d4753atagen2026-10-16 05:50 UTC395
    this.canvas = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC396
    const dpr = window.devicePixelRatio || 1;
05668b0d4753atagen2026-10-16 05:50 UTC397
    this.canvas.width = W * dpr; this.canvas.height = H * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC398
    this.canvas.style.maxWidth = W + 'px';
05668b0d4753atagen2026-10-16 05:50 UTC399
    this.ctx = this.canvas.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC400
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC401
    el.appendChild(this.canvas);
05668b0d4753atagen2026-10-16 05:50 UTC402
    if (opts.graph) {
05668b0d4753atagen2026-10-16 05:50 UTC403
      this.graph = document.createElement('canvas');
05668b0d4753atagen2026-10-16 05:50 UTC404
      this.graph.width = W * dpr; this.graph.height = 90 * dpr;
05668b0d4753atagen2026-10-16 05:50 UTC405
      this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px';
05668b0d4753atagen2026-10-16 05:50 UTC406
      this.gctx = this.graph.getContext('2d');
05668b0d4753atagen2026-10-16 05:50 UTC407
      this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0);
05668b0d4753atagen2026-10-16 05:50 UTC408
      el.appendChild(this.graph);
05668b0d4753atagen2026-10-16 05:50 UTC409
      this.hist = [];
05668b0d4753atagen2026-10-16 05:50 UTC410
    }
05668b0d4753atagen2026-10-16 05:50 UTC411
    this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls);
05668b0d4753atagen2026-10-16 05:50 UTC412
    this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status);
05668b0d4753atagen2026-10-16 05:50 UTC413
    this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle';
05668b0d4753atagen2026-10-16 05:50 UTC414
    demos.push(this);
05668b0d4753atagen2026-10-16 05:50 UTC415
  }
05668b0d4753atagen2026-10-16 05:50 UTC416
  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 UTC417
  check(label, init, fn) {
05668b0d4753atagen2026-10-16 05:50 UTC418
    const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init;
05668b0d4753atagen2026-10-16 05:50 UTC419
    c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c;
05668b0d4753atagen2026-10-16 05:50 UTC420
  }
05668b0d4753atagen2026-10-16 05:50 UTC421
  set(L) { this.layout = clone(L); this.anim = null; }
05668b0d4753atagen2026-10-16 05:50 UTC422
  go(target, opts = {}) {
05668b0d4753atagen2026-10-16 05:50 UTC423
    const now = performance.now();
05668b0d4753atagen2026-10-16 05:50 UTC424
    const cur = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC425
    const from = {};
05668b0d4753atagen2026-10-16 05:50 UTC426
    for (const id in target) from[id] = cur[id] || target[id];
05668b0d4753atagen2026-10-16 05:50 UTC427
    const p = opts.blocks ? planBlocks(from, target, opts.blocks) : plan(from, target, { naive: this.naive });
05668b0d4753atagen2026-10-16 05:50 UTC428
    const curves = p.durs.map(() => BASE);
05668b0d4753atagen2026-10-16 05:50 UTC429
    const interrupted = !!this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC430
    let matched = false;
05668b0d4753atagen2026-10-16 05:50 UTC431
    if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) {
05668b0d4753atagen2026-10-16 05:50 UTC432
      const K1 = p.keys[1];
05668b0d4753atagen2026-10-16 05:50 UTC433
      let best = null, bs = 0;
05668b0d4753atagen2026-10-16 05:50 UTC434
      for (const id in K1) {
05668b0d4753atagen2026-10-16 05:50 UTC435
        const v = this.vel[id]; if (!v) continue;
05668b0d4753atagen2026-10-16 05:50 UTC436
        const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]);
05668b0d4753atagen2026-10-16 05:50 UTC437
        const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue;
05668b0d4753atagen2026-10-16 05:50 UTC438
        const sp = Math.hypot(...v);
05668b0d4753atagen2026-10-16 05:50 UTC439
        if (sp > bs) { bs = sp; best = { v, D, dd }; }
05668b0d4753atagen2026-10-16 05:50 UTC440
      }
05668b0d4753atagen2026-10-16 05:50 UTC441
      if (best) {
05668b0d4753atagen2026-10-16 05:50 UTC442
        const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd;
05668b0d4753atagen2026-10-16 05:50 UTC443
        const slope = s * p.durs[0];
05668b0d4753atagen2026-10-16 05:50 UTC444
        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 UTC445
      }
05668b0d4753atagen2026-10-16 05:50 UTC446
    }
05668b0d4753atagen2026-10-16 05:50 UTC447
    this.anim = { keys: p.keys, durs: p.durs, curves, start: now };
05668b0d4753atagen2026-10-16 05:50 UTC448
    this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : '');
05668b0d4753atagen2026-10-16 05:50 UTC449
    this.overlapSeen = false;
05668b0d4753atagen2026-10-16 05:50 UTC450
  }
05668b0d4753atagen2026-10-16 05:50 UTC451
  sampleAt(t) {
05668b0d4753atagen2026-10-16 05:50 UTC452
    if (!this.anim) return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC453
    let e = t - this.anim.start;
05668b0d4753atagen2026-10-16 05:50 UTC454
    const { keys, durs, curves } = this.anim;
05668b0d4753atagen2026-10-16 05:50 UTC455
    for (let i = 0; i < durs.length; i++) {
05668b0d4753atagen2026-10-16 05:50 UTC456
      const d = durs[i];
05668b0d4753atagen2026-10-16 05:50 UTC457
      if (e < d) {
05668b0d4753atagen2026-10-16 05:50 UTC458
        this.phase = i;
05668b0d4753atagen2026-10-16 05:50 UTC459
        const s = curves[i](e / d);
05668b0d4753atagen2026-10-16 05:50 UTC460
        const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC461
        for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s);
05668b0d4753atagen2026-10-16 05:50 UTC462
        return L;
05668b0d4753atagen2026-10-16 05:50 UTC463
      }
05668b0d4753atagen2026-10-16 05:50 UTC464
      e -= d;
05668b0d4753atagen2026-10-16 05:50 UTC465
    }
05668b0d4753atagen2026-10-16 05:50 UTC466
    this.layout = clone(keys[keys.length - 1]);
05668b0d4753atagen2026-10-16 05:50 UTC467
    this.anim = null;
05668b0d4753atagen2026-10-16 05:50 UTC468
    return clone(this.layout);
05668b0d4753atagen2026-10-16 05:50 UTC469
  }
05668b0d4753atagen2026-10-16 05:50 UTC470
  frame(now) {
05668b0d4753atagen2026-10-16 05:50 UTC471
    const L = this.sampleAt(now);
05668b0d4753atagen2026-10-16 05:50 UTC472
    if (this.prev) {
05668b0d4753atagen2026-10-16 05:50 UTC473
      const dt = now - this.prev.t;
05668b0d4753atagen2026-10-16 05:50 UTC474
      if (dt > 0) for (const id in L) if (this.prev.L[id]) {
05668b0d4753atagen2026-10-16 05:50 UTC475
        const a = edges(this.prev.L[id]), b = edges(L[id]);
05668b0d4753atagen2026-10-16 05:50 UTC476
        this.vel[id] = b.map((v, i) => (v - a[i]) / dt);
05668b0d4753atagen2026-10-16 05:50 UTC477
      }
05668b0d4753atagen2026-10-16 05:50 UTC478
    }
05668b0d4753atagen2026-10-16 05:50 UTC479
    this.prev = { t: now, L };
05668b0d4753atagen2026-10-16 05:50 UTC480
    this.draw(L);
05668b0d4753atagen2026-10-16 05:50 UTC481
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC482
    if (overlaps.length) this.overlapSeen = true;
05668b0d4753atagen2026-10-16 05:50 UTC483
    const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : '';
05668b0d4753atagen2026-10-16 05:50 UTC484
    this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : '');
05668b0d4753atagen2026-10-16 05:50 UTC485
    if (this.graph) this.drawGraph(now);
05668b0d4753atagen2026-10-16 05:50 UTC486
  }
05668b0d4753atagen2026-10-16 05:50 UTC487
  findOverlaps(L) {
05668b0d4753atagen2026-10-16 05:50 UTC488
    const ids = Object.keys(L).filter(id => L[id].w > 0.5 && L[id].h > 0.5);
05668b0d4753atagen2026-10-16 05:50 UTC489
    const out = [];
05668b0d4753atagen2026-10-16 05:50 UTC490
    for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
05668b0d4753atagen2026-10-16 05:50 UTC491
      const a = L[ids[i]], b = L[ids[j]];
05668b0d4753atagen2026-10-16 05:50 UTC492
      const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y);
05668b0d4753atagen2026-10-16 05:50 UTC493
      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 UTC494
      if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0));
05668b0d4753atagen2026-10-16 05:50 UTC495
    }
05668b0d4753atagen2026-10-16 05:50 UTC496
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC497
  }
05668b0d4753atagen2026-10-16 05:50 UTC498
  draw(L) {
05668b0d4753atagen2026-10-16 05:50 UTC499
    const c = this.ctx;
05668b0d4753atagen2026-10-16 05:50 UTC500
    c.clearRect(0, 0, this.W, this.H);
05668b0d4753atagen2026-10-16 05:50 UTC501
    const overlaps = this.findOverlaps(L);
05668b0d4753atagen2026-10-16 05:50 UTC502
    for (const o of this.outputs) {
05668b0d4753atagen2026-10-16 05:50 UTC503
      c.save();
05668b0d4753atagen2026-10-16 05:50 UTC504
      c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h);
05668b0d4753atagen2026-10-16 05:50 UTC505
      c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip();
05668b0d4753atagen2026-10-16 05:50 UTC506
      for (const id in L) {
05668b0d4753atagen2026-10-16 05:50 UTC507
        const r = L[id];
05668b0d4753atagen2026-10-16 05:50 UTC508
        if (r.w < 0.5 || r.h < 0.5) continue;
05668b0d4753atagen2026-10-16 05:50 UTC509
        const rad = Math.min(8, r.w / 2, r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC510
        c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad);
05668b0d4753atagen2026-10-16 05:50 UTC511
        c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill();
05668b0d4753atagen2026-10-16 05:50 UTC512
        c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC513
        if (r.w > 22 && r.h > 22) {
05668b0d4753atagen2026-10-16 05:50 UTC514
          c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle';
05668b0d4753atagen2026-10-16 05:50 UTC515
          c.fillText(id, r.x + r.w / 2, r.y + r.h / 2);
05668b0d4753atagen2026-10-16 05:50 UTC516
        }
05668b0d4753atagen2026-10-16 05:50 UTC517
      }
05668b0d4753atagen2026-10-16 05:50 UTC518
      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 UTC519
      c.restore();
05668b0d4753atagen2026-10-16 05:50 UTC520
    }
05668b0d4753atagen2026-10-16 05:50 UTC521
  }
05668b0d4753atagen2026-10-16 05:50 UTC522
  drawGraph(now) {
05668b0d4753atagen2026-10-16 05:50 UTC523
    const v = this.vel.A;
05668b0d4753atagen2026-10-16 05:50 UTC524
    const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0;
05668b0d4753atagen2026-10-16 05:50 UTC525
    this.hist.push([now, sp]);
05668b0d4753atagen2026-10-16 05:50 UTC526
    while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift();
05668b0d4753atagen2026-10-16 05:50 UTC527
    const c = this.gctx, W = this.W, H = 90;
05668b0d4753atagen2026-10-16 05:50 UTC528
    c.clearRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC529
    c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H);
05668b0d4753atagen2026-10-16 05:50 UTC530
    const max = Math.max(400, ...this.hist.map(h => h[1]));
05668b0d4753atagen2026-10-16 05:50 UTC531
    c.beginPath();
05668b0d4753atagen2026-10-16 05:50 UTC532
    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 UTC533
    c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke();
05668b0d4753atagen2026-10-16 05:50 UTC534
    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 UTC535
  }
05668b0d4753atagen2026-10-16 05:50 UTC536
}
05668b0d4753atagen2026-10-16 05:50 UTC537
05668b0d4753atagen2026-10-16 05:50 UTC538
const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G);
05668b0d4753atagen2026-10-16 05:50 UTC539
05668b0d4753atagen2026-10-16 05:50 UTC540
{ // d1 swap
05668b0d4753atagen2026-10-16 05:50 UTC541
  const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC542
  let swapped = false;
05668b0d4753atagen2026-10-16 05:50 UTC543
  const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']);
05668b0d4753atagen2026-10-16 05:50 UTC544
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC545
  d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC546
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC547
}
05668b0d4753atagen2026-10-16 05:50 UTC548
05668b0d4753atagen2026-10-16 05:50 UTC549
{ // d2 one phase
05668b0d4753atagen2026-10-16 05:50 UTC550
  const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC551
  const s = { wide: false, d: false, e: false };
05668b0d4753atagen2026-10-16 05:50 UTC552
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC553
    const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C'];
05668b0d4753atagen2026-10-16 05:50 UTC554
    const out = splitX(I, ids, ids.map(id => (id === 'A' && s.wide) ? 2 : 1));
05668b0d4753atagen2026-10-16 05:50 UTC555
    if (!s.d) out.D = R((out.B.x + out.B.w + out.C.x) / 2, I.y, 0, I.h);
05668b0d4753atagen2026-10-16 05:50 UTC556
    const b = out.B;
05668b0d4753atagen2026-10-16 05:50 UTC557
    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 UTC558
    return out;
05668b0d4753atagen2026-10-16 05:50 UTC559
  };
05668b0d4753atagen2026-10-16 05:50 UTC560
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC561
  d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC562
  d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC563
  d.button('Split B with E', () => { s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC564
  d.button('All at once', () => { s.wide = !s.wide; s.d = !s.d; s.e = !s.e; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC565
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC566
}
05668b0d4753atagen2026-10-16 05:50 UTC567
05668b0d4753atagen2026-10-16 05:50 UTC568
function tagLayout(I, view2, order) {
05668b0d4753atagen2026-10-16 05:50 UTC569
  const L = {};
05668b0d4753atagen2026-10-16 05:50 UTC570
  if (view2) {
05668b0d4753atagen2026-10-16 05:50 UTC571
    const rows = splitY(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC572
    Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC573
  } else {
05668b0d4753atagen2026-10-16 05:50 UTC574
    Object.assign(L, splitX(I, ['A', 'B', 'C']));
05668b0d4753atagen2026-10-16 05:50 UTC575
    const y = order[0] === 't2' ? I.y : I.y + I.h;
05668b0d4753atagen2026-10-16 05:50 UTC576
    Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E']));
05668b0d4753atagen2026-10-16 05:50 UTC577
  }
05668b0d4753atagen2026-10-16 05:50 UTC578
  return L;
05668b0d4753atagen2026-10-16 05:50 UTC579
}
05668b0d4753atagen2026-10-16 05:50 UTC580
05668b0d4753atagen2026-10-16 05:50 UTC581
{ // d3 tag toggle
05668b0d4753atagen2026-10-16 05:50 UTC582
  const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC583
  let view2 = false;
05668b0d4753atagen2026-10-16 05:50 UTC584
  d.set(tagLayout(I, view2, ['t1', 't2']));
05668b0d4753atagen2026-10-16 05:50 UTC585
  d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); });
05668b0d4753atagen2026-10-16 05:50 UTC586
}
05668b0d4753atagen2026-10-16 05:50 UTC587
05668b0d4753atagen2026-10-16 05:50 UTC588
{ // d3b tag reorder
05668b0d4753atagen2026-10-16 05:50 UTC589
  const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC590
  let order = ['t1', 't2'], blocks = true;
05668b0d4753atagen2026-10-16 05:50 UTC591
  d.set(tagLayout(I, true, order));
05668b0d4753atagen2026-10-16 05:50 UTC592
  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 UTC593
  d.check('rigid blocks', true, v => blocks = v);
05668b0d4753atagen2026-10-16 05:50 UTC594
}
05668b0d4753atagen2026-10-16 05:50 UTC595
05668b0d4753atagen2026-10-16 05:50 UTC596
{ // d4a orientation flip
05668b0d4753atagen2026-10-16 05:50 UTC597
  const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC598
  let rows = false;
05668b0d4753atagen2026-10-16 05:50 UTC599
  const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']);
05668b0d4753atagen2026-10-16 05:50 UTC600
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC601
  d.button('Flip orientation', () => { rows = !rows; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC602
  d.check('naive', false, v => d.naive = v);
05668b0d4753atagen2026-10-16 05:50 UTC603
}
05668b0d4753atagen2026-10-16 05:50 UTC604
05668b0d4753atagen2026-10-16 05:50 UTC605
{ // d4b grow both axes
05668b0d4753atagen2026-10-16 05:50 UTC606
  const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC607
  let wide = false;
05668b0d4753atagen2026-10-16 05:50 UTC608
  const L = () => {
05668b0d4753atagen2026-10-16 05:50 UTC609
    const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]);
05668b0d4753atagen2026-10-16 05:50 UTC610
    return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) };
05668b0d4753atagen2026-10-16 05:50 UTC611
  };
05668b0d4753atagen2026-10-16 05:50 UTC612
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC613
  d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC614
}
05668b0d4753atagen2026-10-16 05:50 UTC615
05668b0d4753atagen2026-10-16 05:50 UTC616
{ // d5 lanes
05668b0d4753atagen2026-10-16 05:50 UTC617
  const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC618
  let order = ['A', 'B', 'C', 'D'];
05668b0d4753atagen2026-10-16 05:50 UTC619
  const L = () => splitX(I, order);
05668b0d4753atagen2026-10-16 05:50 UTC620
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC621
  d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC622
  d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC623
  d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC624
  d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC625
}
05668b0d4753atagen2026-10-16 05:50 UTC626
05668b0d4753atagen2026-10-16 05:50 UTC627
{ // d6 interrupts
05668b0d4753atagen2026-10-16 05:50 UTC628
  const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC629
  const targets = [
05668b0d4753atagen2026-10-16 05:50 UTC630
    () => splitX(I, ['A', 'B', 'C']),
05668b0d4753atagen2026-10-16 05:50 UTC631
    () => splitX(I, ['C', 'A', 'B']),
05668b0d4753atagen2026-10-16 05:50 UTC632
    () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]),
05668b0d4753atagen2026-10-16 05:50 UTC633
    () => splitX(I, ['B', 'C', 'A']),
05668b0d4753atagen2026-10-16 05:50 UTC634
    () => ({ ...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 UTC635
  ];
05668b0d4753atagen2026-10-16 05:50 UTC636
  const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; };
05668b0d4753atagen2026-10-16 05:50 UTC637
  let idx = 0;
05668b0d4753atagen2026-10-16 05:50 UTC638
  d.set(withB(targets[0]()));
05668b0d4753atagen2026-10-16 05:50 UTC639
  const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); };
05668b0d4753atagen2026-10-16 05:50 UTC640
  d.button('Next target', next);
05668b0d4753atagen2026-10-16 05:50 UTC641
  d.button('Storm (8 rapid interrupts)', () => {
05668b0d4753atagen2026-10-16 05:50 UTC642
    let n = 0;
05668b0d4753atagen2026-10-16 05:50 UTC643
    const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); };
05668b0d4753atagen2026-10-16 05:50 UTC644
    fire();
05668b0d4753atagen2026-10-16 05:50 UTC645
  });
05668b0d4753atagen2026-10-16 05:50 UTC646
  d.check('match velocity', true, v => d.vmatch = v);
05668b0d4753atagen2026-10-16 05:50 UTC647
}
05668b0d4753atagen2026-10-16 05:50 UTC648
05668b0d4753atagen2026-10-16 05:50 UTC649
{ // d7 across outputs
05668b0d4753atagen2026-10-16 05:50 UTC650
  const W = 680, H = 260;
05668b0d4753atagen2026-10-16 05:50 UTC651
  const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H);
05668b0d4753atagen2026-10-16 05:50 UTC652
  const d = new Demo('d7', W, H, { outputs: [o1, o2] });
05668b0d4753atagen2026-10-16 05:50 UTC653
  const I1 = inner(320, H, 0), I2 = inner(320, H, 360);
05668b0d4753atagen2026-10-16 05:50 UTC654
  let s1 = ['A', 'B'], s2 = ['C'];
05668b0d4753atagen2026-10-16 05:50 UTC655
  const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) });
05668b0d4753atagen2026-10-16 05:50 UTC656
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC657
  d.button('Move B across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC658
    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 UTC659
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC660
  });
05668b0d4753atagen2026-10-16 05:50 UTC661
  d.button('Move A across', () => {
05668b0d4753atagen2026-10-16 05:50 UTC662
    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 UTC663
    d.go(L());
05668b0d4753atagen2026-10-16 05:50 UTC664
  });
05668b0d4753atagen2026-10-16 05:50 UTC665
  d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC666
}
05668b0d4753atagen2026-10-16 05:50 UTC667
05668b0d4753atagen2026-10-16 05:50 UTC668
function layoutTree(node, rect, visible, out) {
05668b0d4753atagen2026-10-16 05:50 UTC669
  if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; }
05668b0d4753atagen2026-10-16 05:50 UTC670
  const horiz = node.dir === 'row';
05668b0d4753atagen2026-10-16 05:50 UTC671
  const shown = node.kids.map(k => anyVisible(k, visible));
05668b0d4753atagen2026-10-16 05:50 UTC672
  const m = shown.filter(Boolean).length;
05668b0d4753atagen2026-10-16 05:50 UTC673
  const len = horiz ? rect.w : rect.h;
05668b0d4753atagen2026-10-16 05:50 UTC674
  const avail = len - G * Math.max(0, m - 1);
05668b0d4753atagen2026-10-16 05:50 UTC675
  let pos = horiz ? rect.x : rect.y, seen = 0;
05668b0d4753atagen2026-10-16 05:50 UTC676
  node.kids.forEach((k, i) => {
05668b0d4753atagen2026-10-16 05:50 UTC677
    if (!shown[i]) {
05668b0d4753atagen2026-10-16 05:50 UTC678
      const p = seen ? pos - G / 2 : pos;
05668b0d4753atagen2026-10-16 05:50 UTC679
      const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0);
05668b0d4753atagen2026-10-16 05:50 UTC680
      layoutTree(k, r, () => false, out);
05668b0d4753atagen2026-10-16 05:50 UTC681
      return;
05668b0d4753atagen2026-10-16 05:50 UTC682
    }
05668b0d4753atagen2026-10-16 05:50 UTC683
    const size = avail / m;
05668b0d4753atagen2026-10-16 05:50 UTC684
    const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size);
05668b0d4753atagen2026-10-16 05:50 UTC685
    layoutTree(k, r, visible, out);
05668b0d4753atagen2026-10-16 05:50 UTC686
    pos += size + G; seen++;
05668b0d4753atagen2026-10-16 05:50 UTC687
  });
05668b0d4753atagen2026-10-16 05:50 UTC688
}
05668b0d4753atagen2026-10-16 05:50 UTC689
function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); }
05668b0d4753atagen2026-10-16 05:50 UTC690
05668b0d4753atagen2026-10-16 05:50 UTC691
{ // d8 one tree, tags filter
05668b0d4753atagen2026-10-16 05:50 UTC692
  const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H);
05668b0d4753atagen2026-10-16 05:50 UTC693
  const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 };
05668b0d4753atagen2026-10-16 05:50 UTC694
  const view = { 1: true, 2: true };
05668b0d4753atagen2026-10-16 05:50 UTC695
  let mixed = false;
05668b0d4753atagen2026-10-16 05:50 UTC696
  const tree = () => mixed
05668b0d4753atagen2026-10-16 05:50 UTC697
    ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] }
05668b0d4753atagen2026-10-16 05:50 UTC698
    : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] };
05668b0d4753atagen2026-10-16 05:50 UTC699
  const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; };
05668b0d4753atagen2026-10-16 05:50 UTC700
  d.set(L());
05668b0d4753atagen2026-10-16 05:50 UTC701
  d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC702
  d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC703
  d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); });
05668b0d4753atagen2026-10-16 05:50 UTC704
}
05668b0d4753atagen2026-10-16 05:50 UTC705
05668b0d4753atagen2026-10-16 05:50 UTC706
document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; };
05668b0d4753atagen2026-10-16 05:50 UTC707
document.getElementById('style').onchange = e => { STYLE = e.target.value; };
05668b0d4753atagen2026-10-16 05:50 UTC708
const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); };
05668b0d4753atagen2026-10-16 05:50 UTC709
requestAnimationFrame(loop);
05668b0d4753atagen2026-10-16 05:50 UTC710
</script>
05668b0d4753atagen2026-10-16 05:50 UTC711
</body>
05668b0d4753atagen2026-10-16 05:50 UTC712
</html>