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(); + }); })();