+
{/* 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 应用。
+ 我相信良好的用户体验和干净的代码同样重要。
+
+
+
+
+
+
+
+

+ {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.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.description}
+
+
+ {project.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+ {/* 项目序号 */}
+
+ {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.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) => (
+
+ ))}
+ {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",
},
},
},