All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 5s
401 lines
12 KiB
TypeScript
401 lines
12 KiB
TypeScript
import { animated, useSpring as useThreeSpring } from "@react-spring/three";
|
|
import { useDisableRaycast } from "../hooks/useDisableRaycast";
|
|
import { View, viewToHash } from "@/components/scene/consts";
|
|
import Stack from "@/util/stack";
|
|
import {
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
import {
|
|
type MeshStandardMaterial,
|
|
type BufferGeometry,
|
|
BackSide,
|
|
} from "three";
|
|
import { Billboard, Text } from "@react-three/drei";
|
|
|
|
interface ButtonsProps {
|
|
menuTraversal: Stack<View>;
|
|
setMenuTraversal: Dispatch<SetStateAction<Stack<View>>>;
|
|
pendingView: View | null;
|
|
setPendingView: Dispatch<SetStateAction<View | null>>;
|
|
goPreviousView(): void;
|
|
currentView: View;
|
|
}
|
|
|
|
const AnimatedText = animated(Text);
|
|
|
|
export const Buttons = ({
|
|
menuTraversal,
|
|
setMenuTraversal,
|
|
pendingView,
|
|
setPendingView,
|
|
goPreviousView,
|
|
currentView,
|
|
}: ButtonsProps) => {
|
|
const [hovered, setHovered] = useState<View | null>(null);
|
|
|
|
const desktopRef = useDisableRaycast(currentView !== View.MainView);
|
|
const printerRef = useDisableRaycast(currentView !== View.MainView);
|
|
const sideRef = useDisableRaycast(currentView !== View.MainView);
|
|
const cellphoneRef = useDisableRaycast(currentView !== View.DesktopView);
|
|
const pcRef = useDisableRaycast(currentView !== View.DesktopView);
|
|
|
|
const goToView = useCallback(
|
|
(view: View) => {
|
|
setMenuTraversal((prev) => {
|
|
const next = prev.clone();
|
|
next.push(view);
|
|
return next;
|
|
});
|
|
setPendingView(view);
|
|
},
|
|
[setMenuTraversal, setPendingView]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") goPreviousView();
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [goPreviousView]);
|
|
|
|
useEffect(() => {
|
|
const onPop = () => {
|
|
goPreviousView();
|
|
};
|
|
window.addEventListener("popstate", onPop);
|
|
return () => window.removeEventListener("popstate", onPop);
|
|
}, [goPreviousView]);
|
|
|
|
useEffect(() => {
|
|
if (pendingView === null) return;
|
|
window.history.pushState(
|
|
{ depth: menuTraversal.size() },
|
|
"",
|
|
viewToHash[pendingView] ? `#${viewToHash[pendingView]}` : "/"
|
|
);
|
|
setPendingView(null);
|
|
}, [menuTraversal, pendingView, setPendingView]);
|
|
|
|
useEffect(() => {
|
|
if (hovered) {
|
|
document.body.style.cursor = "pointer";
|
|
} else {
|
|
document.body.style.cursor = "auto";
|
|
}
|
|
}, [hovered]);
|
|
|
|
const mainMenuSpring = useThreeSpring<MeshStandardMaterial>({
|
|
opacity: currentView === View.MainView ? 1 : 0,
|
|
});
|
|
|
|
const desktopMenuSpring = useThreeSpring<MeshStandardMaterial>({
|
|
opacity: currentView === View.DesktopView ? 1 : 0,
|
|
});
|
|
|
|
const creditsButtonSpring = useThreeSpring<MeshStandardMaterial>({
|
|
opacity:
|
|
currentView === View.DesktopView
|
|
? hovered === View.CreditsView
|
|
? 1
|
|
: 0.25
|
|
: 0,
|
|
});
|
|
|
|
const desktopViewSpring = useThreeSpring<BufferGeometry>({
|
|
scale: hovered === View.DesktopView ? 1.25 : 1,
|
|
});
|
|
const resumeViewSpring = useThreeSpring<BufferGeometry>({
|
|
scale: hovered === View.ResumeView ? 1.25 : 1,
|
|
});
|
|
const cellphoneViewSpring = useThreeSpring<BufferGeometry>({
|
|
scale: hovered === View.CellphoneView ? 1.25 : 1,
|
|
});
|
|
const pcViewSpring = useThreeSpring<BufferGeometry>({
|
|
scale: hovered === View.PCView ? 1.25 : 1,
|
|
});
|
|
const creditsViewSpring = useThreeSpring<BufferGeometry>({
|
|
scale: hovered === View.CreditsView ? 1.25 : 1,
|
|
});
|
|
const printerViewSpring = useThreeSpring<BufferGeometry>({
|
|
scale: hovered === View.PrinterView ? 1.25 : 1,
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{/* Main Menu */}
|
|
<group>
|
|
<animated.mesh
|
|
ref={desktopRef}
|
|
position={[0.9, 7, 1.65]}
|
|
onClick={
|
|
currentView === View.MainView
|
|
? () => goToView(View.DesktopView)
|
|
: undefined
|
|
}
|
|
onPointerOver={
|
|
currentView === View.MainView
|
|
? () => setHovered(View.DesktopView)
|
|
: undefined
|
|
}
|
|
onPointerOut={() => setHovered(null)}
|
|
scale={desktopViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.6, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="white"
|
|
opacity={mainMenuSpring.opacity}
|
|
/>
|
|
</animated.mesh>
|
|
<animated.mesh
|
|
position={[0.9, 7, 1.65]}
|
|
scale={desktopViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.65, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="black"
|
|
opacity={mainMenuSpring.opacity}
|
|
side={BackSide}
|
|
/>
|
|
</animated.mesh>
|
|
<Billboard position={[0.9, 8.5, 1.65]}>
|
|
<AnimatedText
|
|
font="/assets/inter.ttf"
|
|
fontSize={0.75}
|
|
outlineWidth={0.05}
|
|
fillOpacity={mainMenuSpring.opacity}
|
|
outlineOpacity={mainMenuSpring.opacity}
|
|
>
|
|
Projects & Socials
|
|
</AnimatedText>
|
|
</Billboard>
|
|
<animated.mesh
|
|
ref={printerRef}
|
|
position={[-5.5, 6, -2]}
|
|
onClick={
|
|
currentView === View.MainView
|
|
? () => goToView(View.PrinterView)
|
|
: undefined
|
|
}
|
|
onPointerOver={
|
|
currentView === View.MainView
|
|
? () => setHovered(View.PrinterView)
|
|
: undefined
|
|
}
|
|
onPointerOut={() => setHovered(null)}
|
|
scale={printerViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.6, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="white"
|
|
opacity={mainMenuSpring.opacity}
|
|
/>
|
|
</animated.mesh>
|
|
<animated.mesh position={[-5.5, 6, -2]} scale={printerViewSpring.scale}>
|
|
<sphereGeometry args={[0.65, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="black"
|
|
opacity={mainMenuSpring.opacity}
|
|
side={BackSide}
|
|
/>
|
|
</animated.mesh>
|
|
<Billboard position={[-5.5, 7.5, -2]}>
|
|
<AnimatedText
|
|
font="/assets/inter.ttf"
|
|
fontSize={0.75}
|
|
outlineWidth={0.05}
|
|
fillOpacity={mainMenuSpring.opacity}
|
|
outlineOpacity={mainMenuSpring.opacity}
|
|
>
|
|
Hobby Corner
|
|
</AnimatedText>
|
|
</Billboard>
|
|
<animated.mesh
|
|
ref={sideRef}
|
|
position={[2.525, 6, 6.75]}
|
|
onClick={
|
|
currentView === View.MainView
|
|
? () => goToView(View.ResumeView)
|
|
: undefined
|
|
}
|
|
onPointerOver={
|
|
currentView === View.MainView
|
|
? () => setHovered(View.ResumeView)
|
|
: undefined
|
|
}
|
|
onPointerOut={() => setHovered(null)}
|
|
scale={resumeViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.6, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="white"
|
|
opacity={mainMenuSpring.opacity}
|
|
/>
|
|
</animated.mesh>
|
|
<animated.mesh
|
|
position={[2.525, 6, 6.75]}
|
|
scale={resumeViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.65, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="black"
|
|
opacity={mainMenuSpring.opacity}
|
|
side={BackSide}
|
|
/>
|
|
</animated.mesh>
|
|
<Billboard position={[2.525, 7.5, 6.75]}>
|
|
<AnimatedText
|
|
font="/assets/inter.ttf"
|
|
fontSize={0.75}
|
|
outlineWidth={0.05}
|
|
fillOpacity={mainMenuSpring.opacity}
|
|
outlineOpacity={mainMenuSpring.opacity}
|
|
>
|
|
Resume
|
|
</AnimatedText>
|
|
</Billboard>
|
|
</group>
|
|
{/* Desktop Menu */}
|
|
<group>
|
|
<animated.mesh
|
|
ref={cellphoneRef}
|
|
position={[2.358, 5.125, -2.055]}
|
|
onClick={
|
|
currentView === View.DesktopView
|
|
? () => goToView(View.CellphoneView)
|
|
: undefined
|
|
}
|
|
onPointerOver={
|
|
currentView === View.DesktopView
|
|
? () => setHovered(View.CellphoneView)
|
|
: undefined
|
|
}
|
|
onPointerOut={() => setHovered(null)}
|
|
scale={cellphoneViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.25, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="white"
|
|
opacity={desktopMenuSpring.opacity}
|
|
/>
|
|
</animated.mesh>
|
|
<animated.mesh
|
|
position={[2.358, 5.125, -2.055]}
|
|
scale={cellphoneViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.25 + 1 / 48, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="black"
|
|
opacity={desktopMenuSpring.opacity}
|
|
side={BackSide}
|
|
/>
|
|
</animated.mesh>
|
|
<Billboard position={[2.358, 5.75, -2.055]}>
|
|
<AnimatedText
|
|
font="/assets/inter.ttf"
|
|
fontSize={0.3125}
|
|
outlineWidth={1 / 48}
|
|
fillOpacity={desktopMenuSpring.opacity}
|
|
outlineOpacity={desktopMenuSpring.opacity}
|
|
>
|
|
Socials
|
|
</AnimatedText>
|
|
</Billboard>
|
|
<animated.mesh
|
|
ref={pcRef}
|
|
position={[3.25, 5.982, 1.668]}
|
|
onClick={
|
|
currentView === View.DesktopView
|
|
? () => goToView(View.PCView)
|
|
: undefined
|
|
}
|
|
onPointerOver={
|
|
currentView === View.DesktopView
|
|
? () => setHovered(View.PCView)
|
|
: undefined
|
|
}
|
|
onPointerOut={() => setHovered(null)}
|
|
scale={pcViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.25, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="white"
|
|
opacity={desktopMenuSpring.opacity}
|
|
/>
|
|
</animated.mesh>
|
|
<animated.mesh
|
|
position={[3.25, 5.982, 1.668]}
|
|
scale={pcViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.25 + 1 / 48, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="black"
|
|
opacity={desktopMenuSpring.opacity}
|
|
side={BackSide}
|
|
/>
|
|
</animated.mesh>
|
|
<Billboard position={[3.25, 6.607, 1.668]}>
|
|
<AnimatedText
|
|
font="/assets/inter.ttf"
|
|
fontSize={0.3125}
|
|
outlineWidth={1 / 48}
|
|
fillOpacity={desktopMenuSpring.opacity}
|
|
outlineOpacity={desktopMenuSpring.opacity}
|
|
>
|
|
Projects
|
|
</AnimatedText>
|
|
</Billboard>
|
|
<animated.mesh
|
|
ref={pcRef}
|
|
position={[3.25, 5.982, -2.15]}
|
|
onClick={
|
|
currentView === View.DesktopView
|
|
? () => goToView(View.CreditsView)
|
|
: undefined
|
|
}
|
|
onPointerOver={
|
|
currentView === View.DesktopView
|
|
? () => setHovered(View.CreditsView)
|
|
: undefined
|
|
}
|
|
onPointerOut={() => setHovered(null)}
|
|
scale={creditsViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.25, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="white"
|
|
opacity={creditsButtonSpring.opacity}
|
|
/>
|
|
</animated.mesh>
|
|
<animated.mesh
|
|
position={[3.25, 5.982, -2.15]}
|
|
scale={creditsViewSpring.scale}
|
|
>
|
|
<sphereGeometry args={[0.25 + 1 / 48, 32, 32]} />
|
|
<animated.meshStandardMaterial
|
|
transparent
|
|
color="black"
|
|
opacity={creditsButtonSpring.opacity}
|
|
side={BackSide}
|
|
/>
|
|
</animated.mesh>
|
|
</group>
|
|
</>
|
|
);
|
|
};
|