Files
Labyricorn feff0577ad
Deploy production / deploy (push) Successful in 21s
Match hero circuit animation prototype
2026-08-22 11:49:09 -07:00

141 lines
4.5 KiB
JavaScript

(() => {
const hero = document.querySelector("[data-circuit-hero]");
if (!hero) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const sequence = [
{ 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 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.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 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;
}
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", runSequence);
animateAmbientPackets();
runSequence();
})();