Files
portfolio-2/src/components/scene/Buttons.tsx
Ahmed Al-Taiar d95ec6e036
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 5s
[#2] Sidemenu for touchscreen devices
2025-05-06 22:22:48 -04:00

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>
</>
);
};