feat: Implement initial cinematic portfolio UI with animated components and page structure.
This commit is contained in:
1 parent
9d779390e5
commit
2ede7e98f9
18 files changed
+2107
-147
No files matched your search
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { motion, useMotionValue, useSpring } from "framer-motion";
|
||||
|
||||
export default function CustomCursor() {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [isClicking, setIsClicking] = useState(false);
|
||||
const [hoverText, setHoverText] = useState("");
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const cursorX = useMotionValue(0);
|
||||
const cursorY = useMotionValue(0);
|
||||
const springConfig = { damping: 25, stiffness: 250, mass: 0.5 };
|
||||
const x = useSpring(cursorX, springConfig);
|
||||
const y = useSpring(cursorY, springConfig);
|
||||
|
||||
const ringSpringConfig = { damping: 20, stiffness: 150, mass: 0.8 };
|
||||
const ringX = useSpring(cursorX, ringSpringConfig);
|
||||
const ringY = useSpring(cursorY, ringSpringConfig);
|
||||
|
||||
const rafRef = useRef<number>(0);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
cursorX.set(e.clientX);
|
||||
cursorY.set(e.clientY);
|
||||
if (!isVisible) setIsVisible(true);
|
||||
});
|
||||
},
|
||||
[cursorX, cursorY, isVisible]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Only show custom cursor on desktop with fine pointer
|
||||
const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
|
||||
if (!hasFinePointer) return;
|
||||
|
||||
const handleDown = () => setIsClicking(true);
|
||||
const handleUp = () => setIsClicking(false);
|
||||
const handleLeave = () => setIsVisible(false);
|
||||
const handleEnter = () => setIsVisible(true);
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mousedown", handleDown);
|
||||
window.addEventListener("mouseup", handleUp);
|
||||
document.addEventListener("mouseleave", handleLeave);
|
||||
document.addEventListener("mouseenter", handleEnter);
|
||||
|
||||
// Detect hoverable elements
|
||||
const observer = new MutationObserver(() => {
|
||||
attachHoverListeners();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
attachHoverListeners();
|
||||
|
||||
function attachHoverListeners() {
|
||||
const hoverables = document.querySelectorAll(
|
||||
'a, button, [role="button"], input, textarea, select, [data-cursor]'
|
||||
);
|
||||
for (const el of hoverables) {
|
||||
el.addEventListener("mouseenter", handleElementEnter as EventListener);
|
||||
el.addEventListener("mouseleave", handleElementLeave as EventListener);
|
||||
}
|
||||
}
|
||||
|
||||
function handleElementEnter(e: Event) {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
setIsHovering(true);
|
||||
const cursorData = el.getAttribute("data-cursor");
|
||||
if (cursorData) setHoverText(cursorData);
|
||||
}
|
||||
|
||||
function handleElementLeave() {
|
||||
setIsHovering(false);
|
||||
setHoverText("");
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mousedown", handleDown);
|
||||
window.removeEventListener("mouseup", handleUp);
|
||||
document.removeEventListener("mouseleave", handleLeave);
|
||||
document.removeEventListener("mouseenter", handleEnter);
|
||||
observer.disconnect();
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [handleMouseMove]);
|
||||
|
||||
// Don't render on touch devices
|
||||
if (typeof window !== "undefined" && !window.matchMedia("(pointer: fine)").matches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
@media (pointer: fine) {
|
||||
* { cursor: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Dot cursor */}
|
||||
<motion.div
|
||||
className="pointer-events-none fixed top-0 left-0 z-[9999] mix-blend-difference"
|
||||
style={{
|
||||
x,
|
||||
y,
|
||||
translateX: "-50%",
|
||||
translateY: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
scale: isClicking ? 0.5 : isHovering ? 0.5 : 1,
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<div
|
||||
className="rounded-full bg-[#ccff00]"
|
||||
style={{ width: 8, height: 8 }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Ring cursor */}
|
||||
<motion.div
|
||||
className="pointer-events-none fixed top-0 left-0 z-[9998] mix-blend-difference"
|
||||
style={{
|
||||
x: ringX,
|
||||
y: ringY,
|
||||
translateX: "-50%",
|
||||
translateY: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
width: isHovering ? 80 : 40,
|
||||
height: isHovering ? 80 : 40,
|
||||
opacity: isVisible ? (isHovering ? 0.8 : 0.4) : 0,
|
||||
borderWidth: isHovering ? 2 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center rounded-full border-[#ccff00]"
|
||||
style={{ borderStyle: "solid", borderColor: "#ccff00", borderWidth: "inherit" }}
|
||||
>
|
||||
{hoverText && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-[9px] font-bold uppercase tracking-widest text-[#ccff00]"
|
||||
>
|
||||
{hoverText}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user