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; }
}
+2 -2
View File
@@ -9,10 +9,10 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600&display=swap" rel="stylesheet">
<link rel="icon" href="{{ '/static/favicon.svg'|url }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=14">
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=15">
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
<script src="{{ '/static/activity.js'|url }}?v=1" defer></script>
<script src="{{ '/static/hero-circuit.js'|url }}?v=1" defer></script>
<script src="{{ '/static/hero-circuit.js'|url }}?v=2" defer></script>
<script src="{{ '/static/narration.js'|url }}?v=1" defer></script>
</head>
<body>
+30 -10
View File
@@ -6,7 +6,7 @@
<div class="circuit-brand-block">
<p class="circuit-brand-kicker">{{ this.eyebrow }}</p>
<h1>{{ this.summary }}</h1>
<h1>{{ this.summary|replace(' Signals', '<br>Signals')|safe }}</h1>
<div class="circuit-dek">{{ this.body }}</div>
<p class="circuit-status"><span aria-hidden="true"></span> network active / routes resolving</p>
</div>
@@ -22,6 +22,15 @@
<path d="M250 670 H390 V690 H520"/><path d="M535 680 H770 V700"/>
<path d="M805 685 H930 V665 H1035"/><path d="M1035 690 H1165"/>
</g>
<g class="circuit-ambient-packets">
<path d="M35 95 H185 V125 H410"/><path class="circuit-live-alt" d="M55 150 H155 V185 H300"/>
<path class="circuit-live-gold" d="M300 45 V115 H455"/><path class="circuit-live-alt" d="M465 65 H610 V100 H690"/>
<path d="M705 55 H835 V90 H930"/><path class="circuit-live-gold" d="M900 115 H1050 V80 H1160"/>
<path d="M1010 230 H1140 V335 H1085"/><path class="circuit-live-alt" d="M940 430 H1095 V465 H1165"/>
<path d="M45 455 H170 V475 H250"/><path class="circuit-live-gold" d="M40 585 H130 V555 H205"/>
<path class="circuit-live-alt" d="M250 670 H390 V690 H520"/><path d="M535 680 H770 V700"/>
<path class="circuit-live-gold" d="M805 685 H930 V665 H1035"/><path d="M1035 690 H1165"/>
</g>
<g class="circuit-ghost">
<path d="M70 350 H510 V180 H720 V140 H805"/>
<path d="M180 390 H565 V320 H865"/>
@@ -33,6 +42,17 @@
<path d="M790 605 H885 V590 H930"/>
<path d="M970 540 H1030 V515 H1060"/>
</g>
<g class="circuit-base">
<path d="M70 350 H510 V180 H720 V140 H805"/>
<path d="M180 390 H565 V320 H865"/>
<path d="M95 425 H470 V470 H720"/>
<path d="M90 505 H260 V530 H345"/>
<path d="M190 560 H350 V585 H430"/>
<path d="M430 610 H515 V630 H585"/>
<path d="M610 650 H700 V655 H755"/>
<path d="M790 605 H885 V590 H930"/>
<path d="M970 540 H1030 V515 H1060"/>
</g>
<g class="circuit-traces">
<path d="M45 200 H260 V255 H390"/><path d="M110 240 H185 V300 H410"/>
<path d="M975 180 H1125 V285 H1080"/><path d="M955 375 H1110 V410 H1160"/>
@@ -62,15 +82,15 @@
</div>
<nav class="circuit-routes" aria-label="Signal routes">
<a class="circuit-node circuit-node-main" data-route-node="projects" href="{{ '/projects'|url }}"><span>Projects</span><i aria-hidden="true">_</i></a>
<a class="circuit-node circuit-node-main" data-route-node="articles" href="{{ '/articles'|url }}"><span>Articles</span><i aria-hidden="true">_</i></a>
<a class="circuit-node circuit-node-main" data-route-node="blog" href="{{ '/blog'|url }}"><span>Blog</span><i aria-hidden="true">_</i></a>
<a class="circuit-node circuit-node-sub" data-route-node="github" href="https://github.com/Labyricorn" rel="me">GitHub</a>
<a class="circuit-node circuit-node-sub" data-route-node="gitea" href="https://git.labyricorn.com/">Gitea</a>
<a class="circuit-node circuit-node-sub" data-route-node="linkedin" href="https://www.linkedin.com/in/christopher-chambers-694b7a86/">LinkedIn</a>
<a class="circuit-node circuit-node-sub" data-route-node="twitch" href="https://www.twitch.tv/labyricorn">Twitch</a>
<a class="circuit-node circuit-node-sub" data-route-node="youtube" href="https://www.youtube.com/@labyricorn">YouTube</a>
<a class="circuit-node circuit-node-sub" data-route-node="kofi" href="https://ko-fi.com/labyricorn">Ko-fi</a>
<a class="circuit-node circuit-node-main" data-route-node="projects" data-route-text="PROJECTS" href="{{ '/projects'|url }}" aria-label="Projects"><span data-route-output>Projects</span><i aria-hidden="true">_</i></a>
<a class="circuit-node circuit-node-main" data-route-node="articles" data-route-text="ARTICLES" href="{{ '/articles'|url }}" aria-label="Articles"><span data-route-output>Articles</span><i aria-hidden="true">_</i></a>
<a class="circuit-node circuit-node-main" data-route-node="blog" data-route-text="BLOG" href="{{ '/blog'|url }}" aria-label="Blog"><span data-route-output>Blog</span><i aria-hidden="true">_</i></a>
<a class="circuit-node circuit-node-sub" data-route-node="github" data-route-text="GITHUB" href="https://github.com/Labyricorn" aria-label="GitHub" rel="me"><span data-route-output>GitHub</span></a>
<a class="circuit-node circuit-node-sub" data-route-node="gitea" data-route-text="GITEA" href="https://git.labyricorn.com/" aria-label="Gitea"><span data-route-output>Gitea</span></a>
<a class="circuit-node circuit-node-sub" data-route-node="linkedin" data-route-text="LINKEDIN" href="https://www.linkedin.com/in/christopher-chambers-694b7a86/" aria-label="LinkedIn"><span data-route-output>LinkedIn</span></a>
<a class="circuit-node circuit-node-sub" data-route-node="twitch" data-route-text="TWITCH" href="https://www.twitch.tv/labyricorn" aria-label="Twitch"><span data-route-output>Twitch</span></a>
<a class="circuit-node circuit-node-sub" data-route-node="youtube" data-route-text="YOUTUBE" href="https://www.youtube.com/@labyricorn" aria-label="YouTube"><span data-route-output>YouTube</span></a>
<a class="circuit-node circuit-node-sub" data-route-node="kofi" data-route-text="KO-FI" href="https://ko-fi.com/labyricorn" aria-label="Ko-fi"><span data-route-output>Ko-fi</span></a>
</nav>
<div class="circuit-legend" aria-hidden="true"><span>◆ live packet</span><span>— Labyricorn bus</span></div>