Compare commits

...

2 Commits

Author SHA1 Message Date
9965bd3529 update submodules 2026-04-14 03:09:20 -07:00
f0ae0b9ce1 hero bugfixes/improvements 2026-04-08 22:25:26 -07:00
4 changed files with 82 additions and 51 deletions

View File

@@ -439,8 +439,15 @@ function GlitchCountdown({ seconds }: { seconds: number }) {
export default function Hero() { export default function Hero() {
const [phase, setPhase] = useState< const [phase, setPhase] = useState<
"intro" | "full" | "retired" | "countdown" | "glitch" "intro" | "full" | "retired" | "countdown" | "glitch" | "void"
>("intro"); >(() => {
if (import.meta.env.DEV && typeof window !== "undefined") {
const p = new URLSearchParams(window.location.search);
if (p.has("debug-glitch")) return "glitch";
if (p.has("debug-countdown")) return "countdown";
}
return "intro";
});
const [fading, setFading] = useState(false); const [fading, setFading] = useState(false);
const [cycle, setCycle] = useState(0); const [cycle, setCycle] = useState(0);
const [countdown, setCountdown] = useState(150); const [countdown, setCountdown] = useState(150);
@@ -482,49 +489,81 @@ export default function Hero() {
}, [phase]); }, [phase]);
// Glitch → transition into void // Glitch → transition into void
const glitchRef = useRef<HTMLDivElement>(null); // Apply animation directly to each visible element (works on both desktop + mobile)
// On mobile, filter/transform on <body> doesn't reach fixed-position children,
// so we target the elements themselves
useEffect(() => { useEffect(() => {
if (phase !== "glitch") return; if (phase !== "glitch") return;
// Apply glitch to all direct children of the layout wrapper
const wrapper = glitchRef.current?.closest("main")?.parentElement || document.body;
const style = document.createElement("style"); const style = document.createElement("style");
style.textContent = ` style.textContent = `
.hero-glitch-child { .hero-glitch-shake {
animation: hero-glitch 3s ease-in forwards; animation: hero-glitch-shake 3s ease-in forwards !important;
} }
@keyframes hero-glitch { @keyframes hero-glitch-shake {
0% { filter: none; transform: none; opacity: 1; } 0% { transform: none; }
5% { filter: hue-rotate(90deg) saturate(3); transform: skewX(2deg); } 5% { transform: skewX(2deg); }
10% { filter: invert(1); transform: skewX(-3deg) translateX(5px); } 10% { transform: skewX(-3deg) translateX(5px); }
15% { filter: hue-rotate(180deg) brightness(1.5); transform: scale(1.02); } 15% { transform: scale(1.02); }
20% { filter: saturate(5) contrast(2); transform: skewX(1deg) translateY(-2px); } 20% { transform: skewX(1deg) translateY(-2px); }
25% { filter: invert(1) hue-rotate(270deg); transform: skewX(-2deg); } 25% { transform: skewX(-2deg); }
30% { filter: brightness(2) saturate(0); transform: scale(0.98); } 30% { transform: scale(0.98); }
40% { filter: hue-rotate(45deg) contrast(3); transform: translateX(-3px); } 40% { transform: translateX(-3px); }
50% { filter: invert(1) brightness(0.5); transform: skewX(4deg) skewY(1deg); } 50% { transform: skewX(4deg) skewY(1deg); }
60% { filter: saturate(0) brightness(1.8); transform: scale(1.01); } 60% { transform: scale(1.01); }
70% { filter: hue-rotate(180deg) brightness(0.3); transform: none; } 70% { transform: none; }
80% { filter: contrast(5) saturate(0); transform: skewX(-1deg); } 80% { transform: skewX(-1deg); }
90% { filter: brightness(0.1); transform: scale(0.99); opacity: 0.1; } 90% { transform: none; }
100% { filter: brightness(0); transform: none; opacity: 0; } 100% { transform: none; }
}
.hero-glitch-filter {
animation: hero-glitch-filter 3s ease-in forwards !important;
position: fixed !important;
inset: 0 !important;
z-index: 99999 !important;
pointer-events: none !important;
}
@keyframes hero-glitch-filter {
0% { backdrop-filter: none; background: transparent; }
5% { backdrop-filter: hue-rotate(90deg) saturate(3); }
10% { backdrop-filter: invert(1); }
15% { backdrop-filter: hue-rotate(180deg) brightness(1.5); }
20% { backdrop-filter: saturate(5) contrast(2); }
25% { backdrop-filter: invert(1) hue-rotate(270deg); }
30% { backdrop-filter: brightness(2) saturate(0); }
40% { backdrop-filter: hue-rotate(45deg) contrast(3); }
50% { backdrop-filter: invert(1) brightness(0.5); }
60% { backdrop-filter: saturate(0) brightness(1.8); }
70% { backdrop-filter: hue-rotate(180deg) brightness(0.3); }
80% { backdrop-filter: contrast(5) saturate(0); }
90% { backdrop-filter: brightness(0); background: #000; }
100% { backdrop-filter: brightness(0); background: #000; }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
const children = Array.from(wrapper.children) as HTMLElement[]; // Overlay for backdrop-filter (color distortion — works on all platforms)
children.forEach(child => child.classList.add("hero-glitch-child")); const overlay = document.createElement("div");
overlay.className = "hero-glitch-filter";
document.body.appendChild(overlay);
// Shake transforms on all layout elements
const targets = document.querySelectorAll<HTMLElement>(
"header, main, footer, nav"
);
targets.forEach(el => el.classList.add("hero-glitch-shake"));
// After glitch animation, transition to void phase
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
children.forEach(child => child.classList.remove("hero-glitch-child")); targets.forEach(el => el.classList.remove("hero-glitch-shake"));
overlay.remove();
style.remove(); style.remove();
setPhase("void"); setPhase("void");
}, 3000); }, 3000);
return () => { return () => {
clearTimeout(timeout); clearTimeout(timeout);
children.forEach(child => child.classList.remove("hero-glitch-child")); targets.forEach(el => el.classList.remove("hero-glitch-shake"));
overlay.remove();
style.remove(); style.remove();
}; };
}, [phase]); }, [phase]);
@@ -587,7 +626,7 @@ export default function Hero() {
} }
if (phase === "glitch") { if (phase === "glitch") {
return <div ref={glitchRef} className="min-h-screen" />; return <div className="min-h-screen" />;
} }
if (phase === "countdown") { if (phase === "countdown") {

View File

@@ -151,28 +151,20 @@ export default function VoidExperience({ token }: VoidExperienceProps) {
const [visitCount, setVisitCount] = useState<number | null>(null); const [visitCount, setVisitCount] = useState<number | null>(null);
const [dissolving, setDissolving] = useState(false); const [dissolving, setDissolving] = useState(false);
// Inject CSS + hide cursor + force fullscreen feel on mobile // Inject CSS + hide cursor + hide layout chrome underneath
useEffect(() => { useEffect(() => {
const style = document.createElement("style"); const style = document.createElement("style");
style.textContent = GLITCH_CSS; style.textContent = GLITCH_CSS;
document.head.appendChild(style); document.head.appendChild(style);
document.body.style.cursor = "none"; document.body.style.cursor = "none";
// Push mobile tab bar off-screen by making the page taller than viewport
const meta = document.querySelector('meta[name="viewport"]');
const origContent = meta?.getAttribute("content") || "";
meta?.setAttribute("content", "width=device-width, initial-scale=1, interactive-widget=resizes-content");
document.documentElement.style.overflow = "hidden"; document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
// Scroll down slightly to trigger mobile browsers hiding the tab bar
window.scrollTo(0, 1);
return () => { return () => {
style.remove(); style.remove();
document.body.style.cursor = ""; document.body.style.cursor = "";
document.documentElement.style.overflow = ""; document.documentElement.style.overflow = "";
document.body.style.overflow = ""; document.body.style.overflow = "";
if (meta) meta.setAttribute("content", origContent);
}; };
}, []); }, []);
@@ -212,7 +204,7 @@ export default function VoidExperience({ token }: VoidExperienceProps) {
const corruption = getCorruption(activeSegment); const corruption = getCorruption(activeSegment);
return ( return (
<div className="fixed inset-0 bg-black" style={{ height: "100dvh" }}> <div className="fixed inset-0 bg-black z-[9999]" style={{ height: "100dvh" }}>
{/* 3D Canvas — full glitch (transforms + filters) */} {/* 3D Canvas — full glitch (transforms + filters) */}
<div className={`fixed inset-0 z-[10] ${getCanvasGlitch(activeSegment, dissolving)}`}> <div className={`fixed inset-0 z-[10] ${getCanvasGlitch(activeSegment, dissolving)}`}>
<Canvas <Canvas
@@ -225,17 +217,16 @@ export default function VoidExperience({ token }: VoidExperienceProps) {
</Canvas> </Canvas>
</div> </div>
{/* Typewriter — filter-only glitch (no transform shift) */} {/* Typewriter — glitch class applied to inner text, not the fixed container */}
{visitCount !== null && ( {visitCount !== null && (
<div className={getTextGlitch(activeSegment, dissolving)}> <VoidTypewriter
<VoidTypewriter startSegment={0}
startSegment={0} onPhaseComplete={handlePhaseComplete}
onPhaseComplete={handlePhaseComplete} onSegmentChange={handleSegmentChange}
onSegmentChange={handleSegmentChange} visitCount={visitCount}
visitCount={visitCount} corruption={corruption}
corruption={corruption} glitchClass={getTextGlitch(activeSegment, dissolving)}
/> />
</div>
)} )}
</div> </div>
); );

View File

@@ -11,6 +11,7 @@ interface VoidTypewriterProps {
onSegmentChange: (index: number) => void; onSegmentChange: (index: number) => void;
visitCount: number; visitCount: number;
corruption: number; corruption: number;
glitchClass: string;
} }
function getTextNodes(node: Node): Text[] { function getTextNodes(node: Node): Text[] {
@@ -25,7 +26,7 @@ function getTextNodes(node: Node): Text[] {
return nodes; return nodes;
} }
export default function VoidTypewriter({ startSegment, onPhaseComplete, onSegmentChange, visitCount, corruption }: VoidTypewriterProps) { export default function VoidTypewriter({ startSegment, onPhaseComplete, onSegmentChange, visitCount, corruption, glitchClass }: VoidTypewriterProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const corruptionRef = useRef(corruption); const corruptionRef = useRef(corruption);
corruptionRef.current = corruption; corruptionRef.current = corruption;
@@ -85,7 +86,7 @@ export default function VoidTypewriter({ startSegment, onPhaseComplete, onSegmen
<div className="fixed inset-0 z-[50] flex justify-center items-center pointer-events-none"> <div className="fixed inset-0 z-[50] flex justify-center items-center pointer-events-none">
<div <div
ref={containerRef} ref={containerRef}
className="text-xl md:text-3xl font-bold text-center max-w-[85vw] md:max-w-[70vw] break-words text-white leading-relaxed" className={`text-xl md:text-3xl font-bold text-center max-w-[85vw] md:max-w-[70vw] break-words text-white leading-relaxed ${glitchClass}`}
> >
<Typewriter <Typewriter
key={`void-${startSegment}-${visitCount}`} key={`void-${startSegment}-${visitCount}`}