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
7
bun.lock
7
bun.lock
|
|
@ -36,6 +36,7 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next": "16.0.0",
|
"next": "16.0.0",
|
||||||
|
|
@ -417,6 +418,8 @@
|
||||||
|
|
||||||
"fast-equals": ["fast-equals@5.3.2", "", {}, "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ=="],
|
"fast-equals": ["fast-equals@5.3.2", "", {}, "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ=="],
|
||||||
|
|
||||||
|
"framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="],
|
||||||
|
|
||||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
@ -461,6 +464,10 @@
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||||
|
|
||||||
|
"motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
|
||||||
|
|
||||||
|
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"next": ["next@16.0.0", "", { "dependencies": { "@next/env": "16.0.0", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.0", "@next/swc-darwin-x64": "16.0.0", "@next/swc-linux-arm64-gnu": "16.0.0", "@next/swc-linux-arm64-musl": "16.0.0", "@next/swc-linux-x64-gnu": "16.0.0", "@next/swc-linux-x64-musl": "16.0.0", "@next/swc-win32-arm64-msvc": "16.0.0", "@next/swc-win32-x64-msvc": "16.0.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg=="],
|
"next": ["next@16.0.0", "", { "dependencies": { "@next/env": "16.0.0", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.0", "@next/swc-darwin-x64": "16.0.0", "@next/swc-linux-arm64-gnu": "16.0.0", "@next/swc-linux-arm64-musl": "16.0.0", "@next/swc-linux-x64-gnu": "16.0.0", "@next/swc-linux-x64-musl": "16.0.0", "@next/swc-win32-arm64-msvc": "16.0.0", "@next/swc-win32-x64-msvc": "16.0.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg=="],
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next": "16.0.0",
|
"next": "16.0.0",
|
||||||
|
|
|
||||||
BIN
public/bg.avif
BIN
public/bg.avif
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
BIN
public/bg.png
BIN
public/bg.png
Binary file not shown.
|
Before Width: | Height: | Size: 333 KiB |
BIN
public/char.avif
BIN
public/char.avif
Binary file not shown.
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 251 KiB |
|
|
@ -15,7 +15,7 @@ export default function Home() {
|
||||||
<p>최근에는 정보보안 분야 중 웹 해킹에 관심이 많습니다.</p>
|
<p>최근에는 정보보안 분야 중 웹 해킹에 관심이 많습니다.</p>
|
||||||
<br />
|
<br />
|
||||||
<p>대표적인 프로젝트들은 아래와 같습니다.</p>
|
<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>
|
<a href="https://github.com/imnyang/today.isangjeong">today.isangjeong</a>
|
||||||
</div>
|
</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