From bbc4ba4eff5b3f2c700e2e54ece792a3de5f3488 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 23 May 2025 01:58:21 +0800 Subject: [PATCH] feat(3d): add camera animation when hover on navbar --- components/models/home/desk-camera.tsx | 44 +++++++++++++++++++++++++- components/pages/nav-bar.tsx | 7 ++++ store/index.ts | 30 ++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/components/models/home/desk-camera.tsx b/components/models/home/desk-camera.tsx index 2acb6be..d7ed60b 100644 --- a/components/models/home/desk-camera.tsx +++ b/components/models/home/desk-camera.tsx @@ -2,16 +2,24 @@ import { useFrame } from '@react-three/fiber'; import { easing } from 'maath'; import { JSX, useRef } from 'react'; import { useMediaQuery } from 'react-responsive'; +import useStore from 'store'; import * as THREE from 'three'; const DeskCamera = ({ children }: { children: JSX.Element }) => { const groupRef = useRef(null); + const navbarHoverItems = useStore((state) => state.navbarHoverItems); const isMobile = useMediaQuery({ query: '(max-width: 768px)' }); useFrame((state, delta) => { + // enter animation easing.damp3(state.camera.position, [0, 0, 26], 0.25, delta); - if (!isMobile && groupRef.current) { + const hasHover = Object.values(navbarHoverItems).some((v) => v); + if (groupRef.current && hasHover) { + easing.dampE(groupRef.current.rotation, [0, 0, 0], 0.25, delta); + } + // mouse move animation + if (!isMobile && groupRef.current && !hasHover) { const x = (() => { const _x = state.pointer.y / 3; if (_x < 0) { @@ -37,6 +45,40 @@ const DeskCamera = ({ children }: { children: JSX.Element }) => { } }); + // move camera to specific position when hovering navbar + useFrame((state, delta) => { + const map = { + blog: () => { + easing.damp3(state.camera.position, [0, 8.8, -10], 0.25, delta); + }, + projects: () => { + easing.damp3(state.camera.position, [-10, 8, -12], 0.25, delta); + if (groupRef.current) { + easing.dampE(groupRef.current.rotation, [0, -7.2, 0], 0.25, delta); + } + }, + tags: () => { + easing.damp3(state.camera.position, [-7, 5, -14], 0.25, delta); + if (groupRef.current) { + easing.dampE(groupRef.current.rotation, [1, 0, 0], 0.25, delta); + } + }, + friends: () => { + easing.damp3(state.camera.position, [10, 5, -12], 0.25, delta); + }, + about: () => { + easing.damp3(state.camera.position, [-24, 14, -10], 0.25, delta); + }, + }; + + for (const [key, value] of Object.entries(navbarHoverItems)) { + if (value) { + /** @ts-expect-error */ + map[key](); + } + } + }); + return {children}; }; diff --git a/components/pages/nav-bar.tsx b/components/pages/nav-bar.tsx index 2993939..144d3c8 100644 --- a/components/pages/nav-bar.tsx +++ b/components/pages/nav-bar.tsx @@ -5,6 +5,7 @@ import clsx from 'clsx'; import Link from 'next/link'; import { memo, useEffect, useState } from 'react'; import { FiMenu } from 'react-icons/fi'; +import useStore from 'store'; import DarkModeBtn from './dark-mode-btn'; const txtMenu = [ @@ -71,6 +72,10 @@ const HeadBar = () => { }; }, []); + const toggleNavbarHoverItems = useStore( + (state) => state.toggleNavbarHoverItems, + ); + return ( <>
{ 'mb-2 last:mb-0 md:mb-0', 'md:mr-4 md:last:mr-0', )} + onMouseOver={() => toggleNavbarHoverItems(m.name.toLowerCase())} + onMouseOut={() => toggleNavbarHoverItems(m.name.toLowerCase())} > {m.name} diff --git a/store/index.ts b/store/index.ts index 1fd024f..1596e12 100644 --- a/store/index.ts +++ b/store/index.ts @@ -1,8 +1,21 @@ import { create } from 'zustand'; +// type NavbarHoverItemsKey = ['blog', 'projects', 'tags', 'friends', 'about']; + interface MainStore { + /** Loading state of the model */ modelLoading: boolean; toggleLoading: (loaded: boolean) => void; + + /** mouse hover on navbar */ + navbarHoverItems: { + blog: boolean; + projects: boolean; + tags: boolean; + friends: boolean; + about: boolean; + }; + toggleNavbarHoverItems: (item: string) => void; } const useStore = create()((set) => ({ @@ -11,6 +24,23 @@ const useStore = create()((set) => ({ set(() => ({ modelLoading: loaded, })), + + navbarHoverItems: { + blog: false, + projects: false, + tags: false, + friends: false, + about: false, + } as const, + toggleNavbarHoverItems: (item) => + set((state) => ({ + navbarHoverItems: { + ...state.navbarHoverItems, + [item as unknown as string]: + /** @ts-expect-error */ + !state.navbarHoverItems[item], + }, + })), })); export default useStore;