diff --git a/docs/site/css/style.css b/docs/site/css/style.css
index 453c8eb..20be272 100644
--- a/docs/site/css/style.css
+++ b/docs/site/css/style.css
@@ -595,3 +595,57 @@ code { font-size: 0.875em; }
.js .gates > li.is-on .kill { animation: none; }
}
+
+.loop-svg {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ overflow: visible;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.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;
+}
+
+.js .gates.is-loop .loop-run {
+ animation: loop-travel 1.7s linear 3 forwards;
+}
+
+@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;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .js .gates.is-loop .loop-run {
+ animation: none;
+ stroke-dasharray: none;
+ opacity: 0.7;
+ }
+}
diff --git a/docs/site/js/main.js b/docs/site/js/main.js
index fda78f8..d35ec47 100644
--- a/docs/site/js/main.js
+++ b/docs/site/js/main.js
@@ -1,4 +1,4 @@
-/* Copy for install. Scroll draws the protocol. No typing. No replay. */
+/* Copy for install. Scroll draws the protocol. The 5→2 path runs as a loop, cap 3. */
(function () {
"use strict";
@@ -51,6 +51,13 @@
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 ink = null;
+ var svg = null;
+ var runPath = null;
+ var trackPath = null;
+ var loopStarted = false;
+ var reenter = gates ? gates.querySelector(".reenter") : null;
+ var reenterHome = reenter ? reenter.innerHTML : "";
function paintRail() {
if (!gates || !ink) return;
@@ -66,7 +73,74 @@
ink.style.height = Math.max(0, end - top) + "px";
}
- var ink = null;
+ function loopPathD() {
+ var a = gates.querySelector(".loop-a .n");
+ var z = gates.querySelector(".loop-z .n");
+ if (!a || !z) return "";
+ var gr = gates.getBoundingClientRect();
+ var ar = a.getBoundingClientRect();
+ var zr = z.getBoundingClientRect();
+ 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";
+ }
+
+ 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() {
+ 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);
+ gates.appendChild(svg);
+ }
+
+ function setLap(n, done) {
+ if (!reenter) return;
+ if (done) {
+ reenter.innerHTML = 'loop 3 / 3 · then 6';
+ return;
+ }
+ reenter.innerHTML = 'loop ' + n + " / 3 · 5 → 2";
+ }
+
+ function startLoop() {
+ if (!gates || loopStarted || reduce) return;
+ loopStarted = true;
+ ensureLoopSvg();
+ 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);
+ gates.classList.add("is-loop-done");
+ });
+ }
+
if (gates) {
ink = document.createElement("span");
ink.className = "rail-ink";
@@ -74,13 +148,12 @@
gates.appendChild(ink);
}
- function onStep(li, vis) {
- if (vis) li.classList.add("is-on");
+ function onStep() {
if (gates) {
var loopOn = steps.some(function (s) {
return s.classList.contains("loop-z") && s.classList.contains("is-on");
});
- gates.classList.toggle("is-loop", loopOn);
+ if (loopOn) startLoop();
}
paintRail();
}
@@ -88,7 +161,12 @@
if (reduce) {
steps.forEach(function (li) { li.classList.add("is-on"); });
blocks.forEach(function (b) { b.classList.add("is-on"); });
- if (gates) gates.classList.add("is-loop");
+ if (gates) {
+ ensureLoopSvg();
+ layoutLoop();
+ gates.classList.add("is-loop");
+ gates.classList.add("is-loop-done");
+ }
paintRail();
return;
}
@@ -98,7 +176,7 @@
entries.forEach(function (e) {
if (!e.isIntersecting) return;
e.target.classList.add("is-on");
- if (e.target.parentElement === gates) onStep(e.target, true);
+ if (e.target.parentElement === gates) onStep();
});
}, { threshold: 0.28, rootMargin: "0px 0px -12% 0px" });
steps.forEach(function (li) { io.observe(li); });
@@ -106,8 +184,11 @@
} else {
steps.forEach(function (li) { li.classList.add("is-on"); });
blocks.forEach(function (b) { b.classList.add("is-on"); });
- if (gates) gates.classList.add("is-loop");
+ startLoop();
}
- window.addEventListener("resize", paintRail);
+ window.addEventListener("resize", function () {
+ paintRail();
+ layoutLoop();
+ });
})();