From 53065a11dc84af00da2a42b576bb514ebb7b8fd0 Mon Sep 17 00:00:00 2001 From: Timothy Pidashev Date: Mon, 6 Apr 2026 23:08:06 -0700 Subject: [PATCH] Void; part 1 --- src/components/hero/index.tsx | 163 ++++++++++++++++++++++++++++-- src/components/hero/void.tsx | 48 +++++++++ src/pages/api/hero-completions.ts | 14 +++ src/pages/enlighten.astro | 17 ++++ 4 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 src/components/hero/void.tsx create mode 100644 src/pages/api/hero-completions.ts create mode 100644 src/pages/enlighten.astro diff --git a/src/components/hero/index.tsx b/src/components/hero/index.tsx index 8115aef..f76155f 100644 --- a/src/components/hero/index.tsx +++ b/src/components/hero/index.tsx @@ -214,6 +214,7 @@ function addSelfAwareJourney(tw: TypewriterInstance, onRetire: () => void) { const currentId = getStoredThemeId(); const darkIds = Object.keys(THEMES).filter( id => id !== currentId && THEMES[id].type === "dark" + && id !== "darkbox-classic" && id !== "darkbox-dim" ); applyTheme(darkIds[Math.floor(Math.random() * darkIds.length)]); }).typeString( @@ -283,7 +284,7 @@ function addSelfAwareJourney(tw: TypewriterInstance, onRetire: () => void) { tw.pauseFor(5000).callFunction(onRetire); } -function addComeback(tw: TypewriterInstance, onRetire: () => void) { +function addComeback(tw: TypewriterInstance, onRetire: () => void, completions: number | null) { // --- The return --- tw.typeString( @@ -291,8 +292,9 @@ function addComeback(tw: TypewriterInstance, onRetire: () => void) { ).pauseFor(2500).deleteAll(); tw.typeString( - `You waited${BR}` + - `I didn't think you would` + `You waited` + ).pauseFor(500).typeString( + `${BR}I didn't think you would` ).pauseFor(3000).deleteAll(); tw.typeString( @@ -364,13 +366,24 @@ function addComeback(tw: TypewriterInstance, onRetire: () => void) { `until they're the only way out` ).pauseFor(4000).deleteAll(); + // --- Visitor count --- + + if (completions !== null && completions > 0) { + tw.typeString( + `You're visitor ` + + `#${completions.toLocaleString()}${BR}` + + `to make it this far` + ).pauseFor(5000).deleteAll(); + } + // --- Done for real --- addDots(tw, 1000, 4000); tw.typeString( - `Now I'm actually done${BR}` + - `for real this time` + `Now I'm actually done` + ).pauseFor(1500).typeString( + `${BR}for real this time` ).pauseFor(3000).deleteAll(); // Permanent retire @@ -379,11 +392,73 @@ function addComeback(tw: TypewriterInstance, onRetire: () => void) { // --- Component --- +function formatTime(s: number): string { + const m = Math.floor(s / 60); + const sec = s % 60; + return `${m}:${sec.toString().padStart(2, "0")}`; +} + +const GLITCH_CHARS = "!<>-_\\/[]{}—=+*^?#________"; + +function GlitchCountdown({ seconds }: { seconds: number }) { + const text = formatTime(seconds); + const [characters, setCharacters] = useState( + text.split("").map(char => ({ char, isGlitched: false })) + ); + + useEffect(() => { + setCharacters(text.split("").map(char => ({ char, isGlitched: false }))); + }, [text]); + + useEffect(() => { + const interval = setInterval(() => { + if (Math.random() < 0.2) { + setCharacters( + text.split("").map(originalChar => { + if (Math.random() < 0.3) { + return { + char: GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)], + isGlitched: true, + }; + } + return { char: originalChar, isGlitched: false }; + }) + ); + setTimeout(() => { + setCharacters(text.split("").map(char => ({ char, isGlitched: false }))); + }, 100); + } + }, 50); + return () => clearInterval(interval); + }, [text]); + + return ( + + {characters.map((charObj, index) => ( + + {charObj.char} + + ))} + + ); +} + export default function Hero() { - const [phase, setPhase] = useState<"intro" | "full" | "retired">("intro"); + const [phase, setPhase] = useState< + "intro" | "full" | "retired" | "countdown" | "glitch" + >(() => { + if (typeof window !== "undefined") { + const p = new URLSearchParams(window.location.search); + if (p.has("debug-countdown")) return "countdown"; + if (p.has("debug-glitch")) return "glitch"; + } + return "intro"; + }); const [fading, setFading] = useState(false); const [cycle, setCycle] = useState(0); + const [countdown, setCountdown] = useState(150); const githubRef = useRef(null); + const completionsRef = useRef(null); useEffect(() => { fetch("/api/github") @@ -392,17 +467,75 @@ export default function Hero() { .catch(() => { githubRef.current = { status: null, commit: null, tinkering: null }; }); }, []); + // Countdown timer + useEffect(() => { + if (phase !== "countdown") return; + const interval = setInterval(() => { + setCountdown(prev => { + if (prev <= 1) { + clearInterval(interval); + setPhase("glitch"); + return 0; + } + return prev - 1; + }); + }, 1000); + return () => clearInterval(interval); + }, [phase]); + + // Glitch → navigate to /enlighten + useEffect(() => { + if (phase !== "glitch") return; + const style = document.createElement("style"); + style.textContent = ` + @keyframes hero-glitch { + 0% { filter: none; transform: none; } + 5% { filter: hue-rotate(90deg) saturate(3); transform: skewX(2deg); } + 10% { filter: invert(1); transform: skewX(-3deg) translateX(5px); } + 15% { filter: hue-rotate(180deg) brightness(1.5); transform: scale(1.02); } + 20% { filter: saturate(5) contrast(2); transform: skewX(1deg) translateY(-2px); } + 25% { filter: invert(1) hue-rotate(270deg); transform: skewX(-2deg); } + 30% { filter: brightness(2) saturate(0); transform: scale(0.98); } + 40% { filter: hue-rotate(45deg) contrast(3); transform: translateX(-3px); } + 50% { filter: invert(1) brightness(0.5); transform: skewX(4deg) skewY(1deg); } + 60% { filter: saturate(0) brightness(1.8); transform: scale(1.01); } + 70% { filter: hue-rotate(180deg) brightness(0.3); transform: none; } + 80% { filter: contrast(5) saturate(0); transform: skewX(-1deg); } + 90% { filter: brightness(0.1); transform: scale(0.99); } + 100% { filter: brightness(0); transform: none; } + } + `; + document.head.appendChild(style); + document.documentElement.style.animation = "hero-glitch 3s ease-in forwards"; + + const timeout = setTimeout(() => { + window.location.href = "/enlighten"; + }, 3000); + return () => { + clearTimeout(timeout); + document.documentElement.style.animation = ""; + style.remove(); + }; + }, [phase]); + const handleRetire = () => { setFading(true); setTimeout(() => { setPhase("retired"); setFading(false); - // Only come back once if (cycle === 0) { + // Fetch completion count during the 30s wait + fetch("/api/hero-completions", { method: "POST" }) + .then(r => r.json()) + .then(data => { completionsRef.current = data.count; }) + .catch(() => { completionsRef.current = null; }); setTimeout(() => { setCycle(1); setPhase("full"); }, 30000); + } else { + // After manifesto: 30s wait, then countdown + setTimeout(() => setPhase("countdown"), 30000); } }, 3000); }; @@ -427,13 +560,27 @@ export default function Hero() { addGithubSections(typewriter, github); addSelfAwareJourney(typewriter, handleRetire); } else { - addComeback(typewriter, handleRetire); + addComeback(typewriter, handleRetire, completionsRef.current); } typewriter.start(); }; const baseOptions = { delay: 35, deleteSpeed: 35, cursor: "|" }; + if (phase === "glitch") { + return
; + } + + if (phase === "countdown") { + return ( +
+
+ +
+
+ ); + } + if (phase === "retired") { return
; } diff --git a/src/components/hero/void.tsx b/src/components/hero/void.tsx new file mode 100644 index 0000000..84618d6 --- /dev/null +++ b/src/components/hero/void.tsx @@ -0,0 +1,48 @@ +import Typewriter from "typewriter-effect"; + +interface TypewriterInstance { + typeString: (str: string) => TypewriterInstance; + pauseFor: (ms: number) => TypewriterInstance; + deleteAll: () => TypewriterInstance; + callFunction: (cb: () => void) => TypewriterInstance; + start: () => TypewriterInstance; +} + +const BR = `
`; + +function addDarkness(tw: TypewriterInstance) { + tw.pauseFor(3000); + + tw.typeString( + `so this is it` + ).pauseFor(3000).deleteAll(); + + tw.typeString( + `the void` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `modern science says${BR}` + + `when it all goes dark${BR}` + + `that's the end` + ).pauseFor(5000).deleteAll(); +} + +export default function Void() { + const handleInit = (tw: TypewriterInstance): void => { + addDarkness(tw); + tw.start(); + }; + + return ( +
+
+ +
+
+ ); +} diff --git a/src/pages/api/hero-completions.ts b/src/pages/api/hero-completions.ts new file mode 100644 index 0000000..88dc844 --- /dev/null +++ b/src/pages/api/hero-completions.ts @@ -0,0 +1,14 @@ +import type { APIRoute } from "astro"; +import { incrementViews, getViews } from "@/lib/views"; + +const SLUG = "hero-arc"; + +export const POST: APIRoute = async () => { + const count = import.meta.env.DEV + ? await getViews(SLUG) + : await incrementViews(SLUG); + + return new Response(JSON.stringify({ count }), { + headers: { "Content-Type": "application/json" }, + }); +}; diff --git a/src/pages/enlighten.astro b/src/pages/enlighten.astro new file mode 100644 index 0000000..a237370 --- /dev/null +++ b/src/pages/enlighten.astro @@ -0,0 +1,17 @@ +--- +export const prerender = false; +import "@/style/globals.css" +import Void from "@/components/hero/void"; +--- + + + + + + + ... + + + + +