diff --git a/assets/static/hero-circuit.js b/assets/static/hero-circuit.js index 62059ea..54cd90c 100644 --- a/assets/static/hero-circuit.js +++ b/assets/static/hero-circuit.js @@ -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(); })(); diff --git a/assets/static/style.css b/assets/static/style.css index d858b69..a84e354 100644 --- a/assets/static/style.css +++ b/assets/static/style.css @@ -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; } } diff --git a/templates/base.html b/templates/base.html index 8cfcc4f..117aeff 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,10 +9,10 @@ - + - +
diff --git a/templates/home.html b/templates/home.html index c03191f..0f6e95f 100644 --- a/templates/home.html +++ b/templates/home.html @@ -6,7 +6,7 @@{{ this.eyebrow }}
-network active / routes resolving