feat(ui): add AnimatedTextCycle component and framer-motion
- add src/components/ui/animated-text-cycle.tsx (animated word cycle using framer-motion) - add framer-motion dependency to package.json and update bun.lock (includes motion-dom / motion-utils) - update About link to use YouTube playlist URL - remove unused background images (public/bg.avif, public/bg.png) and update public/char.avif
This commit is contained in:
parent
47cad5423b
commit
ea5262f7ed
7 changed files with 121 additions and 1 deletions
|
|
@ -15,7 +15,7 @@ export default function Home() {
|
|||
<p>최근에는 정보보안 분야 중 웹 해킹에 관심이 많습니다.</p>
|
||||
<br />
|
||||
<p>대표적인 프로젝트들은 아래와 같습니다.</p>
|
||||
<a href="https://www.youtube.com/watch?v=XTdqpdTMZbw&list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD">Effect Playing Contest 2025 Broadcast Develop</a><br />
|
||||
<a href="https://www.youtube.com/playlist?list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD">Effect Playing Contest 2025 Broadcast Develop</a><br />
|
||||
<a href="https://github.com/imnyang/today.isangjeong">today.isangjeong</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
112
src/components/ui/animated-text-cycle.tsx
Normal file
112
src/components/ui/animated-text-cycle.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import * as React from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface AnimatedTextCycleProps {
|
||||
words: string[];
|
||||
interval?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function AnimatedTextCycle({
|
||||
words,
|
||||
interval = 5000,
|
||||
className = "",
|
||||
}: AnimatedTextCycleProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [width, setWidth] = useState("auto");
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get the width of the current word
|
||||
useEffect(() => {
|
||||
if (measureRef.current) {
|
||||
const elements = measureRef.current.children;
|
||||
if (elements.length > currentIndex) {
|
||||
// Add a small buffer (10px) to prevent text wrapping
|
||||
const newWidth = elements[currentIndex].getBoundingClientRect().width;
|
||||
setWidth(`${newWidth}px`);
|
||||
}
|
||||
}
|
||||
}, [currentIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % words.length);
|
||||
}, interval);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [interval, words.length]);
|
||||
|
||||
// Container animation for the whole word
|
||||
const containerVariants = {
|
||||
hidden: {
|
||||
y: -20,
|
||||
opacity: 0,
|
||||
filter: "blur(8px)"
|
||||
},
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
filter: "blur(0px)",
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: "easeOut"
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
y: 20,
|
||||
opacity: 0,
|
||||
filter: "blur(8px)",
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: "easeIn"
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hidden measurement div with all words rendered */}
|
||||
<div
|
||||
ref={measureRef}
|
||||
aria-hidden="true"
|
||||
className="absolute opacity-0 pointer-events-none"
|
||||
style={{ visibility: "hidden" }}
|
||||
>
|
||||
{words.map((word, i) => (
|
||||
<span key={i} className={`font-bold ${className}`}>
|
||||
{word}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Visible animated word */}
|
||||
<motion.span
|
||||
className="relative inline-block"
|
||||
animate={{
|
||||
width,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 150,
|
||||
damping: 15,
|
||||
mass: 1.2,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.span
|
||||
key={currentIndex}
|
||||
className={`inline-block font-bold ${className}`}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{words[currentIndex]}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</motion.span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue