From b7504d5acf5b66a55f5b0625bfcbc6d9b73bfc0d Mon Sep 17 00:00:00 2001 From: grtsinry43 Date: Tue, 6 May 2025 11:08:39 +0800 Subject: [PATCH] feat: add GSAP animations and new sections for personal intro, skills tree, photography, and project showcase --- src/app/page.tsx | 25 +- src/components/sections/personal-intro.tsx | 261 ++++++++++ .../sections/photography-section.tsx | 389 +++++++++++++++ src/components/sections/project-showcase.tsx | 384 +++++++++++++++ .../sections/rhythm-games-section.tsx | 450 ++++++++++++++++++ src/components/sections/skill-tree.tsx | 328 +++++++++++++ tailwind.config.ts | 21 +- 7 files changed, 1852 insertions(+), 6 deletions(-) create mode 100644 src/components/sections/personal-intro.tsx create mode 100644 src/components/sections/photography-section.tsx create mode 100644 src/components/sections/project-showcase.tsx create mode 100644 src/components/sections/rhythm-games-section.tsx create mode 100644 src/components/sections/skill-tree.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index eff9540..3aa8611 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,6 +18,11 @@ import CreativeCanvas from "@/components/creative-canvas" import PersonalitySection from "@/components/sections/personality-section"; import {LampContainer} from "@/components/ui/lamp-container"; import CardSection from "@/components/sections/card-section"; +import GsapSkillsTree from "@/components/sections/skill-tree"; +import GsapProjectsShowcase from "@/components/sections/project-showcase"; +import GsapPersonalIntro from "@/components/sections/personal-intro"; +import GsapPhotographySection from "@/components/sections/photography-section"; +import GsapRhythmGamesSection from "@/components/sections/rhythm-games-section"; export default function HomePage() { const {theme} = useTheme() @@ -58,7 +63,7 @@ export default function HomePage() { }, [mouseX, mouseY]) return ( -
+
{/* Progress indicator */} @@ -99,16 +104,16 @@ export default function HomePage() {
-
+
-
+
{/* Creative background canvas */}
- {/* Hero Section with 3D animation */} - + {/* Hero Section with GSAP animation */} +
@@ -137,6 +142,10 @@ export default function HomePage() { {/* Skills Section with animation */} + + + + {/* Projects Section with enhanced visuals */} @@ -164,6 +173,12 @@ export default function HomePage() { {/* Personality Section with animation */} + {/**/} + + + + + {/* Contact Section with glass morphism */}
diff --git a/src/components/sections/personal-intro.tsx b/src/components/sections/personal-intro.tsx new file mode 100644 index 0000000..8e03931 --- /dev/null +++ b/src/components/sections/personal-intro.tsx @@ -0,0 +1,261 @@ +"use client" + +import {useRef, useEffect, useState} from "react" +import {AnimatePresence, motion, useScroll, useTransform} from "framer-motion" +import gsap from "gsap" +import {ScrollTrigger} from "gsap/ScrollTrigger" +import {TextPlugin} from "gsap/TextPlugin" +import {Github, Mail, ExternalLink, Code, Heart, Coffee} from "lucide-react" +import {Button} from "@/components/ui/button" +import {useTheme} from "next-themes" + +export default function GsapPersonalIntro() { + const containerRef = useRef(null) + const textRef = useRef(null) + const imageRef = useRef(null) + const {theme, resolvedTheme} = useTheme() + + const {scrollYProgress} = useScroll({ + target: containerRef, + offset: ["start start", "end start"], + }) + + const y = useTransform(scrollYProgress, [0, 1], ["0%", "40%"]) + const scale = useTransform(scrollYProgress, [0, 1], [1, 0.9]) + const opacity = useTransform(scrollYProgress, [0, 0.8], [1, 0]) + + useEffect(() => { + gsap.registerPlugin(ScrollTrigger, TextPlugin); + + if (!containerRef.current) return; + + const container = containerRef.current; + + // 清理所有 ScrollTrigger 实例 + ScrollTrigger.getAll().forEach((trigger) => trigger.kill()); + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: container, + start: "top 80%", + end: "bottom 20%", + toggleActions: "play none none reverse", + }, + }); + + if (textRef.current) { + const headings = textRef.current.querySelectorAll("h3, h4"); + const paragraphs = textRef.current.querySelectorAll("p"); + const buttons = textRef.current.querySelectorAll("button, a"); + const icons = textRef.current.querySelectorAll(".icon-item"); + + tl.from(headings, {opacity: 0, y: 50, duration: 0.8, stagger: 0.2, ease: "power3.out"}) + .from(paragraphs, {opacity: 0, y: 30, duration: 0.6, stagger: 0.2, ease: "power2.out"}, "-=0.4") + .from(buttons, { + opacity: 0, + y: 20, + scale: 0.9, + duration: 0.5, + stagger: 0.1, + ease: "back.out(1.7)" + }, "-=0.3") + .from(icons, { + opacity: 0, + scale: 0, + rotation: -30, + duration: 0.6, + stagger: 0.1, + ease: "back.out(2)" + }, "-=0.4"); + } + + if (imageRef.current) { + const shapes = imageRef.current.querySelectorAll(".shape"); + const particles = imageRef.current.querySelectorAll(".particle"); + + tl.from(shapes, { + opacity: 0, + scale: 0, + rotation: -60, + transformOrigin: "center", + duration: 0.8, + stagger: 0.1, + ease: "back.out(2)" + }, "-=0.8"); + + particles.forEach((particle) => { + gsap.to(particle, { + y: "random(-30, 30)", + x: "random(-30, 30)", + rotation: "random(-360, 360)", + duration: "random(3, 6)", + repeat: -1, + yoyo: true, + ease: "sine.inOut", + }); + }); + } + + return () => { + // 确保清理所有 ScrollTrigger 实例 + ScrollTrigger.getAll().forEach((trigger) => trigger.kill()); + }; + }, [theme, resolvedTheme]); + + const words = ["全栈开发者", "设计者", "创造者"] + const [currentWord, setCurrentWord] = useState(0) + + useEffect(() => { + const interval = setInterval(() => { + setCurrentWord((prev) => (prev + 1) % words.length) + }, 2000) + return () => clearInterval(interval) + }, []) + + return ( + + {/* Background elements */} +
+
+
+
+
+
+
+
+ +
+
+
+
+

+ 你好,我是

grtsinry43

+

+

+ 热爱生活的{" "} + + + {words[currentWord]} + + +

+
+

+ 我专注于构建优雅、高效的应用程序,无论是 Web 应用还是 Android 应用。 + 我相信良好的用户体验和干净的代码同样重要。 +

+
+
+
+ +
+ 开发者 +
+
+
+ +
+ 热爱创造 +
+
+
+ +
+ 咖啡爱好者 +
+
+ +
+
+
+
+ grtsinry43 + {resolvedTheme === "dark" && ( +
+ )} +
+

grtsinry43

+

全栈开发者 & 设计爱好者

+
+
+
+
+
+ {[...Array(10)].map((_, i) => ( +
+ ))} +
+
+
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/src/components/sections/photography-section.tsx b/src/components/sections/photography-section.tsx new file mode 100644 index 0000000..0fa3ff4 --- /dev/null +++ b/src/components/sections/photography-section.tsx @@ -0,0 +1,389 @@ +"use client" + +import {useRef, useEffect, useState} from "react" +import {motion, AnimatePresence} from "framer-motion" +import {Camera, Film, Scissors, ImageIcon, ChevronLeft, ChevronRight} from "lucide-react" +import {Button} from "@/components/ui/button" +import {useTheme} from "next-themes" + +export default function GsapPhotographySection() { + const containerRef = useRef(null) + const {theme} = useTheme() + const [activeIndex, setActiveIndex] = useState(0) + const [isInView, setIsInView] = useState(false) + const [direction, setDirection] = useState(0) + + // 摄影作品数据 + const photographyItems = [ + { + id: 1, + title: "城市风光", + description: "城市建筑与自然光影的完美结合", + image: "/placeholder.svg?height=600&width=800", + tags: ["城市", "建筑", "光影"], + }, + { + id: 2, + title: "自然风景", + description: "大自然的壮丽景色与细腻纹理", + image: "/placeholder.svg?height=600&width=800", + tags: ["自然", "风景", "纹理"], + }, + { + id: 3, + title: "人像摄影", + description: "捕捉人物神态与情感的瞬间", + image: "/placeholder.svg?height=600&width=800", + tags: ["人像", "情感", "瞬间"], + }, + ] + + // 检测元素是否在视口中 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsInView(true) + } + }, + {threshold: 0.2}, + ) + + if (containerRef.current) { + observer.observe(containerRef.current) + } + + return () => { + if (containerRef.current) { + observer.unobserve(containerRef.current) + } + } + }, []) + + // GSAP动画 + useEffect(() => { + if (!isInView) return + + const loadGsap = async () => { + try { + const gsapModule = await import("gsap") + const ScrollTriggerModule = await import("gsap/ScrollTrigger") + + const gsap = gsapModule.default + const ScrollTrigger = ScrollTriggerModule.default + + // 注册ScrollTrigger插件 + gsap.registerPlugin(ScrollTrigger) + + if (!containerRef.current) return + + const container = containerRef.current + const titleElements = container.querySelectorAll(".section-title") + const iconElements = container.querySelectorAll(".icon-item") + + // 标题动画 + gsap.fromTo( + titleElements, + {y: 30, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + stagger: 0.2, + ease: "power3.out", + scrollTrigger: { + trigger: container, + start: "top 70%", + }, + }, + ) + + // 图标动画 + gsap.fromTo( + iconElements, + {scale: 0.5, opacity: 0}, + { + scale: 1, + opacity: 1, + duration: 0.6, + stagger: 0.1, + ease: "back.out(1.7)", + scrollTrigger: { + trigger: container, + start: "top 70%", + }, + }, + ) + + // 创建浮动动画 + iconElements.forEach((icon, index) => { + gsap.to(icon, { + y: "random(-10, 10)", + duration: 2 + index * 0.2, + repeat: -1, + yoyo: true, + ease: "sine.inOut", + }) + }) + + return () => { + // 清理动画 + ScrollTrigger.getAll().forEach((trigger) => trigger.kill()) + } + } catch (error) { + console.error("Failed to load GSAP:", error) + } + } + + loadGsap() + }, [isInView]) + + // 切换到下一张幻灯片 + const nextSlide = () => { + setDirection(1) + setActiveIndex((prev) => (prev + 1) % photographyItems.length) + } + + // 切换到上一张幻灯片 + const prevSlide = () => { + setDirection(-1) + setActiveIndex((prev) => (prev - 1 + photographyItems.length) % photographyItems.length) + } + + // 自动播放幻灯片 + useEffect(() => { + if (!isInView) return + + const interval = setInterval(() => { + nextSlide() + }, 5000) + + return () => clearInterval(interval) + }, [isInView, activeIndex]) + + // 幻灯片动画变体 + const slideVariants = { + enter: (direction: number) => ({ + x: direction > 0 ? "100%" : "-100%", + opacity: 0, + scale: 0.9, + }), + center: { + x: 0, + opacity: 1, + scale: 1, + transition: { + x: {duration: 0.8, ease: [0.16, 1, 0.3, 1]}, + opacity: {duration: 0.7}, + scale: {duration: 0.7, ease: [0.16, 1, 0.3, 1]}, + }, + }, + exit: (direction: number) => ({ + x: direction < 0 ? "100%" : "-100%", + opacity: 0, + scale: 0.9, + transition: { + x: {duration: 0.8, ease: [0.16, 1, 0.3, 1]}, + opacity: {duration: 0.7}, + scale: {duration: 0.7, ease: [0.16, 1, 0.3, 1]}, + }, + }), + } + + // 内容动画变体 + const contentVariants = { + hidden: {opacity: 0, y: 20}, + visible: (delay: number) => ({ + opacity: 1, + y: 0, + transition: { + delay, + duration: 0.6, + ease: [0.16, 1, 0.3, 1], + }, + }), + } + + return ( +
+ {/* 背景效果 */} +
+
+
+ +
+ +
+

摄影与剪辑

+
+ + + + 我的视觉创作 + + + {/* 图标展示 */} +
+
+
+ +
+ 摄影 +
+
+
+ +
+ 视频 +
+
+
+ +
+ 剪辑 +
+
+
+ +
+ 后期 +
+
+ + {/* 全屏幻灯片 */} +
+ {/* 幻灯片导航按钮 */} + + + + + {/* 幻灯片指示器 */} +
+ {photographyItems.map((_, index) => ( +
+ + {/* 幻灯片内容 */} + + {photographyItems.map( + (item, index) => + index === activeIndex && ( + + {/* 幻灯片背景图片 */} +
+
+ {item.title} +
+ + {/* 幻灯片内容 */} +
+ + {item.title} + + + + {item.description} + + + + {item.tags.map((tag) => ( + + {tag} + + ))} + +
+ + ), + )} + +
+ + {/* 底部CTA */} + +

想了解更多我的视觉作品?

+ +
+
+
+ ) +} diff --git a/src/components/sections/project-showcase.tsx b/src/components/sections/project-showcase.tsx new file mode 100644 index 0000000..7511e9d --- /dev/null +++ b/src/components/sections/project-showcase.tsx @@ -0,0 +1,384 @@ +"use client" + +import {useRef, useEffect, useState} from "react" +import {motion} from "framer-motion" +import {Github, ExternalLink, ArrowRight} from "lucide-react" +import {Button} from "@/components/ui/button" +import {Badge} from "@/components/ui/badge" +import {useTheme} from "next-themes" + +interface Project { + id: number + title: string + description: string + tags: string[] + image: string + githubUrl?: string + liveUrl?: string +} + +export default function GsapProjectsShowcase() { + const containerRef = useRef(null) + const horizontalRef = useRef(null) + const [containerWidth, setContainerWidth] = useState(0) + const [isInView, setIsInView] = useState(false) + const {theme} = useTheme() + + // Project data + const projects: Project[] = [ + { + id: 1, + title: "grtblog", + description: + "个人博客站点,一站式快速解决方案—一套源,扩展性强,快速部署的博客框架,使用 Nextjs + SpringBoot 构建,致力于轻松搭建个性化你的博客站点", + tags: ["React", "Next.js", "Spring Boot", "Umi.js", "Socket.io", "Radix UI"], + image: "/placeholder.svg?height=600&width=800", + githubUrl: "https://github.com/grtsinry43/grtblog", + liveUrl: "https://grtblog.js.org", + }, + { + id: 2, + title: "csu-dynamic-youth", + description: "途有青,去追山!学校官微毕业节的微信网页程序,统计参与次数与时长,生成排行榜,完成完成打卡", + tags: ["微信小程序", "Vue.js", "微信API"], + image: "/placeholder.svg?height=600&width=800", + githubUrl: "#", + }, + { + id: 3, + title: "Portfolio Website", + description: "个人作品集网站,展示我的项目和技能,使用 Next.js 和 Framer Motion 构建", + tags: ["Next.js", "Framer Motion", "Tailwind CSS", "TypeScript"], + image: "/placeholder.svg?height=600&width=800", + githubUrl: "#", + liveUrl: "#", + }, + { + id: 4, + title: "AI Chat Application", + description: "基于人工智能的聊天应用,支持多种语言和自然语言处理", + tags: ["React", "Node.js", "OpenAI API", "WebSockets"], + image: "/placeholder.svg?height=600&width=800", + githubUrl: "#", + }, + ] + + // 检测元素是否在视口中 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsInView(true) + } + }, + {threshold: 0.1}, + ) + + if (containerRef.current) { + observer.observe(containerRef.current) + } + + return () => { + if (containerRef.current) { + observer.unobserve(containerRef.current) + } + } + }, []) + + // 计算水平滚动容器的宽度 + useEffect(() => { + if (horizontalRef.current) { + // 计算所有项目卡片的总宽度 + 间距 + const totalWidth = projects.length * 600 + (projects.length - 1) * 40 + setContainerWidth(totalWidth) + } + }, [projects.length]) + + // 设置水平滚动 + useEffect(() => { + if (!isInView) return + + const loadGsap = async () => { + try { + const gsapModule = await import("gsap") + const ScrollTriggerModule = await import("gsap/ScrollTrigger") + + const gsap = gsapModule.default + const ScrollTrigger = ScrollTriggerModule.default + + // 注册ScrollTrigger插件 + gsap.registerPlugin(ScrollTrigger) + + if (!containerRef.current || !horizontalRef.current) return + + // 创建水平滚动动画 + const scrollTween = gsap.to(horizontalRef.current, { + x: () => -(containerWidth - window.innerWidth + 100), + ease: "none", + scrollTrigger: { + trigger: containerRef.current, + start: "top top", + end: () => `+=${containerWidth}`, + pin: true, + scrub: 1, + invalidateOnRefresh: true, + anticipatePin: 1, + }, + }) + + // 为每个项目卡片创建视差效果 + const projectCards = horizontalRef.current.querySelectorAll(".project-card") + projectCards.forEach((card, i) => { + const image = card.querySelector(".project-image") + const content = card.querySelector(".project-content") + const title = card.querySelector(".project-title") + const desc = card.querySelector(".project-desc") + const tags = card.querySelector(".project-tags") + const links = card.querySelector(".project-links") + + // 图片视差效果 + if (image) { + gsap.fromTo( + image, + {y: 50, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 1, + scrollTrigger: { + containerAnimation: scrollTween, + trigger: card, + start: "left center", + toggleActions: "play none none reverse", + }, + }, + ) + } + + // 内容视差效果 - 依次显示 + if (title) { + gsap.fromTo( + title, + {y: 30, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + delay: 0.2, + scrollTrigger: { + containerAnimation: scrollTween, + trigger: card, + start: "left center", + toggleActions: "play none none reverse", + }, + }, + ) + } + + if (desc) { + gsap.fromTo( + desc, + {y: 30, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + delay: 0.4, + scrollTrigger: { + containerAnimation: scrollTween, + trigger: card, + start: "left center", + toggleActions: "play none none reverse", + }, + }, + ) + } + + if (tags) { + gsap.fromTo( + tags, + {y: 30, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + delay: 0.6, + scrollTrigger: { + containerAnimation: scrollTween, + trigger: card, + start: "left center", + toggleActions: "play none none reverse", + }, + }, + ) + } + + if (links) { + gsap.fromTo( + links, + {y: 30, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + delay: 0.8, + scrollTrigger: { + containerAnimation: scrollTween, + trigger: card, + start: "left center", + toggleActions: "play none none reverse", + }, + }, + ) + } + }) + + return () => { + // 清理动画 + scrollTween.kill() + ScrollTrigger.getAll().forEach((trigger) => trigger.kill()) + } + } catch (error) { + console.error("Failed to load GSAP:", error) + } + } + + loadGsap() + }, [isInView, containerWidth]) + + return ( +
+
+
+ +
+

我的项目

+
+ + + + + 以往项目展示 + + + + + 向下滚动探索我的项目作品集 - 体验视差滚动效果 + +
+
+ + {/* 水平滚动容器 */} +
+ {projects.map((project, index) => ( +
+
+
+ {project.title} +
+ +
+

{project.title}

+

{project.description}

+ +
+ {project.tags.map((tag) => ( + + {tag} + + ))} +
+ +
+ {project.githubUrl && ( + + )} + {project.liveUrl && ( + + )} +
+ + {/* 项目序号 */} +
+ {String(index + 1).padStart(2, "0")} +
+
+
+ ))} + + {/* 结束提示 */} +
+
+

想了解更多项目?

+ +
+
+
+ + {/* 滚动提示 */} +
+ + 向下滚动继续探索 + +
+
+ ) +} diff --git a/src/components/sections/rhythm-games-section.tsx b/src/components/sections/rhythm-games-section.tsx new file mode 100644 index 0000000..581a805 --- /dev/null +++ b/src/components/sections/rhythm-games-section.tsx @@ -0,0 +1,450 @@ +"use client" + +import {useRef, useEffect, useState} from "react" +import {motion} from "framer-motion" +import {Music, Star, Trophy, Gamepad2} from "lucide-react" +import {Button} from "@/components/ui/button" +import {useTheme} from "next-themes" + +export default function GsapRhythmGamesSection() { + const containerRef = useRef(null) + const canvasRef = useRef(null) + const {theme} = useTheme() + const [activeGame, setActiveGame] = useState(0) + const [isInView, setIsInView] = useState(false) + const [isPlaying, setIsPlaying] = useState(false) + + // 音游数据 + const rhythmGames = [ + { + id: 1, + name: "Arcaea", + description: "一款独特的音乐节奏游戏,以其创新的轨道系统和精美的视觉效果而闻名", + image: "/placeholder.svg?height=400&width=400", + level: "Expert", + achievements: ["最高分数: 9,950,000+", "Pure Memory x 15", "EX+ Rating"], + color: "#5d7bf7", + trackColor: "#8e9fff", + }, + { + id: 2, + name: "Cytus II", + description: "由雷亚游戏开发的音乐节奏游戏,以其独特的扫描线机制和丰富的剧情而著名", + image: "/placeholder.svg?height=400&width=400", + level: "Advanced", + achievements: ["Million Master x 20", "TP 99.50+", "All characters unlocked"], + color: "#4cc2ff", + trackColor: "#7ad5ff", + }, + { + id: 3, + name: "Phigros", + description: "一款具有创新判定线机制的音乐节奏游戏,以其高难度和视觉效果而受到玩家喜爱", + image: "/placeholder.svg?height=400&width=400", + level: "Master", + achievements: ["Phi级别达成率 95%+", "全曲目解锁", "EX评级 x 25"], + color: "#ff5a87", + trackColor: "#ff8daa", + }, + { + id: 4, + name: "BanG Dream!", + description: "以日本偶像乐队为主题的音乐节奏游戏,结合了角色培养和社交元素", + image: "/placeholder.svg?height=400&width=400", + level: "Intermediate", + achievements: ["Full Combo x 30", "All bands at max level", "Event ranking top 1000"], + color: "#ffaa44", + trackColor: "#ffc477", + }, + ] + + // 检测元素是否在视口中 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsInView(true) + } else { + setIsPlaying(false) + } + }, + {threshold: 0.2}, + ) + + if (containerRef.current) { + observer.observe(containerRef.current) + } + + return () => { + if (containerRef.current) { + observer.unobserve(containerRef.current) + } + } + }, []) + + // 音符动画 + useEffect(() => { + if (!isInView || !canvasRef.current) return + + const canvas = canvasRef.current + const ctx = canvas.getContext("2d") + if (!ctx) return + + // 设置canvas尺寸 + const setCanvasSize = () => { + canvas.width = canvas.clientWidth * window.devicePixelRatio + canvas.height = canvas.clientHeight * window.devicePixelRatio + ctx.scale(window.devicePixelRatio, window.devicePixelRatio) + } + + setCanvasSize() + window.addEventListener("resize", setCanvasSize) + + // 音符类 + class Note { + x: number + y: number + size: number + speed: number + color: string + rotation: number + rotationSpeed: number + + constructor(canvasWidth: number, canvasHeight: number, color: string) { + this.x = Math.random() * canvasWidth + this.y = canvasHeight + Math.random() * 100 + this.size = 5 + Math.random() * 15 + this.speed = 1 + Math.random() * 3 + this.color = color + this.rotation = Math.random() * Math.PI * 2 + this.rotationSpeed = (Math.random() - 0.5) * 0.1 + } + + update() { + this.y -= this.speed + this.rotation += this.rotationSpeed + } + + draw(ctx: CanvasRenderingContext2D) { + ctx.save() + ctx.translate(this.x, this.y) + ctx.rotate(this.rotation) + + // 绘制音符 + ctx.fillStyle = this.color + ctx.beginPath() + + // 音符头部 + ctx.ellipse(0, 0, this.size, this.size * 0.8, 0, 0, Math.PI * 2) + ctx.fill() + + // 音符尾部 + ctx.fillRect(this.size - 2, -this.size * 0.8, 2, -this.size * 2) + + ctx.restore() + } + + isOffScreen() { + return this.y < -this.size * 3 + } + } + + // 创建音符 + const notes: Note[] = [] + const currentGame = rhythmGames[activeGame] + const noteColor = currentGame.color + + // 动画循环 + let animationId: number + let lastTime = 0 + let noteInterval = 0 + + const animate = (timestamp: number) => { + if (!isPlaying) return + + const deltaTime = timestamp - lastTime + lastTime = timestamp + + // 每隔一段时间添加新音符 + noteInterval += deltaTime + if (noteInterval > 200) { + notes.push(new Note(canvas.width / window.devicePixelRatio, canvas.height / window.devicePixelRatio, noteColor)) + noteInterval = 0 + } + + // 清空画布 + ctx.clearRect(0, 0, canvas.width / window.devicePixelRatio, canvas.height / window.devicePixelRatio) + + // 更新和绘制音符 + for (let i = notes.length - 1; i >= 0; i--) { + notes[i].update() + notes[i].draw(ctx) + + // 移除离开屏幕的音符 + if (notes[i].isOffScreen()) { + notes.splice(i, 1) + } + } + + animationId = requestAnimationFrame(animate) + } + + // 开始动画 + if (isPlaying) { + animationId = requestAnimationFrame(animate) + } + + return () => { + window.removeEventListener("resize", setCanvasSize) + cancelAnimationFrame(animationId) + } + }, [isInView, isPlaying, activeGame]) + + // GSAP动画 + useEffect(() => { + if (!isInView) return + + const loadGsap = async () => { + try { + const gsapModule = await import("gsap") + const ScrollTriggerModule = await import("gsap/ScrollTrigger") + + const gsap = gsapModule.default + const ScrollTrigger = ScrollTriggerModule.default + + // 注册ScrollTrigger插件 + gsap.registerPlugin(ScrollTrigger) + + if (!containerRef.current) return + + const container = containerRef.current + const titleElements = container.querySelectorAll(".section-title") + const iconElements = container.querySelectorAll(".icon-item") + const gameCards = container.querySelectorAll(".game-card") + + // 标题动画 + gsap.fromTo( + titleElements, + {y: 30, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + stagger: 0.2, + ease: "power3.out", + scrollTrigger: { + trigger: container, + start: "top 70%", + }, + }, + ) + + // 图标动画 + gsap.fromTo( + iconElements, + {scale: 0.5, opacity: 0}, + { + scale: 1, + opacity: 1, + duration: 0.6, + stagger: 0.1, + ease: "back.out(1.7)", + scrollTrigger: { + trigger: container, + start: "top 70%", + }, + }, + ) + + // 游戏卡片动画 + gsap.fromTo( + gameCards, + {y: 50, opacity: 0}, + { + y: 0, + opacity: 1, + duration: 0.8, + stagger: 0.2, + ease: "power3.out", + scrollTrigger: { + trigger: container, + start: "top 70%", + }, + }, + ) + + return () => { + // 清理动画 + ScrollTrigger.getAll().forEach((trigger) => trigger.kill()) + } + } catch (error) { + console.error("Failed to load GSAP:", error) + } + } + + loadGsap() + }, [isInView]) + + return ( +
+ {/* 背景效果 */} +
+
+
+ +
+ +
+

音乐游戏

+
+ + + + 我的音游之旅 + + + + 点击游戏卡片,体验互动音符动画效果 + + + {/* 图标展示 */} +
+
+
+ +
+ 音乐 +
+
+
+ +
+ 游戏 +
+
+
+ +
+ 技巧 +
+
+
+ +
+ 成就 +
+
+ + {/* 交互式音符动画画布 */} +
+ + + {!isPlaying && ( +
+

点击下方游戏卡片,激活音符动画

+
+ )} +
+ + {/* 游戏卡片 */} +
+ {rhythmGames.map((game, index) => ( + { + setActiveGame(index) + setIsPlaying(true) + }} + > + {/* 背景装饰 */} +
+ +
+
+ {game.name} +
+ +
+

{game.name}

+
+ + {game.level} +
+
+
+ +

{game.description}

+ + {/* 成就进度条 */} +
+ {game.achievements.map((achievement, i) => ( +
+
+ {achievement} +
+
+ +
+
+ ))} +
+ + {/* 点击提示 */} +
点击激活
+ + ))} +
+ + {/* 底部CTA */} + +

喜欢音乐游戏?一起来切磋技艺吧!

+ +
+
+
+ ) +} diff --git a/src/components/sections/skill-tree.tsx b/src/components/sections/skill-tree.tsx new file mode 100644 index 0000000..61249b3 --- /dev/null +++ b/src/components/sections/skill-tree.tsx @@ -0,0 +1,328 @@ +"use client" + +import {useRef, useEffect, useState} from "react" +import {motion} from "framer-motion" +import {useTheme} from "next-themes" + +export default function GsapSkillsTree() { + const containerRef = useRef(null) + const canvasRef = useRef(null) + const {theme} = useTheme() + const [activeSkill, setActiveSkill] = useState(null) + const [isInView, setIsInView] = useState(false) + + // 技能数据 + const skills = [ + // Web开发技能 + {name: "React", level: 95, group: "web", color: "#61DAFB"}, + {name: "Next.js", level: 90, group: "web", color: "#000000"}, + {name: "Vue.js", level: 85, group: "web", color: "#4FC08D"}, + {name: "TypeScript", level: 90, group: "web", color: "#3178C6"}, + {name: "JavaScript", level: 95, group: "web", color: "#F7DF1E"}, + {name: "HTML/CSS", level: 95, group: "web", color: "#E34F26"}, + + // 后端开发技能 + {name: "Spring Boot", level: 60, group: "backend", color: "#6DB33F"}, + {name: "Spring Security", level: 60, group: "backend", color: "#6DB33F"}, + {name: "Java", level: 60, group: "backend", color: "#007396"}, + {name: "Node.js", level: 75, group: "backend", color: "#339933"}, + {name: "MySQL", level: 70, group: "backend", color: "#4479A1"}, + {name: "PostgreSQL", level: 65, group: "backend", color: "#336791"}, + + // Android开发技能 + {name: "Kotlin", level: 70, group: "android", color: "#7F52FF"}, + {name: "Jetpack Compose", level: 65, group: "android", color: "#4285F4"}, + {name: "Kotlin Multiplatform", level: 60, group: "android", color: "#7F52FF"}, + {name: "Android SDK", level: 75, group: "android", color: "#3DDC84"}, + ] + + // 技能分组 + const skillGroups = [ + {id: "web", name: "Web开发", color: "from-blue-500 to-violet-500"}, + {id: "backend", name: "后端开发", color: "from-green-500 to-emerald-500"}, + {id: "android", name: "Android开发", color: "from-amber-500 to-orange-500"}, + ] + + useEffect(() => { + // 检测元素是否在视口中 + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsInView(true) + } + }, + {threshold: 0.2}, + ) + + if (containerRef.current) { + observer.observe(containerRef.current) + } + + return () => { + if (containerRef.current) { + observer.unobserve(containerRef.current) + } + } + }, []) + + useEffect(() => { + if (!isInView) return + + // 动态导入GSAP以避免SSR问题 + const loadGsap = async () => { + try { + const gsapModule = await import("gsap") + const ScrollTriggerModule = await import("gsap/ScrollTrigger") + + const gsap = gsapModule.default + const ScrollTrigger = ScrollTriggerModule.default + + // 注册ScrollTrigger插件 + gsap.registerPlugin(ScrollTrigger) + + if (!containerRef.current || !canvasRef.current) return + + const container = containerRef.current + const canvas = canvasRef.current + const ctx = canvas.getContext("2d") + if (!ctx) return + + // 设置canvas尺寸 + const setCanvasSize = () => { + const rect = canvas.getBoundingClientRect() + canvas.width = rect.width * window.devicePixelRatio + canvas.height = rect.height * window.devicePixelRatio + ctx.scale(window.devicePixelRatio, window.devicePixelRatio) + } + + setCanvasSize() + window.addEventListener("resize", setCanvasSize) + + // 创建3D技能球体 + const skillSphereRadius = Math.min(canvas.width, canvas.height) * 0.3 + const center = {x: canvas.width / 2, y: canvas.height / 2} + + // 为每个技能分配3D位置 + const skillNodes = skills.map((skill, index) => { + const phi = Math.acos(-1 + (2 * index) / skills.length) + const theta = Math.sqrt(skills.length * Math.PI) * phi + + return { + ...skill, + x3d: skillSphereRadius * Math.cos(theta) * Math.sin(phi), + y3d: skillSphereRadius * Math.sin(theta) * Math.sin(phi), + z3d: skillSphereRadius * Math.cos(phi), + x2d: 0, + y2d: 0, + scale: 0, + opacity: 0, + } + }) + + // 动画参数 + const rotation = {x: 0, y: 0} + const targetRotation = {x: 0, y: 0} + let autoRotate = true + let mouseX = 0 + let mouseY = 0 + + // 鼠标移动事件 + const handleMouseMove = (e: MouseEvent) => { + const rect = canvas.getBoundingClientRect() + mouseX = ((e.clientX - rect.left) / rect.width - 0.5) * 2 + mouseY = ((e.clientY - rect.top) / rect.height - 0.5) * 2 + + if (Math.abs(mouseX) > 0.1 || Math.abs(mouseY) > 0.1) { + autoRotate = false + targetRotation.x = mouseY * 0.5 + targetRotation.y = mouseX * 0.5 + + // 5秒后恢复自动旋转 + clearTimeout(autoRotateTimeout) + autoRotateTimeout = setTimeout(() => { + autoRotate = true + }, 5000) + } + } + + let autoRotateTimeout: NodeJS.Timeout + canvas.addEventListener("mousemove", handleMouseMove) + + // 绘制函数 + const draw = () => { + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // 更新旋转 + if (autoRotate) { + targetRotation.y += 0.003 + } + + rotation.x += (targetRotation.x - rotation.x) * 0.05 + rotation.y += (targetRotation.y - rotation.y) * 0.05 + + // 计算3D旋转 + const cosX = Math.cos(rotation.x) + const sinX = Math.sin(rotation.x) + const cosY = Math.cos(rotation.y) + const sinY = Math.sin(rotation.y) + + // 更新每个技能节点的位置 + skillNodes.forEach((node) => { + // 应用旋转变换 + const x = node.x3d + const y = node.y3d * cosX - node.z3d * sinX + const z = node.y3d * sinX + node.z3d * cosX + + const x2 = x * cosY - z * sinY + const z2 = x * sinY + z * cosY + + // 投影到2D + const scale = (z2 / skillSphereRadius + 2) / 3 + node.x2d = center.x + x2 / window.devicePixelRatio + node.y2d = center.y + y / window.devicePixelRatio + node.scale = scale + node.opacity = (scale - 0.6) * 2.5 + + // 绘制技能节点 + if (node.opacity > 0) { + ctx.save() + ctx.globalAlpha = node.opacity + + // 节点大小基于技能等级 + const nodeSize = (10 + node.level / 10) * scale + + // 绘制节点 + ctx.fillStyle = node.color + ctx.beginPath() + ctx.arc(node.x2d, node.y2d, nodeSize, 0, Math.PI * 2) + ctx.fill() + + // 绘制技能名称 + ctx.font = `${12 * scale}px Arial` + ctx.fillStyle = theme === "dark" ? "#ffffff" : "#000000" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText(node.name, node.x2d, node.y2d) + + ctx.restore() + } + }) + + requestAnimationFrame(draw) + } + + // 开始动画 + draw() + + // 清理函数 + return () => { + window.removeEventListener("resize", setCanvasSize) + canvas.removeEventListener("mousemove", handleMouseMove) + clearTimeout(autoRotateTimeout) + } + } catch (error) { + console.error("Failed to load GSAP:", error) + } + } + + loadGsap() + }, [isInView, theme]) + + return ( +
+ {/* 背景效果 */} +
+
+
+ +
+ +
+

专业技能

+
+ + + + 我的技术栈和能力 + + + + 探索我的技能宇宙 - 移动鼠标与技能球体互动,查看我的专业技能和熟练程度 + + + {/* 3D技能球体 */} +
+ + + {/* 技能说明 */} +
+

{activeSkill ? `${activeSkill}` : "移动鼠标探索技能球体"}

+
+
+ + {/* 技能分组说明 */} +
+ {skillGroups.map((group, idx) => ( + +
+

{group.name}

+
+ {skills + .filter((skill) => skill.group === group.id) + .slice(0, 4) + .map((skill) => ( +
+ {skill.name} +
+
+
+
+ {skill.level}% +
+
+ ))} + {skills.filter((skill) => skill.group === group.id).length > 4 && ( +
+ +{skills.filter((skill) => skill.group === group.id).length - 4} 更多技能 +
+ )} +
+ + ))} +
+
+
+ ) +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 0c67d3e..b382e74 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -70,12 +70,31 @@ const config = { transform: "translate(calc(-50% - 0.5rem))", }, }, - + float: { + "0%, 100%": { + transform: "translateY(0) scale(1)", + }, + "50%": { + transform: "translateY(-20px) scale(1.5)", + }, + }, + wave: { + "0%": {transform: "rotate(0.0deg)"}, + "10%": {transform: "rotate(14.0deg)"}, + "20%": {transform: "rotate(-8.0deg)"}, + "30%": {transform: "rotate(14.0deg)"}, + "40%": {transform: "rotate(-4.0deg)"}, + "50%": {transform: "rotate(10.0deg)"}, + "60%": {transform: "rotate(0.0deg)"}, + "100%": {transform: "rotate(0.0deg)"}, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", scroll: "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite", + float: "float 6s ease-in-out infinite", + wave: "wave 2.5s ease-in-out infinite", }, }, },