diff --git a/docs/site/css/style.css b/docs/site/css/style.css index 20be272..8046b26 100644 --- a/docs/site/css/style.css +++ b/docs/site/css/style.css @@ -126,7 +126,7 @@ code { font-size: 0.875em; } .gates { list-style: none; margin: 0; - padding: 0 56px 0 0; + padding: 0 8px 0 0; position: relative; } @@ -143,7 +143,7 @@ code { font-size: 0.875em; } .gates > li { display: grid; grid-template-columns: 32px 1fr; - gap: 16px; + gap: 48px; position: relative; padding: 0 0 28px; } @@ -231,32 +231,7 @@ code { font-size: 0.875em; } /* right-hand loop 5 → 2 */ .loop-a::after, .loop-m::after, -.loop-z::after { - content: ""; - position: absolute; - right: -48px; - width: 40px; - box-sizing: border-box; - border-right: 1px solid var(--ink); - pointer-events: none; -} - -.loop-a::after { - top: 16px; - bottom: 0; - border-top: 1px solid var(--ink); -} - -.loop-m::after { - top: 0; - bottom: 0; -} - -.loop-z::after { - top: 0; - height: 16px; - border-bottom: 1px solid var(--ink); -} +.loop-z::after { content: none; } .reenter { margin: 10px 0 0; @@ -268,10 +243,7 @@ code { font-size: 0.875em; } .reenter .arrow { color: var(--mute); } @media (max-width: 720px) { - .gates { padding-right: 0; } - .loop-a::after, - .loop-m::after, - .loop-z::after { display: none; } + .gates > li { gap: 40px; } } /* Record rows: 1px lines, no cards, no shadows */ @@ -496,7 +468,7 @@ code { font-size: 0.875em; } } .js .gates > li { - opacity: 0.22; + opacity: 0.55; transform: translateY(14px); transition: opacity 0.55s ease, transform 0.55s ease; } @@ -519,23 +491,6 @@ code { font-size: 0.875em; } .js .gates > li.is-on .n-src { background: var(--ink); } - -.js .loop-a::after, -.js .loop-m::after, -.js .loop-z::after { - opacity: 0; - transform: scaleY(0.08); - transform-origin: top; - transition: opacity 0.5s ease, transform 0.7s ease; -} - -.js .gates.is-loop .loop-a::after, -.js .gates.is-loop .loop-m::after, -.js .gates.is-loop .loop-z::after { - opacity: 1; - transform: none; -} - .js .reenter { opacity: 0; transform: translateX(8px); @@ -558,7 +513,7 @@ code { font-size: 0.875em; } } .js .block { - opacity: 0.28; + opacity: 0.7; transform: translateY(18px); transition: opacity 0.65s ease, transform 0.65s ease; } @@ -583,10 +538,7 @@ code { font-size: 0.875em; } .js .gates > li, .js .block, .js .ident, - .js .reenter, - .js .loop-a::after, - .js .loop-m::after, - .js .loop-z::after { + .js .reenter { opacity: 1; transform: none; animation: none; @@ -596,6 +548,7 @@ code { font-size: 0.875em; } } + .loop-svg { position: absolute; inset: 0; @@ -603,49 +556,29 @@ code { font-size: 0.875em; } height: 100%; overflow: visible; pointer-events: none; - z-index: 2; + z-index: 3; } .loop-track { - fill: none; - stroke: var(--line); - stroke-width: 1; -} - -.loop-run { fill: none; stroke: var(--ink); - stroke-width: 1.75; - stroke-linecap: square; - stroke-dashoffset: 0; + stroke-width: 1.5; + opacity: 0.55; } -.js .gates.is-loop .loop-run { - animation: loop-travel 1.7s linear 3 forwards; +.loop-token { + fill: var(--ink); + stroke: var(--bg); + stroke-width: 2; } -@keyframes loop-travel { - from { stroke-dashoffset: 0; } - to { stroke-dashoffset: calc(-1 * var(--len, 800)); } -} - -.js .gates.is-loop .loop-a::after, -.js .gates.is-loop .loop-m::after, -.js .gates.is-loop .loop-z::after { - display: none; -} - -.js .gates.is-loop-done .loop-run { - stroke-dasharray: none; - animation: none; - stroke: var(--ink); - opacity: 0.35; +.js .gates > li.is-now .n:not(.n-src) { + background: var(--kill); + border-color: var(--kill); + color: var(--bg); } @media (prefers-reduced-motion: reduce) { - .js .gates.is-loop .loop-run { - animation: none; - stroke-dasharray: none; - opacity: 0.7; - } + .loop-token { display: none; } + .loop-track { opacity: 1; } } diff --git a/docs/site/js/main.js b/docs/site/js/main.js index d35ec47..0e11b20 100644 --- a/docs/site/js/main.js +++ b/docs/site/js/main.js @@ -1,4 +1,4 @@ -/* Copy for install. Scroll draws the protocol. The 5→2 path runs as a loop, cap 3. */ +/* Copy for install. Scroll draws the protocol. A token orbits 2→5→2, cap 3. */ (function () { "use strict"; @@ -51,13 +51,16 @@ var gates = document.querySelector(".gates"); var steps = gates ? Array.prototype.slice.call(gates.querySelectorAll(":scope > li")) : []; var blocks = Array.prototype.slice.call(document.querySelectorAll(".block")); + var loopSteps = gates + ? Array.prototype.slice.call(gates.querySelectorAll(".loop-a, .loop-m, .loop-z")) + : []; var ink = null; var svg = null; - var runPath = null; - var trackPath = null; + var track = null; + var token = null; var loopStarted = false; + var raf = 0; var reenter = gates ? gates.querySelector(".reenter") : null; - var reenterHome = reenter ? reenter.innerHTML : ""; function paintRail() { if (!gates || !ink) return; @@ -83,36 +86,48 @@ var x1 = ar.left - gr.left + ar.width / 2; var y1 = ar.top - gr.top + ar.height / 2; var y2 = zr.top - gr.top + zr.height / 2; - var x2 = Math.max(x1 + 40, gates.clientWidth - 24); - return "M " + x1 + " " + y1 + " L " + x1 + " " + y2 + " L " + x2 + " " + y2 + " L " + x2 + " " + y1 + " Z"; + var x2 = x1 + 44; + var r = 8; + return ( + "M " + x1 + " " + (y1 + r) + + " L " + x1 + " " + (y2 - r) + + " Q " + x1 + " " + y2 + " " + (x1 + r) + " " + y2 + + " L " + (x2 - r) + " " + y2 + + " Q " + x2 + " " + y2 + " " + x2 + " " + (y2 - r) + + " L " + x2 + " " + (y1 + r) + + " Q " + x2 + " " + y1 + " " + (x2 - r) + " " + y1 + + " L " + (x1 + r) + " " + y1 + + " Q " + x1 + " " + y1 + " " + x1 + " " + (y1 + r) + ); } - function layoutLoop() { - if (!gates || !runPath || !trackPath) return; - var d = loopPathD(); - if (!d) return; - trackPath.setAttribute("d", d); - runPath.setAttribute("d", d); - var len = 0; - try { len = runPath.getTotalLength(); } catch (e) { len = 800; } - runPath.style.setProperty("--len", String(len)); - runPath.style.strokeDasharray = "22 " + len; - } - - function ensureLoopSvg() { + function ensureLoop() { if (!gates || svg) return; svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("class", "loop-svg"); svg.setAttribute("aria-hidden", "true"); - trackPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); - trackPath.setAttribute("class", "loop-track"); - runPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); - runPath.setAttribute("class", "loop-run"); - svg.appendChild(trackPath); - svg.appendChild(runPath); + track = document.createElementNS("http://www.w3.org/2000/svg", "path"); + track.setAttribute("class", "loop-track"); + token = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + token.setAttribute("class", "loop-token"); + token.setAttribute("r", "6"); + svg.appendChild(track); + svg.appendChild(token); gates.appendChild(svg); } + function layoutLoop() { + if (!gates || !track) return; + var w = Math.max(80, gates.offsetWidth); + var h = Math.max(1, gates.offsetHeight); + svg.setAttribute("width", String(w)); + svg.setAttribute("height", String(h)); + svg.setAttribute("viewBox", "0 0 " + w + " " + h); + var d = loopPathD(); + if (!d) return; + track.setAttribute("d", d); + } + function setLap(n, done) { if (!reenter) return; if (done) { @@ -122,23 +137,78 @@ reenter.innerHTML = 'loop ' + n + " / 3 · 5 → 2"; } + function markNow(pt) { + var best = null; + var bestD = 1e9; + loopSteps.forEach(function (li) { + var n = li.querySelector(".n"); + if (!n) return; + var gr = gates.getBoundingClientRect(); + var r = n.getBoundingClientRect(); + var y = r.top - gr.top + r.height / 2; + var d = Math.abs(y - pt.y); + if (d < bestD) { + bestD = d; + best = li; + } + }); + loopSteps.forEach(function (li) { + if (li === best) li.classList.add("is-now"); + else li.classList.remove("is-now"); + }); + } + + function place(len, dist) { + if (!track || !token || !len) return; + var pt = track.getPointAtLength(((dist % len) + len) % len); + token.setAttribute("cx", pt.x); + token.setAttribute("cy", pt.y); + markNow(pt); + } + function startLoop() { - if (!gates || loopStarted || reduce) return; + if (!gates || loopStarted) return; loopStarted = true; - ensureLoopSvg(); + ensureLoop(); layoutLoop(); gates.classList.add("is-loop"); setLap(1, false); - if (!runPath) return; - var lap = 1; - runPath.addEventListener("animationiteration", function () { - lap += 1; - if (lap <= 3) setLap(lap, false); - }); - runPath.addEventListener("animationend", function () { - setLap(3, true); + if (reduce) { gates.classList.add("is-loop-done"); - }); + setLap(3, true); + return; + } + var len = 0; + try { len = track.getTotalLength(); } catch (e) { len = 0; } + if (!len) return; + var lapMs = 1600; + var laps = 3; + var t0 = null; + place(len, 0); + function frame(now) { + if (!t0) t0 = now; + var elapsed = now - t0; + var total = lapMs * laps; + if (elapsed >= total) { + place(len, 0); + setLap(3, true); + gates.classList.add("is-loop-done"); + loopSteps.forEach(function (li) { li.classList.remove("is-now"); }); + var six = gates.querySelector("li:last-child .n"); + if (six && token) { + var gr = gates.getBoundingClientRect(); + var r = six.getBoundingClientRect(); + token.setAttribute("cx", r.left - gr.left + r.width / 2); + token.setAttribute("cy", r.top - gr.top + r.height / 2); + } + return; + } + var lap = Math.min(laps, Math.floor(elapsed / lapMs) + 1); + setLap(lap, false); + place(len, (elapsed / lapMs) * len); + raf = window.requestAnimationFrame(frame); + } + raf = window.requestAnimationFrame(frame); } if (gates) { @@ -161,12 +231,7 @@ if (reduce) { steps.forEach(function (li) { li.classList.add("is-on"); }); blocks.forEach(function (b) { b.classList.add("is-on"); }); - if (gates) { - ensureLoopSvg(); - layoutLoop(); - gates.classList.add("is-loop"); - gates.classList.add("is-loop-done"); - } + startLoop(); paintRail(); return; } @@ -189,6 +254,17 @@ window.addEventListener("resize", function () { paintRail(); - layoutLoop(); + if (svg) { + layoutLoop(); + if (!raf && gates.classList.contains("is-loop-done")) { + var six = gates.querySelector("li:last-child .n"); + if (six && token) { + var gr = gates.getBoundingClientRect(); + var r = six.getBoundingClientRect(); + token.setAttribute("cx", r.left - gr.left + r.width / 2); + token.setAttribute("cy", r.top - gr.top + r.height / 2); + } + } + } }); })();