๐ React ์นด๋ ์ธํฐ๋์ ์ ๋๋ฉ์ด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์นด๋ ๋ทฐ์ ์ธํฐ๋์ ์ ๋๋ฉ์ด์ ์ ์ ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ๋ ๋ง๋ค์ด ๋ดค์ต๋๋ค.
https://www.youtube.com/watch?v=YDCCauu4lIk
์์ ์ ๋ณธ ์ฝ๋ฉ ์ ํ๋์ ์์์ ๊ธฐ์ต์ ๋ด์๋๋ค ์ฌ์ฌํ๋ ์ฐจ์ ๋ฆฌ์กํธ ํจํค์ง๋ก ์ฌ๊ตฌ์ฑํ์ต๋๋ค.
๐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋ณด
๐ install
$ npm i @lasbe/react-card-animation
๐ example
import { CardAnimation } from '@lasbe/react-card-animation';
export default function App() {
return (
<div>
<CardAnimation>
<div className={`w-[300px] h-[200px]`}>...</div>
</CardAnimation>
<CardAnimation angle={10}>
<div className={`w-[300px] h-[200px]`}>...</div>
</CardAnimation>
</div>
);
}
์ ์ฉํ ์ปจํ
์ด๋ ๋ฐ๊นฅ์ CardAnimation
์ ๊ฐ์์ค๋๋ค.
๐ Props
๐ ์ฝ๋
'use client';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import './react-card-animation.css';
type CardAnimationType = {
children: React.ReactElement;
angle?: number;
};
type MousePositionType = {
x: null | number;
y: null | number;
};
export const CardAnimation = ({ children, angle = 30 }: CardAnimationType) => {
const [mousePosition, setMousePosition] = useState<MousePositionType>({
x: null,
y: null,
});
const [isInMouse, setIsInMouse] = useState(false);
const mousePositionRef = useRef<MousePositionType>({ x: null, y: null });
const requestRef = useRef<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const containerWidth = containerRef?.current?.offsetWidth;
const containerHeight = containerRef?.current?.offsetHeight;
const mouseX = e.nativeEvent.offsetX; // ๋ง์ฐ์ค X ์ขํ
const mouseY = e.nativeEvent.offsetY; // ๋ง์ฐ์ค Y ์ขํ
document.body?.style.setProperty('--card-animation-lighting-x', `${mouseX}px`);
document.body?.style.setProperty('--card-animation-lighting-y', `${mouseY}px`);
const xRatio = mouseX / (containerWidth ?? 1);
const yRatio = mouseY / (containerHeight ?? 1);
const ratio = angle;
const xValue = ratio - xRatio * (ratio * 2);
const yValue = ratio - yRatio * (ratio * 2);
mousePositionRef.current = { x: -yValue, y: xValue };
setIsInMouse(true);
},
[angle],
);
const handleMouseOut = useCallback(() => {
setTimeout(() => {
setIsInMouse(false);
}, 500);
}, []);
const loop = useCallback(() => {
setMousePosition(mousePositionRef.current);
requestRef.current = requestAnimationFrame(loop);
}, []);
useEffect(() => {
requestRef.current = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(requestRef.current ?? 0);
document.body.style.removeProperty('--card-animation-lighting-x');
document.body.style.removeProperty('--card-animation-lighting-y');
};
}, [loop]);
return React.cloneElement(children, {
ref: containerRef,
onMouseMove: handleMouseMove,
onMouseOut: handleMouseOut,
style: {
...children.props.style,
transform: `perspective(800px) rotateX(${isInMouse ? mousePosition.x : 0}deg) rotateY(${isInMouse ? mousePosition.y : 0}deg)`,
},
className: `${children.props.className ?? ''} card-animation`,
});
};
.card-animation {
transition: 0.25s cubic-bezier(0.08, 0.59, 0.29, 0.99);
transform-style: preserve-3d;
}
.card-animation::after,
.card-animation::before {
content: '';
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
border-radius: inherit;
z-index: 1;
transition: opacity 0.25s;
}
.card-animation::before {
background: radial-gradient(800px circle at 30% 30%, rgba(255, 255, 255, 0.25), transparent 40%);
}
.card-animation:hover::before {
opacity: 0;
}
.card-animation::after {
opacity: 0;
background: radial-gradient(
500px circle at var(--card-animation-lighting-x) var(--card-animation-lighting-y),
rgba(255, 255, 255, 0.4),
transparent 40%
);
}
.card-animation:hover::after {
opacity: 1;
}
๐ ์ต์ ํ
onMouseMove
์ด๋ฒคํธ๋ก ์ธํ ์ฆ์ ์ํ ๋ณํ๋ฅผ ์ค์ด๊ธฐ ์ํด requestAnimationFrame
๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
ํ๋ ์์ ์ฃผ์ฌ์จ์ ๋ง์ถฐ ์ฝ๋ฐฑ์ ํธ์ถํ๊ธฐ์ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ ํจ๊ณผ์ ํจ์จ์ ํจ๊ป ์ฑ๊ธธ ์ ์์์ต๋๋ค.
๐ ๊ด์ํจ๊ณผ
์ ์ ํ๋ธ ์์๊ณผ๋ ๋ค๋ฅด๊ฒ ์นด๋ ๋ทฐ์ ์ด๋ฏธ์ง๋ฅผ ์ ์ฉํ์ง ์๊ณ ์์์ ๋ฃ์ ์ฉ๋๋ก ๋ง๋ค์๊ธฐ ๋๋ฌธ์ css์ filter ์ต์ ์ ์ฌ์ฉํ๊ธฐ์ ๋ค์๊ณผ ๊ฐ์ ์ ์ฝ์ด ์์์ต๋๋ค.
- ์นด๋ ๋ ์ด์ด๋ฅผ ์๋จ, ๊ด์ ํจ๊ณผ๋ฅผ ํ๋จ ๋ ์ด์ด๋ก ๋ฐฐ์น
์นด๋ ๋ ์ด์ด์ ๋ฐฐ๊ฒฝ ์์ ์ ์ฉํ ๊ฒฝ์ฐ ๊ด์ ํจ๊ณผ๊ฐ ๋ณด์ด์ง ์์ - ์นด๋ ๋ ์ด์ด๋ฅผ ํ๋จ, ๊ด์ ํจ๊ณผ๋ฅผ ์๋จ ๋ ์ด์ด๋ก ๋ฐฐ์น
- ๋ฐฐ๊ฒฝ์ ๊ด์ ํจ๊ณผ๊ฐ ํฉ์ฑ๋จ
- ๋ง์ฐ์ค ์ขํ ๊ฐ์ ๋ฐ๋ฅธ ์์ง์ ์ฐจ์ด๊ฐ ์ฌํด์ง
- React.cloneElement ์ฌ์ฉ์ ์ ์ฝ์ด ์๊น
๊ทธ๋์ body์ css ์ ์ญ ๊ฐ(var(--card-animation-lighting-x)
)์ ๊ณ์ํด์ ์์ ํ๊ณ ๊ฐ์ ์ ํ์๋ฅผ ํตํด ๊ทธ๋๋์ธํธ์ ์์น๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ์์ผ๋ก ํํํ๊ฒ ๋์์ต๋๋ค.
'Project' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ํฌํธํด๋ฆฌ์ค (0) | 2024.10.18 |
---|---|
Nextjs ํ ์ดํ๋ก์ ํธ, ์์ฑ์ฌ ์๋ฆฌ ํ๊ฐ GPT (0) | 2024.10.10 |
[React] ์ธ์ ์ฌ์ด ๋ฆฌ์กํธ ๋ชจ๋ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (0) | 2024.05.07 |
[React] ๋ฆฌ์กํธ ์คํฌ๋กค ์ ๋๋ฉ์ด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ฐ๋ฐ (0) | 2024.03.13 |
[React, ์ฌ์ด๋ ํ๋ก์ ํธ] ๋ง๋ชจ(LINGMO), ๋น์ ์ ๋งํฌ ๋ชจ์ (0) | 2023.08.21 |
๋๊ธ