Match hero circuit animation prototype
Deploy production / deploy (push) Successful in 21s

This commit is contained in:
2026-08-22 11:49:09 -07:00
parent 1b5d05b794
commit feff0577ad
4 changed files with 244 additions and 61 deletions
+118 -26
View File
@@ -4,45 +4,137 @@
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const sequence = [
["projects", 180], ["articles", 320], ["blog", 460],
["github", 820], ["gitea", 930], ["linkedin", 1040],
["twitch", 1150], ["youtube", 1260], ["kofi", 1370],
{ name: "projects", travel: 620, character: 62 },
{ name: "articles", travel: 600, character: 58 },
{ name: "blog", travel: 560, character: 72 },
{ name: "github", travel: 380, character: 46 },
{ name: "gitea", travel: 360, character: 48 },
{ name: "linkedin", travel: 360, character: 42 },
{ name: "twitch", travel: 340, character: 46 },
{ name: "youtube", travel: 360, character: 42 },
{ name: "kofi", travel: 320, character: 50 },
];
let run = 0;
let timers = [];
const clearTimers = () => {
timers.forEach(window.clearTimeout);
timers = [];
let runToken = 0;
const sleep = (milliseconds) => new Promise((resolve) => window.setTimeout(resolve, milliseconds));
const reset = () => {
hero.classList.remove("is-glitching");
hero.querySelectorAll(".circuit-node").forEach((node) => {
node.classList.remove("is-revealing", "is-ready");
node.querySelector("[data-route-output]").textContent = "";
const cursor = node.querySelector("i");
if (cursor) cursor.hidden = false;
});
hero.querySelectorAll(".circuit-live path").forEach((path) => {
path.style.opacity = "0";
path.style.strokeDashoffset = "0";
});
};
const revealAll = () => {
hero.querySelectorAll(".circuit-node").forEach((node) => node.classList.add("is-ready"));
hero.querySelectorAll(".circuit-live path").forEach((path) => path.classList.add("is-live"));
hero.querySelectorAll(".circuit-node").forEach((node) => {
node.querySelector("[data-route-output]").textContent = node.dataset.routeText;
const cursor = node.querySelector("i");
if (cursor) cursor.hidden = true;
node.classList.add("is-revealing", "is-ready");
});
hero.querySelectorAll(".circuit-live path").forEach((path) => {
path.style.opacity = ".34";
});
};
const start = () => {
run += 1;
const token = run;
clearTimers();
hero.classList.remove("is-glitching");
hero.querySelectorAll(".circuit-node").forEach((node) => node.classList.remove("is-ready"));
hero.querySelectorAll(".circuit-live path").forEach((path) => path.classList.remove("is-live"));
const animatePath = (path, duration, token) => {
const length = path.getTotalLength();
path.style.strokeDasharray = `30 ${Math.max(40, length)}`;
path.style.strokeDashoffset = "30";
path.style.opacity = "1";
return new Promise((resolve) => {
const started = performance.now();
const frame = (now) => {
if (token !== runToken) {
resolve();
return;
}
const progress = Math.min(1, (now - started) / duration);
const eased = 1 - Math.pow(1 - progress, 3);
path.style.strokeDashoffset = String(30 - (length + 55) * eased);
if (progress < 1) {
requestAnimationFrame(frame);
} else {
path.style.opacity = ".34";
resolve();
}
};
requestAnimationFrame(frame);
});
};
const typeRoute = async (node, characterDelay, token) => {
const output = node.querySelector("[data-route-output]");
node.classList.add("is-revealing");
for (const character of node.dataset.routeText) {
if (token !== runToken) return;
output.textContent += character;
await sleep(characterDelay);
}
const cursor = node.querySelector("i");
if (cursor) cursor.hidden = true;
node.classList.add("is-ready");
};
const animateAmbientPackets = () => {
if (reducedMotion) return;
hero.querySelectorAll(".circuit-ambient-packets path").forEach((packet, index) => {
const length = packet.getTotalLength();
packet.style.strokeDasharray = `16 ${Math.max(30, length)}`;
packet.animate(
[
{ strokeDashoffset: "20", opacity: .06 },
{ opacity: .2, offset: .18 },
{ opacity: .2, offset: .82 },
{ strokeDashoffset: String(-(length + 25)), opacity: .05 },
],
{ duration: 3300 + (index % 5) * 620, delay: index * 210, iterations: Infinity, easing: "linear" },
);
});
};
const runSequence = async () => {
runToken += 1;
const token = runToken;
if (reducedMotion) {
reset();
revealAll();
return;
}
sequence.forEach(([name, delay]) => {
timers.push(window.setTimeout(() => {
if (token !== run) return;
hero.querySelector(`[data-route-node="${name}"]`)?.classList.add("is-ready");
hero.querySelector(`#route-${name}`)?.classList.add("is-live");
}, delay));
});
while (token === runToken) {
reset();
await sleep(280);
for (const item of sequence) {
if (token !== runToken) return;
const node = hero.querySelector(`[data-route-node="${item.name}"]`);
const path = hero.querySelector(`#route-${item.name}`);
await animatePath(path, item.travel, token);
await typeRoute(node, item.character, token);
await sleep(55);
}
await sleep(3000);
if (token !== runToken) return;
hero.classList.add("is-glitching");
await sleep(430);
if (token !== runToken) return;
reset();
await sleep(180);
}
};
hero.querySelector("[data-circuit-replay]")?.addEventListener("click", start);
start();
hero.querySelector("[data-circuit-replay]")?.addEventListener("click", runSequence);
animateAmbientPackets();
runSequence();
})();