05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 1 | <!doctype html> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 2 | <html lang="en"> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 3 | <head> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 4 | <meta charset="utf-8"> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 5 | <title>monolith planner design, draft 1</title> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 6 | <style> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 7 | :root { --bg:#111317; --panel:#181b21; --fg:#d8dbe2; --dim:#8a909c; --accent:#e0774a; --bad:#ff4d5e; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 8 | * { box-sizing:border-box; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 9 | body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.6 system-ui, sans-serif; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 10 | main { max-width:920px; margin:0 auto; padding:32px 20px 120px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 11 | h1 { font-size:28px; margin:0 0 4px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 12 | h2 { font-size:20px; margin:48px 0 8px; border-bottom:1px solid #2a2e37; padding-bottom:4px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 13 | h3 { font-size:16px; margin:24px 0 6px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 14 | p, li { color:var(--fg); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 15 | .sub { color:var(--dim); margin:0 0 24px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 16 | code { background:#22262e; padding:1px 5px; border-radius:4px; font-size:13px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 17 | pre { background:#1a1d23; padding:14px; border-radius:8px; overflow-x:auto; font-size:13px; line-height:1.45; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 18 | table { border-collapse:collapse; width:100%; font-size:14px; margin:12px 0; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 19 | th, td { border:1px solid #2a2e37; padding:6px 8px; text-align:left; vertical-align:top; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 20 | th { background:#1d2027; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 21 | .demo { background:var(--panel); border:1px solid #2a2e37; border-radius:10px; padding:12px; margin:16px 0; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 22 | .demo canvas { display:block; width:100%; height:auto; border-radius:6px; background:#0d0f12; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 23 | .controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 24 | button { background:#2a2f39; color:var(--fg); border:1px solid #3a404c; border-radius:6px; padding:5px 12px; cursor:pointer; font:inherit; font-size:14px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 25 | button:hover { background:#343a46; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 26 | label { color:var(--dim); font-size:14px; display:flex; gap:6px; align-items:center; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 27 | .status { font:13px/1.4 ui-monospace, monospace; color:var(--dim); margin-top:8px; min-height:1.4em; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 28 | .status .bad { color:var(--bad); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 29 | .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; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 30 | .q { background:#1d2027; border-left:3px solid var(--accent); padding:8px 12px; margin:10px 0; border-radius:0 6px 6px 0; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 31 | .note { color:var(--dim); font-size:14px; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 32 | </style> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 33 | </head> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 34 | <body> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 35 | <div class="global"> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 36 | <label>phase duration <input id="phaseMs" type="range" min="150" max="1500" step="50" value="450"> <span id="phaseMsOut">450 ms</span></label> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 37 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 38 | </div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 39 | <main> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 40 | <h1>monolith planner, draft 2</h1> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 41 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 42 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 43 | <h2>1. What the planner does</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 44 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 45 | <p>Rules carried over from wry <code>master</code>.</p> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 46 | <ul> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 47 | <li>Tiled windows never overlap while they move.</li> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 48 | <li>Phases are synchronised. Everything in a phase starts and stops together.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 49 | <li>In orthogonal style, the default, a window moves or resizes along one axis per phase.</li> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 50 | <li>An interrupt throws the plan away and replans from what is on screen. Plans never stack.</li> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 51 | </ul> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 52 | <p>New requirements wry never met.</p> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 53 | <ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 54 | <li>Moves between outputs, and large groups such as viewing or hiding a tag.</li> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 55 | <li>Best-effort velocity continuity when an interrupt lands mid-phase.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 56 | <li>A hard bound on plan length, and a construction that never needs a fallback.</li> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 57 | </ul> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 58 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 59 | <h2>2. The problem</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 60 | <p>Swapping two windows by interpolating straight from start to end drives them through each other. Toggle "naive" to compare.</p> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 61 | <div class="demo" id="d1"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 62 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 63 | <h2>3. The core rule, shared separators</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 64 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 65 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 66 | <p>Looking at a single pair gives a lower bound on phases.</p> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 67 | <table> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 68 | <tr><th>pair goes from</th><th>to</th><th>minimum phases</th><th>why</th></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 69 | <tr><td>left of</td><td>left of (any sizes)</td><td>1</td><td>relation shared</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 70 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 71 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 72 | </table> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 73 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 74 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 75 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 76 | <h2>4. One phase, and seeds</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 77 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 78 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 79 | <ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 80 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 81 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 82 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 83 | </ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 84 | <p>Cycle the seed mode below and insert D to compare.</p> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 85 | <div class="demo" id="d2"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 86 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 87 | <div class="demo" id="d3"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 88 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 89 | <h2>5. Two phases, axis order</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 90 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 91 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 92 | <div class="demo" id="d4a"></div> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 93 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 94 | <div class="demo" id="d4b"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 95 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 96 | <h2>6. Three phases, lanes</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 97 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 98 | <ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 99 | <li>Two windows may share a lane only if they never pass each other.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 100 | <li>Windows stacked along the strip axis must keep their stacking order in the lanes, at both ends.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 101 | <li>Only windows that pass something enter lanes. Everything else moves in whichever phase validates, preferring the slide phase.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 102 | <li>The strips span only the band the involved windows occupy, not the whole output.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 103 | </ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 104 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 105 | <div class="demo" id="d5"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 106 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 107 | <h2>7. Four phases, the grid</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 108 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 109 | <ol> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 110 | <li>Every involved window shrinks vertically into its own horizontal strip, ordered by where it starts.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 111 | <li>It shrinks horizontally into its own column, ordered by where it ends. Every window is now a separate small cell.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 112 | <li>It moves vertically to its final height. Its column keeps it clear of everything.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 113 | <li>It expands horizontally to its final width.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 114 | </ol> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 115 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 116 | <div class="demo" id="d9c"></div> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 117 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 118 | <h2>8. Joining and leaving stacks</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 119 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 120 | <div class="demo" id="d9a"></div> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 121 | <div class="demo" id="d9b"></div> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 122 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 123 | <h2>9. Rigid blocks and movement commands</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 124 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 125 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 126 | <div class="demo" id="d3b"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 127 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 128 | <h2>10. Interrupts and velocity</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 129 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 130 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 131 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 132 | <div class="demo" id="d6"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 133 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 134 | <h2>11. Across outputs</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 135 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 136 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 137 | <div class="demo" id="d7"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 138 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 139 | <h2>12. Floating to tiled</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 140 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 141 | <div class="demo" id="d10"></div> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 142 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 143 | <h2>13. Fallbacks</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 144 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 145 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 146 | <h2>14. Algorithm</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 147 | <pre>plan(from, to): |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 148 | groups = motion groups (windows whose swept areas can touch, transitively) |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 149 | for each group: |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 150 | if every pair shares a relation between from and to |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 151 | and (style is direct or every window changes only one axis): |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 152 | return [from, to] # 1 phase |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 153 | if 2-SAT over axis order finds K1: # mixed rects |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 154 | return [from, K1, to] # 2 phases |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 155 | for lane_axis in [y, x]: # strips, then columns |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 156 | for set in [windows in reversals, all movers, all]: |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 157 | for k in 2..|set|: |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 158 | if lanes assignable with k lanes and all three phases validate: |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 159 | return [from, K1, K2, to] # 3 phases |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 160 | for set in [windows in reversals, all movers, all]: |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 161 | if the grid validates for set: # always true for all |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 162 | return [from, K1, K2, K3, to] # 4 phases |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 163 | return fallback(from, to) # unreachable in theory |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 164 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 165 | rigid blocks: run plan() on block rectangles first, then map windows into them |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 166 | floating: slot opens via plan() with a seed, floater travels x, then y, then lands |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 167 | interrupts: from = rectangles sampled at the current presentation time |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 168 | velocity: first-phase curve per group, slope from the fastest mover, |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 169 | applied only if moving the same way, control point height ≤ 1 |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 170 | validation: exact pairwise check over the curve actually used</pre> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 171 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 172 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 173 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 174 | <h2>15. Compared with wry master</h2> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 175 | <table> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 176 | <tr><th>wry master</th><th>this design</th></tr> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 177 | <tr><td>SingleAction, MixedSinglePhase</td><td>1 phase</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 178 | <tr><td>SpaceThenOrthogonalGrowth, OrientationChange, HierarchyOrderedScales</td><td>2 phases, one 2-SAT problem instead of three strategies</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 179 | <tr><td>SwapLanes</td><td>3 phases, general lane assignment for any permutation</td></tr> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 180 | <tr><td>no equivalent</td><td>4 phases, the grid, for simultaneous reversals on both axes</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 181 | <tr><td>ReversedForwardPlan</td><td>not needed, the construction is symmetric</td></tr> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 182 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 183 | <tr><td>Bridged retarget can make plans longer than the original</td><td>every replan obeys the same bound</td></tr> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 184 | <tr><td>Spawns grow from the target centre</td><td>spawns grow from a seed on the old boundary, concurrently</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 185 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 186 | <tr><td>Cross-output moves skip animation</td><td>planned in global coordinates, clipped per output</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 187 | </table> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 188 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 189 | <h2>16. Interspersing tags (open, changes the layout model)</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 190 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 191 | <table> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 192 | <tr><th>option</th><th>meaning</th><th>verdict</th></tr> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 193 | <tr><td>Forbid it</td><td>tags stay in their rows</td><td>limiting</td></tr> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 194 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 195 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 196 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 197 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 198 | </table> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 199 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 200 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 201 | <div class="demo" id="d8"></div> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 202 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 203 | <h2>17. Decisions</h2> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 204 | <h3>Settled</h3> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 205 | <ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 206 | <li>Orthogonal is the default and the flagship look. Direct is a toggle.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 207 | <li>Window moves and block moves both exist as commands. Tag reordering moves rigid blocks.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 208 | <li>One curve per motion group, bounded by phase time rather than amount of motion.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 209 | <li>Floating to tiled is phased, travel one axis per phase, then land.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 210 | <li>The planner must never need a fallback. Pinch and lerp stay as safety nets to try.</li> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 211 | </ul> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 212 | <h3>Open</h3> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 213 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 214 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 215 | <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> |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 216 | <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> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 217 | </main> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 218 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 219 | <script id="planner"> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 220 | const G = 10; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 221 | const EPS = 0.5; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 222 | let PHASE_MS = 450; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 223 | let STYLE = 'orthogonal'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 224 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 225 | const R = (x, y, w, h) => ({ x, y, w, h }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 226 | const clone = L => { const o = {}; for (const k in L) o[k] = { ...L[k] }; return o; }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 227 | const eq = (a, b) => Math.abs(a - b) < 0.05; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 228 | const lerp = (a, b, s) => a + (b - a) * s; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 229 | 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)); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 230 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 231 | function splitX(rect, ids, ratios) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 232 | ratios = ratios || ids.map(() => 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 233 | const tot = ratios.reduce((a, b) => a + b, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 234 | const avail = rect.w - G * (ids.length - 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 235 | let x = rect.x; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 236 | const out = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 237 | ids.forEach((id, i) => { const w = avail * ratios[i] / tot; out[id] = R(x, rect.y, w, rect.h); x += w + G; }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 238 | return out; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 239 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 240 | function splitY(rect, ids, ratios) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 241 | ratios = ratios || ids.map(() => 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 242 | const tot = ratios.reduce((a, b) => a + b, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 243 | const avail = rect.h - G * (ids.length - 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 244 | let y = rect.y; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 245 | const out = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 246 | ids.forEach((id, i) => { const h = avail * ratios[i] / tot; out[id] = R(rect.x, y, rect.w, h); y += h + G; }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 247 | return out; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 248 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 249 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 250 | function relBits(a, b) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 251 | let s = 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 252 | if (a.x + a.w <= b.x + EPS) s |= 1; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 253 | if (b.x + b.w <= a.x + EPS) s |= 2; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 254 | if (a.y + a.h <= b.y + EPS) s |= 4; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 255 | if (b.y + b.h <= a.y + EPS) s |= 8; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 256 | return s; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 257 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 258 | function phaseOk(K1, K2, list) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 259 | for (let i = 0; i < list.length; i++) |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 260 | for (let j = i + 1; j < list.length; j++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 261 | const a = list[i], b = list[j]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 262 | if ((relBits(K1[a], K1[b]) & relBits(K2[a], K2[b])) === 0) return false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 263 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 264 | return true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 265 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 266 | 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)) }; } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 267 | function pureOk(keys) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 268 | if (STYLE !== 'orthogonal') return true; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 269 | for (let k = 0; k + 1 < keys.length; k++) |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 270 | for (const id in keys[k + 1]) { const c = chg(keys[k][id], keys[k + 1][id]); if (c.x && c.y) return false; } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 271 | return true; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 272 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 273 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 274 | function plan(from, to, opts = {}) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 275 | const list = Object.keys(to); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 276 | const T = PHASE_MS; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 277 | if (opts.naive) return { kind: 'naive lerp', keys: [from, to], durs: [T] }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 278 | const movers = list.filter(id => { const c = chg(from[id], to[id]); return c.x || c.y; }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 279 | if (!movers.length) return { kind: 'no-op', keys: [from, to], durs: [0] }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 280 | const pure = movers.every(id => { const c = chg(from[id], to[id]); return !(c.x && c.y); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 281 | if (phaseOk(from, to, list) && (STYLE === 'direct' || pure)) |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 282 | return { kind: '1 phase', keys: [from, to], durs: [T] }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 283 | const two = axisOrder(from, to, list, movers); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 284 | if (two) return two; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 285 | for (const ax of ['y', 'x']) { const l = lanes(from, to, list, movers, ax); if (l) return l; } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 286 | const g = grid(from, to, list, movers); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 287 | if (g) return g; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 288 | return pinch(from, to, movers); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 289 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 290 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 291 | function conflictSet(from, to, list) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 292 | const out = new Set(); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 293 | for (let i = 0; i < list.length; i++) |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 294 | for (let j = i + 1; j < list.length; j++) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 295 | const a = list[i], b = list[j]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 296 | if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { out.add(a); out.add(b); } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 297 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 298 | return [...out]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 299 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 300 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 301 | function grid(from, to, list, movers) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 302 | const cx = r => r.x + r.w / 2, cy = r => r.y + r.h / 2; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 303 | for (const set of [conflictSet(from, to, list), movers, list]) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 304 | const n = set.length; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 305 | if (n < 2) continue; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 306 | let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 307 | for (const id of set) for (const r of [from[id], to[id]]) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 308 | 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); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 309 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 310 | const sh = (y1 - y0 - (n - 1) * G) / n, sw = (x1 - x0 - (n - 1) * G) / n; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 311 | const rowOf = {}, colOf = {}; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 312 | [...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)); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 313 | [...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)); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 314 | const inSet = new Set(set); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 315 | for (const phase of [3, 2, 4, 1]) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 316 | const K = [{}, {}, {}]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 317 | for (const id of list) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 318 | const f = from[id], t = to[id]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 319 | if (inSet.has(id)) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 320 | K[0][id] = R(f.x, rowOf[id], f.w, sh); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 321 | K[1][id] = R(colOf[id], rowOf[id], sw, sh); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 322 | K[2][id] = R(colOf[id], t.y, sw, t.h); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 323 | } else for (let k = 0; k < 3; k++) K[k][id] = k + 1 >= phase ? t : f; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 324 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 325 | const keys = [from, ...K, to]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 326 | let ok = true; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 327 | for (let k = 0; k < 4 && ok; k++) ok = phaseOk(keys[k], keys[k + 1], list); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 328 | if (ok && pureOk(keys)) return { kind: `4 phases (${n}×${n} grid)`, keys, durs: [PHASE_MS, PHASE_MS, PHASE_MS, PHASE_MS] }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 329 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 330 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 331 | return null; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 332 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 333 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 334 | 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); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 335 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 336 | function axisOrder(from, to, list, movers) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 337 | const n = movers.length; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 338 | if (n > 14) return null; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 339 | let best = null, bestScore = Infinity; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 340 | for (let mask = 0; mask < (1 << n); mask++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 341 | const K1 = { ...from }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 342 | let score = 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 343 | movers.forEach((id, i) => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 344 | const xf = !!(mask & (1 << i)); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 345 | const c = chg(from[id], to[id]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 346 | K1[id] = mixed(from[id], to[id], xf); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 347 | if (c.x && !c.y && !xf) score++; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 348 | if (c.y && !c.x && xf) score++; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 349 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 350 | if (score >= bestScore) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 351 | if (phaseOk(from, K1, list) && phaseOk(K1, to, list)) { best = K1; bestScore = score; if (score === 0) break; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 352 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 353 | return best ? { kind: '2 phases (axis order)', keys: [from, best, to], durs: [PHASE_MS, PHASE_MS] } : null; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 354 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 355 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 356 | function lanes(from, to, list, movers, ax) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 357 | const size = ax === 'y' ? 'h' : 'w'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 358 | const bitsT = ax === 'y' ? 3 : 12; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 359 | const before = ax === 'y' ? 4 : 1, after = ax === 'y' ? 8 : 2; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 360 | const conflict = new Set(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 361 | for (let i = 0; i < list.length; i++) |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 362 | for (let j = i + 1; j < list.length; j++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 363 | const a = list[i], b = list[j]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 364 | if ((relBits(from[a], from[b]) & relBits(to[a], to[b])) === 0) { conflict.add(a); conflict.add(b); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 365 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 366 | const sets = [[...conflict], movers, list]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 367 | for (const set of sets) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 368 | const n = set.length; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 369 | if (n < 2) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 370 | let lo = Infinity, hi = -Infinity; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 371 | 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]); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 372 | const diff = [], lt = []; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 373 | for (let i = 0; i < n; i++) { diff.push(new Array(n).fill(false)); lt.push(new Array(n).fill(false)); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 374 | for (let i = 0; i < n; i++) |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 375 | for (let j = 0; j < n; j++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 376 | if (i === j) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 377 | const f = relBits(from[set[i]], from[set[j]]), t = relBits(to[set[i]], to[set[j]]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 378 | if (((f & bitsT) & (t & bitsT)) === 0) diff[i][j] = true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 379 | for (const r of [f, t]) if ((r & bitsT) === 0 && (r & before)) lt[i][j] = true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 380 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 381 | for (let k = 2; k <= n; k++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 382 | const s = (hi - lo - (k - 1) * G) / k; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 383 | if (s < 6) break; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 384 | const lane = new Array(n).fill(-1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 385 | const assign = i => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 386 | if (i === n) return true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 387 | for (let l = 0; l < k; l++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 388 | let ok = true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 389 | for (let j = 0; j < i && ok; j++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 390 | if (diff[i][j] && lane[j] === l) ok = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 391 | if (lt[i][j] && !(l < lane[j])) ok = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 392 | if (lt[j][i] && !(lane[j] < l)) ok = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 393 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 394 | if (ok) { lane[i] = l; if (assign(i + 1)) return true; lane[i] = -1; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 395 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 396 | return false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 397 | }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 398 | if (!assign(0)) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 399 | const strip = l => lo + l * (s + G); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 400 | const inSet = new Set(set); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 401 | for (const phase of [2, 1, 3]) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 402 | const K1 = {}, K2 = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 403 | for (const id of list) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 404 | const f = from[id], t = to[id]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 405 | if (inSet.has(id)) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 406 | const l = lane[set.indexOf(id)]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 407 | K1[id] = ax === 'y' ? R(f.x, strip(l), f.w, s) : R(strip(l), f.y, s, f.h); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 408 | K2[id] = ax === 'y' ? R(t.x, strip(l), t.w, s) : R(strip(l), t.y, s, t.h); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 409 | } else if (phase === 1) { K1[id] = t; K2[id] = t; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 410 | else if (phase === 2) { K1[id] = f; K2[id] = t; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 411 | else { K1[id] = f; K2[id] = f; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 412 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 413 | if (phaseOk(from, K1, list) && phaseOk(K1, K2, list) && phaseOk(K2, to, list) && pureOk([from, K1, K2, to])) |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 414 | return { kind: `3 phases (${k} lanes)`, keys: [from, K1, K2, to], durs: [PHASE_MS, PHASE_MS, PHASE_MS] }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 415 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 416 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 417 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 418 | return null; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 419 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 420 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 421 | function pinch(from, to, movers) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 422 | const K1 = { ...from }, K2 = { ...to }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 423 | for (const id of movers) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 424 | const f = from[id], t = to[id]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 425 | K1[id] = R(f.x + f.w / 2, f.y + f.h / 2, 0, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 426 | K2[id] = R(t.x + t.w / 2, t.y + t.h / 2, 0, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 427 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 428 | return { kind: 'pinch fallback', keys: [from, K1, K2, to], durs: [PHASE_MS, 0, PHASE_MS] }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 429 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 430 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 431 | function bbox(rects) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 432 | let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 433 | 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); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 434 | return R(x0, y0, x1 - x0, y1 - y0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 435 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 436 | function planBlocks(from, to, groups) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 437 | const bf = {}, bt = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 438 | for (const b in groups) { bf[b] = bbox(groups[b].map(id => from[id])); bt[b] = bbox(groups[b].map(id => to[id])); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 439 | const bp = plan(bf, bt); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 440 | const last = bp.keys.length - 1; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 441 | const keys = bp.keys.map((K, ki) => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 442 | if (ki === 0) return from; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 443 | if (ki === last) return to; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 444 | const L = { ...from }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 445 | for (const b in groups) for (const id of groups[b]) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 446 | const r = from[id], box = bf[b], k = K[b]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 447 | const nx = box.w ? (r.x - box.x) / box.w : 0, ny = box.h ? (r.y - box.y) / box.h : 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 448 | const nw = box.w ? r.w / box.w : 0, nh = box.h ? r.h / box.h : 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 449 | L[id] = R(k.x + nx * k.w, k.y + ny * k.h, nw * k.w, nh * k.h); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 450 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 451 | return L; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 452 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 453 | return { kind: bp.kind + ' on blocks', keys, durs: bp.durs }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 454 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 455 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 456 | function bezier(x1, y1, x2, y2) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 457 | const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 458 | const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 459 | const X = u => ((ax * u + bx) * u + cx) * u, Y = u => ((ay * u + by) * u + cy) * u; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 460 | const dX = u => (3 * ax * u + 2 * bx) * u + cx; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 461 | return t => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 462 | if (t <= 0) return 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 463 | if (t >= 1) return 1; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 464 | let u = t; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 465 | for (let i = 0; i < 8; i++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 466 | const x = X(u) - t; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 467 | if (Math.abs(x) < 1e-6) return Y(u); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 468 | const d = dX(u); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 469 | if (Math.abs(d) < 1e-6) break; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 470 | u -= x / d; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 471 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 472 | let lo = 0, hi = 1; u = t; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 473 | for (let i = 0; i < 40; i++) { if (X(u) < t) lo = u; else hi = u; u = (lo + hi) / 2; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 474 | return Y(u); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 475 | }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 476 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 477 | const BASE = bezier(0.45, 0, 0.2, 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 478 | const edges = r => [r.x, r.x + r.w, r.y, r.y + r.h]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 479 | </script> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 480 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 481 | <script> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 482 | const PAL = { A: '#e0774a', B: '#4aa3e0', C: '#7bc96f', D: '#c77be0', E: '#e0c24a', F: '#4ae0c2' }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 483 | const demos = []; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 484 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 485 | class Demo { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 486 | constructor(id, W, H, opts = {}) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 487 | this.W = W; this.H = H; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 488 | this.outputs = opts.outputs || [R(0, 0, W, H)]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 489 | this.floating = new Set(opts.floating || []); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 490 | const el = document.getElementById(id); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 491 | this.canvas = document.createElement('canvas'); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 492 | const dpr = window.devicePixelRatio || 1; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 493 | this.canvas.width = W * dpr; this.canvas.height = H * dpr; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 494 | this.canvas.style.maxWidth = W + 'px'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 495 | this.ctx = this.canvas.getContext('2d'); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 496 | this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 497 | el.appendChild(this.canvas); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 498 | if (opts.graph) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 499 | this.graph = document.createElement('canvas'); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 500 | this.graph.width = W * dpr; this.graph.height = 90 * dpr; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 501 | this.graph.style.maxWidth = W + 'px'; this.graph.style.marginTop = '8px'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 502 | this.gctx = this.graph.getContext('2d'); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 503 | this.gctx.setTransform(dpr, 0, 0, dpr, 0, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 504 | el.appendChild(this.graph); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 505 | this.hist = []; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 506 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 507 | this.controls = document.createElement('div'); this.controls.className = 'controls'; el.appendChild(this.controls); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 508 | this.status = document.createElement('div'); this.status.className = 'status'; el.appendChild(this.status); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 509 | this.anim = null; this.prev = null; this.vel = {}; this.naive = false; this.vmatch = true; this.lastKind = 'idle'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 510 | demos.push(this); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 511 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 512 | button(label, fn) { const b = document.createElement('button'); b.textContent = label; b.onclick = fn; this.controls.appendChild(b); return b; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 513 | check(label, init, fn) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 514 | const l = document.createElement('label'); const c = document.createElement('input'); c.type = 'checkbox'; c.checked = init; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 515 | c.onchange = () => fn(c.checked); l.appendChild(c); l.appendChild(document.createTextNode(label)); this.controls.appendChild(l); return c; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 516 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 517 | set(L) { this.layout = clone(L); this.anim = null; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 518 | go(target, opts = {}) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 519 | const now = performance.now(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 520 | const cur = this.sampleAt(now); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 521 | const from = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 522 | for (const id in target) from[id] = cur[id] || target[id]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 523 | const p = opts.planFn ? opts.planFn(from) : opts.blocks ? planBlocks(from, target, opts.blocks) : plan(from, target, { naive: this.naive }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 524 | const curves = p.durs.map(() => BASE); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 525 | const interrupted = !!this.anim; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 526 | let matched = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 527 | if (this.vmatch && interrupted && p.durs[0] > 0 && p.keys.length > 1) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 528 | const K1 = p.keys[1]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 529 | let best = null, bs = 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 530 | for (const id in K1) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 531 | const v = this.vel[id]; if (!v) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 532 | const D = edges(K1[id]).map((e, i) => e - edges(from[id])[i]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 533 | const dd = D.reduce((a, b) => a + b * b, 0); if (dd < 1) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 534 | const sp = Math.hypot(...v); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 535 | if (sp > bs) { bs = sp; best = { v, D, dd }; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 536 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 537 | if (best) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 538 | const s = best.v.reduce((a, b, i) => a + b * best.D[i], 0) / best.dd; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 539 | const slope = s * p.durs[0]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 540 | if (slope > 0.05) { const x1 = 0.3; curves[0] = bezier(x1, Math.min(1, x1 * slope), 0.2, 1); matched = true; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 541 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 542 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 543 | this.anim = { keys: p.keys, durs: p.durs, curves, start: now }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 544 | this.lastKind = p.kind + (interrupted ? ', replanned' : '') + (matched ? ', velocity matched' : ''); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 545 | this.overlapSeen = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 546 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 547 | sampleAt(t) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 548 | if (!this.anim) return clone(this.layout); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 549 | let e = t - this.anim.start; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 550 | const { keys, durs, curves } = this.anim; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 551 | for (let i = 0; i < durs.length; i++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 552 | const d = durs[i]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 553 | if (e < d) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 554 | this.phase = i; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 555 | const s = curves[i](e / d); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 556 | const L = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 557 | for (const id in keys[i + 1]) L[id] = lerpRect(keys[i][id], keys[i + 1][id], s); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 558 | return L; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 559 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 560 | e -= d; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 561 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 562 | this.layout = clone(keys[keys.length - 1]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 563 | this.anim = null; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 564 | return clone(this.layout); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 565 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 566 | frame(now) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 567 | const L = this.sampleAt(now); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 568 | if (this.prev) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 569 | const dt = now - this.prev.t; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 570 | if (dt > 0) for (const id in L) if (this.prev.L[id]) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 571 | const a = edges(this.prev.L[id]), b = edges(L[id]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 572 | this.vel[id] = b.map((v, i) => (v - a[i]) / dt); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 573 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 574 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 575 | this.prev = { t: now, L }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 576 | this.draw(L); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 577 | const overlaps = this.findOverlaps(L); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 578 | if (overlaps.length) this.overlapSeen = true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 579 | const phaseTxt = this.anim ? ` · phase ${this.phase + 1}/${this.anim.durs.length}` : ''; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 580 | this.status.innerHTML = `${this.lastKind}${phaseTxt}` + (this.overlapSeen ? ' · <span class="bad">overlap</span>' : ''); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 581 | if (this.graph) this.drawGraph(now); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 582 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 583 | findOverlaps(L) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 584 | const ids = Object.keys(L).filter(id => L[id].w > 0.5 && L[id].h > 0.5 && !this.floating.has(id)); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 585 | const out = []; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 586 | for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 587 | const a = L[ids[i]], b = L[ids[j]]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 588 | const x0 = Math.max(a.x, b.x), y0 = Math.max(a.y, b.y); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 589 | const x1 = Math.min(a.x + a.w, b.x + b.w), y1 = Math.min(a.y + a.h, b.y + b.h); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 590 | if (x1 - x0 > 1 && y1 - y0 > 1) out.push(R(x0, y0, x1 - x0, y1 - y0)); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 591 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 592 | return out; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 593 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 594 | draw(L) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 595 | const c = this.ctx; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 596 | c.clearRect(0, 0, this.W, this.H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 597 | const overlaps = this.findOverlaps(L); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 598 | for (const o of this.outputs) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 599 | c.save(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 600 | c.fillStyle = '#1b1e25'; c.fillRect(o.x, o.y, o.w, o.h); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 601 | c.beginPath(); c.rect(o.x, o.y, o.w, o.h); c.clip(); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 602 | for (const id of Object.keys(L).sort((a, b) => this.floating.has(a) - this.floating.has(b))) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 603 | const r = L[id]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 604 | if (r.w < 0.5 || r.h < 0.5) continue; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 605 | const rad = Math.min(8, r.w / 2, r.h / 2); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 606 | c.beginPath(); c.roundRect(r.x, r.y, r.w, r.h, rad); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 607 | c.fillStyle = (PAL[id] || '#888') + 'cc'; c.fill(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 608 | c.strokeStyle = PAL[id] || '#888'; c.lineWidth = 1.5; c.stroke(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 609 | if (r.w > 22 && r.h > 22) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 610 | c.fillStyle = '#0d0f12'; c.font = '600 16px system-ui'; c.textAlign = 'center'; c.textBaseline = 'middle'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 611 | c.fillText(id, r.x + r.w / 2, r.y + r.h / 2); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 612 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 613 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 614 | 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); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 615 | c.restore(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 616 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 617 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 618 | drawGraph(now) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 619 | const v = this.vel.A; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 620 | const sp = v ? Math.hypot(v[0] + v[1], v[2] + v[3]) * 1000 / 2 : 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 621 | this.hist.push([now, sp]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 622 | while (this.hist.length && now - this.hist[0][0] > 4000) this.hist.shift(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 623 | const c = this.gctx, W = this.W, H = 90; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 624 | c.clearRect(0, 0, W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 625 | c.fillStyle = '#0d0f12'; c.fillRect(0, 0, W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 626 | const max = Math.max(400, ...this.hist.map(h => h[1])); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 627 | c.beginPath(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 628 | 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); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 629 | c.strokeStyle = PAL.A; c.lineWidth = 2; c.stroke(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 630 | c.fillStyle = '#8a909c'; c.font = '12px system-ui'; c.textAlign = 'left'; c.fillText('speed of window A, last 4 s', 8, 14); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 631 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 632 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 633 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 634 | const inner = (W, H, ox = 0) => R(ox + G, G, W - 2 * G, H - 2 * G); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 635 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 636 | { // d1 swap |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 637 | const W = 640, H = 260, d = new Demo('d1', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 638 | let swapped = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 639 | const L = () => splitX(I, swapped ? ['B', 'A'] : ['A', 'B']); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 640 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 641 | d.button('Swap A and B', () => { swapped = !swapped; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 642 | d.check('naive', false, v => d.naive = v); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 643 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 644 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 645 | { // d2 one phase |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 646 | const W = 640, H = 300, d = new Demo('d2', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 647 | const s = { wide: false, d: false, e: false }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 648 | const modes = ['line on old boundary', 'point on old boundary', 'target centre']; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 649 | let mode = 0; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 650 | const ratios = ids => ids.map(id => (id === 'A' && s.wide) ? 2 : 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 651 | const L = () => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 652 | const ids = s.d ? ['A', 'B', 'D', 'C'] : ['A', 'B', 'C']; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 653 | const out = splitX(I, ids, ratios(ids)); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 654 | if (!s.d) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 655 | const bx = (out.B.x + out.B.w + out.C.x) / 2; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 656 | const T = splitX(I, ['A', 'B', 'D', 'C'], ratios(['A', 'B', 'D', 'C'])).D; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 657 | 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]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 658 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 659 | const b = out.B; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 660 | if (s.e) Object.assign(out, splitY(b, ['B', 'E'])); else out.E = R(b.x, b.y + b.h, b.w, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 661 | return out; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 662 | }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 663 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 664 | d.button('Widen A', () => { s.wide = !s.wide; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 665 | d.button('Insert / remove D', () => { s.d = !s.d; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 666 | d.button('Split B with E', () => { s.e = !s.e; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 667 | d.button('All at once', () => { s.wide = !s.wide; s.d = !s.d; s.e = !s.e; d.go(L()); }); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 668 | const mb = d.button('seed: ' + modes[0], () => { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 669 | mode = (mode + 1) % modes.length; mb.textContent = 'seed: ' + modes[mode]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 670 | if (!s.d && !d.anim) d.set(L()); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 671 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 672 | d.check('naive', false, v => d.naive = v); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 673 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 674 | function tagLayout(I, view2, order) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 675 | const L = {}; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 676 | if (view2) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 677 | const rows = splitY(I, order); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 678 | Object.assign(L, splitX(rows.t1, ['A', 'B', 'C']), splitX(rows.t2, ['D', 'E'])); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 679 | } else { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 680 | Object.assign(L, splitX(I, ['A', 'B', 'C'])); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 681 | const y = order[0] === 't2' ? I.y : I.y + I.h; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 682 | Object.assign(L, splitX(R(I.x, y, I.w, 0), ['D', 'E'])); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 683 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 684 | return L; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 685 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 686 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 687 | { // d3 tag toggle |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 688 | const W = 640, H = 320, d = new Demo('d3', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 689 | let view2 = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 690 | d.set(tagLayout(I, view2, ['t1', 't2'])); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 691 | d.button('View / hide tag 2 (D, E)', () => { view2 = !view2; d.go(tagLayout(I, view2, ['t1', 't2'])); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 692 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 693 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 694 | { // d3b tag reorder |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 695 | const W = 640, H = 320, d = new Demo('d3b', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 696 | let order = ['t1', 't2'], blocks = true; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 697 | d.set(tagLayout(I, true, order)); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 698 | 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'] } } : {}); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 699 | d.check('rigid blocks', true, v => blocks = v); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 700 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 701 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 702 | { // d4a orientation flip |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 703 | const W = 640, H = 300, d = new Demo('d4a', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 704 | let rows = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 705 | const L = () => rows ? splitY(I, ['A', 'B', 'C']) : splitX(I, ['A', 'B', 'C']); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 706 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 707 | d.button('Flip orientation', () => { rows = !rows; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 708 | d.check('naive', false, v => d.naive = v); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 709 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 710 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 711 | { // d4b grow both axes |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 712 | const W = 640, H = 300, d = new Demo('d4b', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 713 | let wide = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 714 | const L = () => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 715 | const r = splitY(I, ['top', 'bot'], wide ? [7, 3] : [1, 1]); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 716 | return { ...splitX(r.top, ['A', 'B'], wide ? [7, 3] : [1, 1]), ...splitX(r.bot, ['C', 'D']) }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 717 | }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 718 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 719 | d.button('Grow A in both directions', () => { wide = !wide; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 720 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 721 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 722 | { // d5 lanes |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 723 | const W = 640, H = 300, d = new Demo('d5', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 724 | let order = ['A', 'B', 'C', 'D']; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 725 | const L = () => splitX(I, order); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 726 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 727 | d.button('Swap first two', () => { order = [order[1], order[0], ...order.slice(2)]; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 728 | d.button('Rotate', () => { order = [...order.slice(1), order[0]]; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 729 | d.button('Reverse', () => { order = [...order].reverse(); d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 730 | d.button('Reset', () => { order = ['A', 'B', 'C', 'D']; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 731 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 732 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 733 | { // d6 interrupts |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 734 | const W = 640, H = 260, d = new Demo('d6', W, H, { graph: true }), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 735 | const targets = [ |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 736 | () => splitX(I, ['A', 'B', 'C']), |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 737 | () => splitX(I, ['C', 'A', 'B']), |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 738 | () => splitX(I, ['A', 'B', 'C'], [3, 1, 1]), |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 739 | () => splitX(I, ['B', 'C', 'A']), |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 740 | () => ({ ...splitX(I, ['A', 'C'], [2, 1]), B: R(I.x + I.w * 0.66, I.y + I.h, I.w * 0.34, 0) }), |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 741 | ]; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 742 | const withB = L => { if (!L.B) L.B = R(I.x, I.y, 0, 0); return L; }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 743 | let idx = 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 744 | d.set(withB(targets[0]())); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 745 | const next = () => { idx = (idx + 1) % targets.length; d.go(withB(targets[idx]())); }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 746 | d.button('Next target', next); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 747 | d.button('Storm (8 rapid interrupts)', () => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 748 | let n = 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 749 | const fire = () => { next(); if (++n < 8) setTimeout(fire, 180 + Math.random() * 300); }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 750 | fire(); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 751 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 752 | d.check('match velocity', true, v => d.vmatch = v); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 753 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 754 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 755 | { // d7 across outputs |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 756 | const W = 680, H = 260; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 757 | const o1 = R(0, 0, 320, H), o2 = R(360, 0, 320, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 758 | const d = new Demo('d7', W, H, { outputs: [o1, o2] }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 759 | const I1 = inner(320, H, 0), I2 = inner(320, H, 360); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 760 | let s1 = ['A', 'B'], s2 = ['C']; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 761 | const L = () => ({ ...splitX(I1, s1), ...splitX(I2, s2) }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 762 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 763 | d.button('Move B across', () => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 764 | if (s1.includes('B')) { s1 = s1.filter(x => x !== 'B'); s2 = ['B', ...s2]; } else { s2 = s2.filter(x => x !== 'B'); s1 = [...s1, 'B']; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 765 | d.go(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 766 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 767 | d.button('Move A across', () => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 768 | if (s1.includes('A')) { s1 = s1.filter(x => x !== 'A'); s2 = ['A', ...s2]; } else { s2 = s2.filter(x => x !== 'A'); s1 = ['A', ...s1]; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 769 | d.go(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 770 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 771 | d.button('Reset', () => { s1 = ['A', 'B']; s2 = ['C']; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 772 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 773 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 774 | function layoutTree(node, rect, visible, out) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 775 | if (typeof node === 'string') { out[node] = visible(node) ? rect : R(rect.x, rect.y, 0, 0); return; } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 776 | const horiz = node.dir === 'row'; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 777 | const shown = node.kids.map(k => anyVisible(k, visible)); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 778 | const m = shown.filter(Boolean).length; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 779 | const len = horiz ? rect.w : rect.h; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 780 | const avail = len - G * Math.max(0, m - 1); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 781 | let pos = horiz ? rect.x : rect.y, seen = 0; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 782 | node.kids.forEach((k, i) => { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 783 | if (!shown[i]) { |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 784 | const p = seen ? pos - G / 2 : pos; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 785 | const r = horiz ? R(p, rect.y, 0, rect.h) : R(rect.x, p, rect.w, 0); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 786 | layoutTree(k, r, () => false, out); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 787 | return; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 788 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 789 | const size = avail / m; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 790 | const r = horiz ? R(pos, rect.y, size, rect.h) : R(rect.x, pos, rect.w, size); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 791 | layoutTree(k, r, visible, out); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 792 | pos += size + G; seen++; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 793 | }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 794 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 795 | function anyVisible(node, visible) { return typeof node === 'string' ? visible(node) : node.kids.some(k => anyVisible(k, visible)); } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 796 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 797 | { // d8 one tree, tags filter |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 798 | const W = 640, H = 320, d = new Demo('d8', W, H), I = inner(W, H); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 799 | const tagOf = { A: 1, B: 1, C: 1, D: 2, E: 2 }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 800 | const view = { 1: true, 2: true }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 801 | let mixed = false; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 802 | const tree = () => mixed |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 803 | ? { dir: 'col', kids: [{ dir: 'row', kids: ['B', 'C'] }, { dir: 'row', kids: ['D', 'A', 'E'] }] } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 804 | : { dir: 'col', kids: [{ dir: 'row', kids: ['A', 'B', 'C'] }, { dir: 'row', kids: ['D', 'E'] }] }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 805 | const L = () => { const out = {}; layoutTree(tree(), I, id => view[tagOf[id]], out); return out; }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 806 | d.set(L()); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 807 | d.button('Toggle tag 1 (A, B, C)', () => { view[1] = !view[1]; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 808 | d.button('Toggle tag 2 (D, E)', () => { view[2] = !view[2]; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 809 | d.button('Intersperse A between D and E', () => { mixed = !mixed; d.go(L()); }); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 810 | } |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 811 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 812 | function stackLayouts(I) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 813 | const col = (L, id, ids) => { const r = L[id]; delete L[id]; Object.assign(L, splitY(r, ids)); return L; }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 814 | const row = (L, id, ids) => { const r = L[id]; delete L[id]; Object.assign(L, splitX(r, ids)); return L; }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 815 | return { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 816 | 'A | B | C': () => splitX(I, ['A', 'B', 'C']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 817 | 'A | (B / C)': () => col(splitX(I, ['A', 'R']), 'R', ['B', 'C']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 818 | '(A / C) | B': () => col(splitX(I, ['L', 'B']), 'L', ['A', 'C']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 819 | '(A / B) | C': () => col(splitX(I, ['L', 'C']), 'L', ['A', 'B']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 820 | 'B | C | A': () => splitX(I, ['B', 'C', 'A']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 821 | 'A / B / C': () => splitY(I, ['A', 'B', 'C']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 822 | 'A / (B | C)': () => row(splitY(I, ['A', 'R']), 'R', ['B', 'C']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 823 | '(A / B) | (C / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'B']), 'R', ['C', 'D']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 824 | 'A | (C / B / D)': () => col(splitX(I, ['A', 'R']), 'R', ['C', 'B', 'D']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 825 | '(A / D) | (C / B)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'D']), 'R', ['C', 'B']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 826 | '(A / B / C) | D': () => col(splitX(I, ['L', 'D']), 'L', ['A', 'B', 'C']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 827 | '(A / C) | (B / D)': () => col(col(splitX(I, ['L', 'R']), 'L', ['A', 'C']), 'R', ['B', 'D']), |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 828 | }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 829 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 830 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 831 | for (const [id, names] of [ |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 832 | ['d9a', ['A | B | C', 'A | (B / C)', '(A / C) | B', '(A / B) | C', 'B | C | A', 'A / B / C', 'A / (B | C)']], |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 833 | ['d9b', ['(A / B) | (C / D)', 'A | (C / B / D)', '(A / D) | (C / B)', '(A / B / C) | D', '(A / C) | (B / D)']], |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 834 | ]) { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 835 | const W = 640, H = 300, d = new Demo(id, W, H), I = inner(W, H), S = stackLayouts(I); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 836 | d.set(S[names[0]]()); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 837 | for (const n of names) d.button(n, () => d.go(S[n]())); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 838 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 839 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 840 | { // d9c grid |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 841 | const W = 640, H = 300, d = new Demo('d9c', W, H), I = inner(W, H), S = stackLayouts(I); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 842 | let at = 0; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 843 | const names = ['(A / D) | (C / B)', '(A / B / C) | D']; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 844 | d.set(S[names[0]]()); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 845 | d.button('(A / D) | (C / B) ⇄ (A / B / C) | D', () => { at = 1 - at; d.go(S[names[at]]()); }); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 846 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 847 | |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 848 | { // d10 floating to tiled |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 849 | const W = 640, H = 300, d = new Demo('d10', W, H, { floating: ['F'] }), I = inner(W, H); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 850 | const FL = R(220, 40, 220, 130); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 851 | let tiled = false; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 852 | const tiledL = () => splitX(I, ['A', 'B', 'F', 'C']); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 853 | const floatL = () => ({ ...splitX(I, ['A', 'B', 'C']), F: FL }); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 854 | const ortho = () => STYLE === 'orthogonal'; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 855 | const assemble = (n, tiledAt, fAt, kind) => { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 856 | const keys = []; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 857 | 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) }); } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 858 | return { kind, keys, durs: keys.slice(1).map(() => PHASE_MS) }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 859 | }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 860 | const floatIn = from => { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 861 | const slot = tiledL().F, to = tiledL(); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 862 | const bx = (from.B.x + from.B.w + from.C.x) / 2; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 863 | 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 }); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 864 | const k = tp.durs.length, f = from.F; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 865 | const cx = slot.x + slot.w / 2 - f.w / 2, cy = slot.y + slot.h / 2 - f.h / 2; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 866 | 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)]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 867 | const n = Math.max(path.length - 1, k) + 1; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 868 | 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}`); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 869 | }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 870 | const floatOut = from => { |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 871 | const slot = from.F, to = floatL(); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 872 | const bx = (to.B.x + to.B.w + to.C.x) / 2; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 873 | 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) }); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 874 | const k = tp.durs.length; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 875 | const lift = R(slot.x + slot.w / 2 - FL.w / 2, slot.y + slot.h / 2 - FL.h / 2, FL.w, FL.h); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 876 | const path = ortho() ? [slot, lift, R(lift.x, FL.y, FL.w, FL.h), FL] : [slot, lift, FL]; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 877 | const n = Math.max(path.length - 1, k + 1); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 878 | 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}`); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 879 | }; |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 880 | d.set(floatL()); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 881 | d.button('Tile F / float F', () => { tiled = !tiled; d.go(tiled ? tiledL() : floatL(), { planFn: tiled ? floatIn : floatOut }); }); |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 882 | } |
cd7085e54c88 | atagen | 2026-10-16 06:58 UTC | 883 | |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 884 | document.getElementById('phaseMs').oninput = e => { PHASE_MS = +e.target.value; document.getElementById('phaseMsOut').textContent = PHASE_MS + ' ms'; }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 885 | document.getElementById('style').onchange = e => { STYLE = e.target.value; }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 886 | const loop = now => { for (const d of demos) d.frame(now); requestAnimationFrame(loop); }; |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 887 | requestAnimationFrame(loop); |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 888 | </script> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 889 | </body> |
05668b0d4753 | atagen | 2026-10-16 05:50 UTC | 890 | </html> |