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();
})();
+94 -23
View File
@@ -96,7 +96,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
z-index: 10;
top: clamp(72px, 11vh, 104px);
left: clamp(28px, 5vw, 72px);
width: min(610px, 64vw);
width: min(640px, 64vw);
}
.circuit-brand-kicker {
margin: 0 0 12px;
@@ -108,14 +108,20 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.circuit-brand-block h1 {
max-width: none;
margin: 0;
font: 700 clamp(45px, 7vw, 98px)/.86 var(--serif);
font: 700 clamp(45px, 7vw, 98px)/.86 Georgia, "Times New Roman", serif;
font-style: normal;
letter-spacing: -.055em;
text-shadow: 0 0 24px rgba(197,160,89,.05);
}
.circuit-dek {
position: relative;
z-index: 2;
display: inline-block;
max-width: 430px;
margin-top: 22px;
padding-right: 8px;
background: rgba(7,8,8,.94);
box-shadow: 10px 0 16px rgba(7,8,8,.98);
color: var(--muted);
font-size: clamp(15px, 1.6vw, 17px);
}
@@ -147,25 +153,32 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.circuit-ambient path { stroke: #1d2221; stroke-width: 1; opacity: .72; }
.circuit-ambient path:nth-child(3n+1) { stroke: rgba(101,247,255,.22); }
.circuit-ambient path:nth-child(3n+2) { stroke: rgba(255,79,216,.16); }
.circuit-ghost path { stroke: #020303; stroke-width: 8; opacity: .9; }
.circuit-ambient-packets path {
stroke: rgba(101,247,255,.38);
stroke-width: 1;
stroke-linecap: square;
opacity: .18;
filter: drop-shadow(0 0 2px rgba(101,247,255,.22));
}
.circuit-ambient-packets path.circuit-live-alt { stroke: rgba(255,79,216,.3); filter: drop-shadow(0 0 2px rgba(255,79,216,.18)); }
.circuit-ambient-packets path.circuit-live-gold { stroke: rgba(197,160,89,.3); filter: drop-shadow(0 0 2px rgba(197,160,89,.16)); }
.circuit-ghost path { stroke: rgba(197,160,89,.08); stroke-width: 4; filter: blur(4px); }
.circuit-base path,
.circuit-traces path,
.circuit-live path { stroke: #303837; stroke-width: 2; }
.circuit-live path { stroke: #282b2a; stroke-width: 1.35; }
.circuit-live path {
opacity: .34;
opacity: 0;
stroke: var(--circuit-cyan);
stroke-dasharray: 22 52;
stroke-width: 1.55;
stroke-linecap: square;
stroke-dasharray: 30 9999;
filter: drop-shadow(0 0 5px rgba(101,247,255,.36));
transition: opacity .35s ease;
}
.circuit-live path.circuit-live-alt {
stroke: var(--circuit-pink);
filter: drop-shadow(0 0 5px rgba(255,79,216,.3));
}
.js .circuit-live path { opacity: 0; }
.js .circuit-live path.is-live {
opacity: .82;
animation: circuit-packet 1.65s linear infinite;
}
.circuit-vias circle {
fill: #090b0b;
stroke: var(--circuit-cyan);
@@ -186,7 +199,15 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
transition: opacity .28s ease, transform .28s ease, color .2s ease, filter .2s ease;
}
.js .circuit-node { opacity: .22; transform: translateY(3px); pointer-events: none; }
.js .circuit-node.is-revealing,
.js .circuit-node.is-ready { opacity: 1; transform: none; pointer-events: auto; }
.circuit-node > span,
.circuit-node > i {
position: relative;
z-index: 2;
background: rgba(7,8,8,.95);
box-shadow: 0 0 0 5px rgba(7,8,8,.95);
}
.circuit-node::before {
content: "";
position: absolute;
@@ -208,12 +229,12 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.circuit-node[data-route-node="projects"] { left: 67%; top: 27%; }
.circuit-node[data-route-node="articles"] { left: 72%; top: 47%; }
.circuit-node[data-route-node="blog"] { left: 61%; top: 68%; }
.circuit-node[data-route-node="github"] { left: 27%; top: 73%; }
.circuit-node[data-route-node="gitea"] { left: 36%; top: 81%; }
.circuit-node[data-route-node="linkedin"] { left: 49%; top: 87%; }
.circuit-node[data-route-node="twitch"] { left: 63%; top: 91%; }
.circuit-node[data-route-node="youtube"] { left: 77%; top: 83%; }
.circuit-node[data-route-node="kofi"] { left: 88%; top: 72%; }
.circuit-node[data-route-node="github"] { left: 22%; top: 62%; }
.circuit-node[data-route-node="gitea"] { left: 28%; top: 72%; }
.circuit-node[data-route-node="linkedin"] { left: 42%; top: 82%; }
.circuit-node[data-route-node="twitch"] { left: 58%; top: 86%; }
.circuit-node[data-route-node="youtube"] { left: 74%; top: 80%; }
.circuit-node[data-route-node="kofi"] { left: 84%; top: 72%; }
.circuit-legend {
position: absolute;
z-index: 12;
@@ -246,9 +267,45 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.circuit-replay:hover,
.circuit-replay:focus-visible { border-color: var(--circuit-cyan); color: var(--circuit-cyan); }
@keyframes circuit-packet { to { stroke-dashoffset: -74; } }
@keyframes circuit-cursor { 50% { opacity: 0; } }
@keyframes circuit-pulse { 50% { opacity: .35; transform: scale(.8); } }
@keyframes circuit-hero-glitch {
0% { transform: translate(0); filter: none; }
18% { transform: translate(3px,-1px); filter: contrast(1.25) saturate(1.3); }
34% { transform: translate(-5px,1px); }
52% { transform: translate(2px,0); clip-path: inset(0 0 38% 0); }
70% { transform: translate(-2px,1px); clip-path: inset(35% 0 0); }
86% { transform: translate(4px,-1px); filter: contrast(1.4) saturate(1.5); }
100% { transform: translate(0); filter: brightness(.18); }
}
@keyframes circuit-node-glitch {
0%, 100% { transform: translate(0); }
25% { transform: translate(7px,-1px); }
50% { transform: translate(-6px,1px); }
75% { transform: translate(3px,0); }
}
@keyframes circuit-stage-glitch {
0%, 100% { transform: translate(0); }
30% { transform: translate(-6px,0); }
55% { transform: translate(8px,1px); }
80% { transform: translate(-3px,-1px); }
}
@keyframes circuit-scan-tear {
0% { transform: translateY(0); }
33% { transform: translateY(-8px); }
66% { transform: translateY(11px); }
100% { transform: translateY(0); }
}
.circuit-hero.is-glitching { animation: circuit-hero-glitch .42s steps(2,end) both; }
.circuit-hero.is-glitching .circuit-node {
animation: circuit-node-glitch .42s steps(2,end) both;
text-shadow: 4px 0 rgba(101,247,255,.8), -4px 0 rgba(255,79,216,.65);
}
.circuit-hero.is-glitching .circuit-stage {
animation: circuit-stage-glitch .42s steps(2,end) both;
filter: drop-shadow(5px 0 rgba(101,247,255,.45)) drop-shadow(-5px 0 rgba(255,79,216,.35));
}
.circuit-hero.is-glitching::after { opacity: .85; animation: circuit-scan-tear .42s steps(3,end) both; }
.activity-section { min-height: 370px; padding: 48px 0 72px; }
.homepage-discovery-grid { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(290px, 1fr); gap: 36px; align-items: start; }
@@ -568,6 +625,21 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.footer-inner { min-height: 105px; display: flex; justify-content: space-between; align-items: center; gap: 24px; }
.site-footer nav { display: flex; gap: 22px; }
@media (max-width: 860px) and (min-width: 721px) {
.circuit-hero { height: 720px; min-height: 720px; }
.circuit-brand-block { width: 72vw; }
.circuit-node[data-route-node="projects"] { left: 58%; top: 36%; }
.circuit-node[data-route-node="articles"] { left: 61%; top: 49%; }
.circuit-node[data-route-node="blog"] { left: 55%; top: 61%; }
.circuit-node[data-route-node="github"] { left: 20%; top: 72%; }
.circuit-node[data-route-node="gitea"] { left: 38%; top: 76%; }
.circuit-node[data-route-node="linkedin"] { left: 57%; top: 73%; }
.circuit-node[data-route-node="twitch"] { left: 20%; top: 83%; }
.circuit-node[data-route-node="youtube"] { left: 42%; top: 87%; }
.circuit-node[data-route-node="kofi"] { left: 70%; top: 84%; }
.circuit-legend { display: none; }
}
@media (max-width: 720px) {
.shell { width: min(100% - 28px, 1100px); }
.narrow { margin-inline: auto; }
@@ -588,11 +660,11 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.circuit-node[data-route-node="articles"] { left: 38%; top: 53%; }
.circuit-node[data-route-node="blog"] { left: 28%; top: 64%; }
.circuit-node[data-route-node="github"] { left: 18%; top: 74%; }
.circuit-node[data-route-node="gitea"] { left: 56%; top: 74%; }
.circuit-node[data-route-node="gitea"] { left: 53%; top: 74%; }
.circuit-node[data-route-node="linkedin"] { left: 18%; top: 81%; }
.circuit-node[data-route-node="twitch"] { left: 60%; top: 81%; }
.circuit-node[data-route-node="twitch"] { left: 56%; top: 81%; }
.circuit-node[data-route-node="youtube"] { left: 18%; top: 88%; }
.circuit-node[data-route-node="kofi"] { left: 64%; top: 88%; }
.circuit-node[data-route-node="kofi"] { left: 60%; top: 88%; }
.circuit-legend { display: none; }
.circuit-replay { left: 16px; bottom: 13px; }
h1 { font-size: 30px; }
@@ -657,6 +729,5 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
@media (prefers-reduced-motion: reduce) {
.circuit-status span,
.circuit-node-main i,
.circuit-live path.is-live { animation: none; }
.circuit-node-main i { animation: none; }
}