141 lines
4.5 KiB
JavaScript
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();
|
|
})();
|