diff --git a/app/stickers/page.tsx b/app/stickers/page.tsx new file mode 100644 index 0000000..cf72172 --- /dev/null +++ b/app/stickers/page.tsx @@ -0,0 +1,74 @@ +"use client" + +import Floating, { FloatingElement } from "../../components/fancy/parallax-floating" +import LandingLayoutView from "@hhs/layouts/landing-layout" +import { motion } from "framer-motion" +import { useRef } from "react" + +const bgColors = [ + "bg-red-500", + "bg-blue-500", + "bg-green-500", + "bg-yellow-500", + "bg-purple-500", + "bg-pink-500", + "bg-indigo-500", + "bg-orange-500", +] + +const positions = [ + "top-1/4 left-[10%]", + "top-1/2 left-[15%]", + "top-1/3 left-[40%]", + "top-2/3 left-[60%]", + "top-1/4 right-[20%]", + "bottom-1/3 left-[20%]", + "bottom-1/2 right-[30%]", + "bottom-1/4 left-[45%]", +] + +const stickers = Array.from({ length: 8 }, (_, i) => ({ + depth: Math.random() * 2 + 0.5, + className: `${positions[i]} ${bgColors[i]} w-32 h-32 rounded-xl cursor-pointer`, +})) + +export default function StickersPage() { + const scope = useRef(null) + + return ( + +
+ + {stickers.map((sticker, index) => ( + + + + ))} + +
+ +

+ Stickers +

+
+
+
+
+ ) +} diff --git a/bun.lockb b/bun.lockb index 4b8d6b8..19bb597 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/fancy/parallax-floating.tsx b/components/fancy/parallax-floating.tsx new file mode 100644 index 0000000..c04af5c --- /dev/null +++ b/components/fancy/parallax-floating.tsx @@ -0,0 +1,134 @@ +"use client" + +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, +} from "react" +import { useAnimationFrame } from "motion/react" + +import { cn } from "@hhs/utils/cn" +import { useMousePositionRef } from "@hhs/hooks/use-mouse-position-ref" + +interface FloatingContextType { + registerElement: (id: string, element: HTMLDivElement, depth: number) => void + unregisterElement: (id: string) => void +} + +const FloatingContext = createContext(null) + +interface FloatingProps { + children: ReactNode + className?: string + sensitivity?: number + easingFactor?: number +} + +const Floating = ({ + children, + className, + sensitivity = 1, + easingFactor = 0.05, + ...props +}: FloatingProps) => { + const containerRef = useRef(null) + const elementsMap = useRef( + new Map< + string, + { + element: HTMLDivElement + depth: number + currentPosition: { x: number; y: number } + } + >() + ) + const mousePositionRef = useMousePositionRef(containerRef) + + const registerElement = useCallback( + (id: string, element: HTMLDivElement, depth: number) => { + elementsMap.current.set(id, { + element, + depth, + currentPosition: { x: 0, y: 0 }, + }) + }, + [] + ) + + const unregisterElement = useCallback((id: string) => { + elementsMap.current.delete(id) + }, []) + + useAnimationFrame(() => { + if (!containerRef.current) return + + elementsMap.current.forEach((data) => { + const strength = (data.depth * sensitivity) / 20 + + // Calculate new target position + const newTargetX = mousePositionRef.current.x * strength + const newTargetY = mousePositionRef.current.y * strength + + // Check if we need to update + const dx = newTargetX - data.currentPosition.x + const dy = newTargetY - data.currentPosition.y + + // Update position only if we're still moving + data.currentPosition.x += dx * easingFactor + data.currentPosition.y += dy * easingFactor + + data.element.style.transform = `translate3d(${data.currentPosition.x}px, ${data.currentPosition.y}px, 0)` + }) + }) + + return ( + +
+ {children} +
+
+ ) +} + +export default Floating + +interface FloatingElementProps { + children: ReactNode + className?: string + depth?: number +} + +export const FloatingElement = ({ + children, + className, + depth = 1, +}: FloatingElementProps) => { + const elementRef = useRef(null) + const idRef = useRef(Math.random().toString(36).substring(7)) + const context = useContext(FloatingContext) + + useEffect(() => { + if (!elementRef.current || !context) return + + const nonNullDepth = depth ?? 0.01 + + context.registerElement(idRef.current, elementRef.current, nonNullDepth) + return () => context.unregisterElement(idRef.current) + }, [depth]) + + return ( +
+ {children} +
+ ) +} diff --git a/constants/layout.ts b/constants/layout.ts index e8376d7..77f5758 100644 --- a/constants/layout.ts +++ b/constants/layout.ts @@ -27,6 +27,10 @@ export const NAV_ITEMS = [ label: "Live", href: "/live", }, + { + label: "Stickers", + href: "/stickers", + }, { label: "HHS", href: "#", diff --git a/hooks/use-mouse-position-ref.ts b/hooks/use-mouse-position-ref.ts new file mode 100644 index 0000000..c801d82 --- /dev/null +++ b/hooks/use-mouse-position-ref.ts @@ -0,0 +1,42 @@ +import { RefObject, useEffect, useRef } from "react" + +export const useMousePositionRef = ( + containerRef?: RefObject +) => { + const positionRef = useRef({ x: 0, y: 0 }) + + useEffect(() => { + const updatePosition = (x: number, y: number) => { + if (containerRef && containerRef.current) { + const rect = containerRef.current.getBoundingClientRect() + const relativeX = x - rect.left + const relativeY = y - rect.top + + // Calculate relative position even when outside the container + positionRef.current = { x: relativeX, y: relativeY } + } else { + positionRef.current = { x, y } + } + } + + const handleMouseMove = (ev: MouseEvent) => { + updatePosition(ev.clientX, ev.clientY) + } + + const handleTouchMove = (ev: TouchEvent) => { + const touch = ev.touches[0] + updatePosition(touch.clientX, touch.clientY) + } + + // Listen for both mouse and touch events + window.addEventListener("mousemove", handleMouseMove) + window.addEventListener("touchmove", handleTouchMove) + + return () => { + window.removeEventListener("mousemove", handleMouseMove) + window.removeEventListener("touchmove", handleTouchMove) + } + }, [containerRef]) + + return positionRef +} diff --git a/package.json b/package.json index 3cdecc2..b5dc833 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "ical-generator": "^8.0.1", "lodash.debounce": "^4.0.8", "lucide-react": "^0.447.0", + "motion": "^11.18.0", "next": "14.2.21", "next-themes": "^0.3.0", "react": "^18",