Skip to content

Commit 12efe1a

Browse files
committed
feat: add mobile gravity tilt to ProfileCard
1 parent a19053d commit 12efe1a

1 file changed

Lines changed: 90 additions & 5 deletions

File tree

src/components/ProfileCard.tsx

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
useCallback,
3+
useEffect,
34
useRef,
45
type CSSProperties,
56
type PointerEvent,
@@ -24,6 +25,21 @@ type ProfileCardProps = {
2425
onContactClick?: () => void;
2526
};
2627

28+
type DeviceOrientationPermissionEvent = DeviceOrientationEventConstructor & {
29+
requestPermission?: () => Promise<PermissionState>;
30+
};
31+
32+
const clamp = (value: number, min: number, max: number) =>
33+
Math.min(Math.max(value, min), max);
34+
35+
const resetTilt = (node: HTMLElement) => {
36+
node.style.setProperty("--profile-x", "50%");
37+
node.style.setProperty("--profile-y", "50%");
38+
node.style.setProperty("--profile-rotate-x", "0deg");
39+
node.style.setProperty("--profile-rotate-y", "0deg");
40+
node.style.setProperty("--profile-glow-opacity", "0");
41+
};
42+
2743
const ProfileCard = ({
2844
avatarUrl,
2945
iconUrl,
@@ -43,6 +59,79 @@ const ProfileCard = ({
4359
onContactClick,
4460
}: ProfileCardProps) => {
4561
const ref = useRef<HTMLDivElement>(null);
62+
const orientationEnabledRef = useRef(false);
63+
const orientationPermissionRequestedRef = useRef(false);
64+
65+
useEffect(() => {
66+
if (!enableTilt) return;
67+
68+
const reduceMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
69+
const mobileQuery = window.matchMedia(
70+
"(hover: none) and (pointer: coarse) and (max-width: 768px)",
71+
);
72+
if (reduceMotionQuery.matches || !mobileQuery.matches) return;
73+
74+
const handleOrientation = (event: DeviceOrientationEvent) => {
75+
const node = ref.current;
76+
if (!node || event.beta === null || event.gamma === null) return;
77+
78+
const beta = clamp(event.beta, -35, 35);
79+
const gamma = clamp(event.gamma, -25, 25);
80+
const percentX = clamp(50 + gamma * 1.6, 18, 82);
81+
const percentY = clamp(50 + beta * 1.15, 18, 82);
82+
const rotateX = (beta / 35) * -6;
83+
const rotateY = (gamma / 25) * 6;
84+
85+
node.style.setProperty("--profile-x", `${percentX}%`);
86+
node.style.setProperty("--profile-y", `${percentY}%`);
87+
node.style.setProperty("--profile-rotate-x", `${rotateX}deg`);
88+
node.style.setProperty("--profile-rotate-y", `${rotateY}deg`);
89+
node.style.setProperty("--profile-glow-opacity", "1");
90+
};
91+
92+
const startOrientation = () => {
93+
if (orientationEnabledRef.current) return;
94+
orientationEnabledRef.current = true;
95+
window.addEventListener("deviceorientation", handleOrientation, true);
96+
};
97+
98+
const requestOrientationPermission = () => {
99+
if (orientationPermissionRequestedRef.current) return;
100+
orientationPermissionRequestedRef.current = true;
101+
102+
const orientationEvent = window.DeviceOrientationEvent as
103+
| DeviceOrientationPermissionEvent
104+
| undefined;
105+
if (!orientationEvent?.requestPermission) {
106+
startOrientation();
107+
return;
108+
}
109+
110+
void orientationEvent
111+
.requestPermission()
112+
.then((state) => {
113+
if (state === "granted") startOrientation();
114+
})
115+
.catch(() => {
116+
orientationPermissionRequestedRef.current = false;
117+
});
118+
};
119+
120+
requestOrientationPermission();
121+
window.addEventListener("pointerdown", requestOrientationPermission, {
122+
once: true,
123+
passive: true,
124+
});
125+
126+
return () => {
127+
orientationEnabledRef.current = false;
128+
window.removeEventListener("deviceorientation", handleOrientation, true);
129+
window.removeEventListener("pointerdown", requestOrientationPermission);
130+
131+
const node = ref.current;
132+
if (node) resetTilt(node);
133+
};
134+
}, [enableTilt]);
46135

47136
const setPointer = useCallback(
48137
(event: PointerEvent<HTMLDivElement>) => {
@@ -70,11 +159,7 @@ const ProfileCard = ({
70159
const node = ref.current;
71160
if (!node) return;
72161

73-
node.style.setProperty("--profile-x", "50%");
74-
node.style.setProperty("--profile-y", "50%");
75-
node.style.setProperty("--profile-rotate-x", "0deg");
76-
node.style.setProperty("--profile-rotate-y", "0deg");
77-
node.style.setProperty("--profile-glow-opacity", "0");
162+
resetTilt(node);
78163
}, []);
79164

80165
const style = {

0 commit comments

Comments
 (0)