์ ๋๋ฉ์ด์ ๊ฐ์ ์ํ(state)์ ์๋(velocity)๋ฅผ ์ถ์
๋ชจ๋ ๋ชจ์
์ปดํฌ๋ํธ๋ ๋ด๋ถ์ ์ผ๋ก MotionValues
๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋๋ฉ์ด์
๊ฐ์ ์ํ์ ์๋๋ฅผ ์ถ์
import { useMotionValue } from "framer-motion";
function App() {
const x = useMotionValue(0);
return (
<Wrapper>
<Box style={{x}} drag="x" dragSnapToOrigin/>
</Wrapper>
);
}
์ด์ useMotionValue
์ x๊ฐ ์ด style์ x์ ์ฐ๊ฒฐ๋์๋ค.
์ฆ, style์ x์ขํ๊ฐ ๋ฐ๋ ๋๋ง๋ค, ์ด MotionValue
์ญ์ ์
๋ฐ์ดํธ ๋ ๊ฒ์!
โญ MotionValue
๋ React State๊ฐ ์๋๊ธฐ ๋๋ฌธ์ Motion Value๊ฐ์ด ๋ฐ๋์ด๋ ๋ฆฌ๋ ๋๋ง์ด ์ผ์ด๋์ง ์๋๋ค.โญ
โ ๊ทธ๋์ ์ฝ์์๋ ๋ฑ ํ ๋ฒ๋ง ์ฐํ!
์ฝ์์ x๊ฐ ๋ณํํ ๋๋ง๋ค ๊ฐ์ด ์ฐํ๊ฒ ํ๊ณ ์ถ์ผ๋ฉด
function App() {
const x = useMotionValue(0);
useEffect(()=> {x.onChange(()=>console.log(x))}, [x]);
return (
<Wrapper>
<Box style={{x}} drag="x" dragSnapToOrigin/>
</Wrapper>
);
}
์ด๋ ๊ฒ useEffect๋ฅผ ์ด์ฉํ๋ฉด ๋๋ค.
x๊ฐ ์ง์ ์ค์ ๋ ๊ฐ๋ฅํ๋ค.
function App() {
const x = useMotionValue(0);
return (
<Wrapper>
<button onClick={()=> x.set(200)}>Click Me</button>
<Box style={{x}} drag="x" dragSnapToOrigin/>
</Wrapper>
);
}
useTransform
-> ์ฒซ๋ฒ์งธ ์ธ์: ๊ฐ
-> ๋๋ฒ์งธ ์ธ์: input
-> ์ธ๋ฒ์งธ ์ธ์: output
input๊ณผ output์ ๋ฐ๋์ ๊ฐ์ ๋ฐฐ์ด ํฌ๊ธฐ์ฌ์ผํจ!
function App() {
const x = useMotionValue(0);
const scale = useTransform(x, [-800, 0, 800], [2,1,0.1]);
return (
<Wrapper>
<button onClick={()=> x.set(200)}>Click Me</button>
<Box style={{x}} drag="x" dragSnapToOrigin/>
</Wrapper>
);
}
์ฆ, ์ ์ฝ๋์์๋ x๊ฐ -800์ผ ๋ 2๋ฅผ, 0์ผ ๋ 1์, 800์ผ ๋ 0.1์ ์ป๊ฒ ๋๋ค.
๋ทฐํฌํธ๊ฐ ์คํฌ๋กค๋ ๋ ์ ๋ฐ์ดํธ๋๋ MotionValues๋ฅผ ๋ฆฌํด
โimport useScroll from "framer-motion";
- scrollX: ์ค์ ์ํ ์คํฌ๋กค ํฝ์
- scrollY: ์ค์ ์์ง ์คํฌ๋กค ํฝ์
- scrollXProgress : 0 ~ 1 ์ฌ์ด์ ์ํ ์คํฌ๋กค
- scrollYProgress : 0 ~ 1 ์ฌ์ด์ ์์ง ์คํฌ๋กค (๊ฐ์ฅ ์๋จ 0, ๊ฐ์ฅ ํ๋จ 1)
function App() {
const x = useMotionValue(0);
const rotateZ = useTransform(x, [-800, 800], [-360, 360]);
const gradient = useTransform(
x,
[-800, 800],
[
"linear-gradient(135deg, rgb(0, 210, 238), rgb(0, 83, 238))",
"linear-gradient(135deg, rgb(0, 238, 155), rgb(238, 178, 0))",
]
);
const { scrollYProgress } = useScroll();
const scale = useTransform(scrollYProgress, [0, 1], [1, 5]);
return (
<Wrapper style={{backgroundColor: gradient}}>
<Box style={{x, rotateZ, scale }} drag="x" dragSnapToOrigin/>
</Wrapper>
);
}
- Line drawing
: svg ์๋ฆฌ๋จผํธ์ 'pathLength', 'pathSpacing', 'pathOffset' ์์ฑ์ ์ฌ์ฉํ์ฌ Line drawing ์ ๋๋ฉ์ด์ ์ ๋ง๋ค ์ ์์- path (SVG)
: ๋ชจ์์ ์ ์ํ๋ ์ผ๋ฐ ์๋ฆฌ๋จผํธ.
๋ชจ๋ ๊ธฐ๋ณธ ๋ชจ์์ path ์๋ฆฌ๋จผํธ๋ก ๋ง๋ค ์ ์์. path์ ์์ฑ d๋ ๊ฒฝ๋ก์ ๋ชจ์์ ์ ์.- Path
: motion.path ์ปดํฌ๋ํธ๋ ์ธ ๊ฐ์ง ๊ฐ๋ ฅํ SVG path ์์ฑ์ธ pathLength, pathSpacing ๋ฐ pathOffset์ ๊ฐ์ง. ์๋ ๊ฒฝ๋ก ์ธก์ ์ด ํ์ ์์ด ๋ชจ๋ 0๊ณผ 1 ์ฌ์ด์ ๊ฐ์ผ๋ก ์ค์ .- Line drawing
: pathLength, pathSpacing ๋ฐ pathOffset์ ์ธ ๊ฐ์ง ํน์ ์์ฑ์ ์ฌ์ฉํ์ฌ ๋ง์ SVG ์์๋ก ๋ง๋ค ์ ์์.
airbnb์ ๋ก๊ณ ๋ฅผ ๊ฐ์ ธ๋ค๊ฐ ์ด์ฉํด๋ณด์
(https://fontawesome.com/v5/icons/airbnb?style=brands&s=solid&f=brands)
const Svg = styled.svg`
width: 300px;
height: 300px;
path {
stroke: white;
stroke-width: 2;
}
`;
const svg = {
start: { pathLength: 0, fill: "rgba(255, 255, 255, 0)" },
end: {
fill: "rgba(255, 255, 255, 1)",
pathLength: 1,
},
};
function App() {
return (
<Wrapper>
<Svg
focusable="false"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
>
<motion.path
variants={svg}
initial="start"
animate="end"
transition={{
default: { duration: 5 },
fill: { duration: 1, delay: 3 },
}}
d="M224 373.12c-25.24-31.67-40.08-59.43-45-83.18-22.55-88 112.61-88 90.06 0-5.45 24.25-20.29 52-45 83.18zm138.15 73.23c-42.06 18.31-83.67-10.88-119.3-50.47 103.9-130.07 46.11-200-18.85-200-54.92 0-85.16 46.51-73.28 100.5 6.93 29.19 25.23 62.39 54.43 99.5-32.53 36.05-60.55 52.69-85.15 54.92-50 7.43-89.11-41.06-71.3-91.09 15.1-39.16 111.72-231.18 115.87-241.56 15.75-30.07 25.56-57.4 59.38-57.4 32.34 0 43.4 25.94 60.37 59.87 36 70.62 89.35 177.48 114.84 239.09 13.17 33.07-1.37 71.29-37.01 86.64zm47-136.12C280.27 35.93 273.13 32 224 32c-45.52 0-64.87 31.67-84.66 72.79C33.18 317.1 22.89 347.19 22 349.81-3.22 419.14 48.74 480 111.63 480c21.71 0 60.61-6.06 112.37-62.4 58.68 63.78 101.26 62.4 112.37 62.4 62.89.05 114.85-60.86 89.61-130.19.02-3.89-16.82-38.9-16.82-39.58z"
></motion.path>
</Svg>
</Wrapper>
);
}
์ด๋ ๊ฒ ํด์ฃผ๋ฉด~!
์์ฐ ๊ฑ ์ ๊ธฐํด์ ๋ฃ์ด๋ดค๋ค.
React ํธ๋ฆฌ์์ ์ปดํฌ๋ํธ๊ฐ ์ ๊ฑฐ๋ ๋ ์ ๊ฑฐ๋๋ ์ปดํฌ๋ํธ์ ์ ๋๋ฉ์ด์ ํจ๊ณผ๋ฅผ ์ค ์ ์์.
React์๋ ์๋ช ์ฃผ๊ธฐ ๋ฉ์๋๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ข ๋ฃ ์ ๋๋ฉ์ด์ ์ ํ์ฑํํด์ผํจ!
AnimatePresence์ ๋จ์ผ ์์ key๋ฅผ ๋ณ๊ฒฝํ์ฌ ์ฌ๋ผ์ด๋์ผ(์ฌ๋ผ์ด๋)์ ๊ฐ์ ์ปดํฌ๋ํธ๋ฅผ ์ฝ๊ฒ ๋ง๋ค ์ ์๋ค.
AnimatePresence
๋ visible ์ํ์ฌ์ผํ๋ค.AnimatePresence
์ ๋ด๋ถ์๋ ์กฐ๊ฑด๋ฌธ์ด ์์ด์ผ ํ๋ค.const boxVariants = {
initial: {
opacity: 0,
scale: 0,
},
visible: {
opacity: 1,
scale: 1,
rotateZ: 360,
},
leaving: {
opacity: 0,
scale: 0,
y: 50,
},
};
function App() {
const [showing, setShowing] = useState(false);
const toggleShowing = () => setShowing((prev) => !prev);
return (
<Wrapper>
<button onClick={toggleShowing}>Click</button>
<AnimatePresence>
{showing ? (
<Box
variants={boxVariants}
initial="initial"
animate="visible"
exit="leaving"
/>
) : null}
</AnimatePresence>
</Wrapper>
);
}
function App() {
const [visible, setVisible] = useState(1);
const nextPlease = () => setVisible((prev) => (prev === 10 ? 10 : prev + 1));
const prevPlease = () => setVisible((prev) => (prev === 1 ? 1 : prev - 1));
return (
<Wrapper>
<AnimatePresence>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) =>
i === visible ? (
<Box
variants={box}
initial="invisible"
animate="visible"
exit="exit"
key={i}
>
{i}
</Box>
) : null
)}
</AnimatePresence>
<button onClick={nextPlease}>next</button>
<button onClick={prevPlease}>prev</button>
</Wrapper>
);
}
<AnimatePresence>
<Box
variants={box}
initial="invisible"
animate="visible"
exit="exit"
key={visible}
>
{visible}
</Box>
</AnimatePresence>
์ด๋ ๊ฒ ํด์ค๋ ๋๊ฐ์ด ์๋ํ๋ค.
const nextPlease = () => setVisible((prev) => (prev === 10 ? 10 : prev + 1));
const prevPlease = () => setVisible((prev) => (prev === 1 ? 1 : prev - 1));
์ด๋ฏธ ์ด๋ ๊ฒ ํด์คฌ๊ธฐ ๋๋ฌธ!
variants์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ์ ์๊ฒ ํด์ฃผ๋ prop๋ก ๊ฐ ์ ๋๋ฉ์ด์ ์ปดํฌ๋ํธ์ ๋ํด ๋์ variants๋ฅผ ๋ค๋ฅด๊ฒ ์ ์ฉํ ๋ ์ฌ์ฉํ ์ ์๋ ์ฌ์ฉ์ ์ง์ ๋ฐ์ดํฐ
const box = {
entry: (back:boolean)=>({
x: back ? -500 : 500,
opacity: 0,
scale: 0,
}),
center: {
x: 0,
opacity: 1,
scale: 1,
transition: {
duration: 0.3,
}
},
exit: (back:boolean)=>({
x: back ? 500 : -500,
opacity: 0,
scale: 0,
transition: {duration:1 },
}),
};
function App() {
const [visible, setVisible] = useState(1);
const [back, setBack] = useState(false);
const nextPlease = () => {
setBack(false);
setVisible((prev) => (prev === 10 ? 10 : prev + 1));
}
const prevPlease = () => {
setBack(true);
setVisible((prev) => (prev === 1 ? 1 : prev - 1));
}
return (
<Wrapper>
<AnimatePresence custom={back}>
<Box
custom={back}
variants={box}
initial="entry"
animate="center"
exit="exit"
key={visible}
>
{visible}
</Box>
</AnimatePresence>
<button onClick={nextPlease}>next</button>
<button onClick={prevPlease}>prev</button>
</Wrapper>
);
}
์ด๋ฐ ์์ผ๋ก ํด์ฃผ๋ฉด ์ด์ prev ๋ฒํผ์ ๋๋ฅด๋ฉด ์ผ์ชฝ์์๋ถํฐ ๋ํ๋๋ค. ์๋ฐฉํฅ์ผ๋ก ์ ๋๋ฉ์ด์
์ ๋ง๋ ๊ฒ!
next๋ฅผ ํด๋ฆญํ๋ฉด back์ false
๊ฐ ๋๋ค. ๊ทธ๋ฌ๋ฉด variant๋ false
์ธ back์ ๋ฐ๊ฒ ๋๋ค.
prev๋ฅผ ๋๋ฅด๋ฉด back์ true
๊ฐ ๋๊ณ back์ด true
๊ฐ ๋๋ฉด variant๋ custom์ true
๋ฅผ ๋ฐ๋๋ค.
ํ ๋ฒ์ ํ๋์ ์ปดํฌ๋ํธ๋ง ๋ ๋๋งํจ.
exiting์ค์ธ ์ปดํฌ๋ํธ๋ enteringํ๋ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๊ธฐ ์ ์ exit ์ ๋๋ฉ์ด์ ์ ์๋ฃํ๋ค.
<AnimatePresence mode="wait" custom={back}> //mode="wait" ์ถ๊ฐ
<Box
custom={back}
variants={box}
initial="entry"
animate="center"
exit="exit"
key={visible}
>
{visible}
</Box>
</AnimatePresence>
์ด๋ ๊ฒ ํด์ฃผ๋ฉด ์๋์ ๊ฐ์ด ๋๋ค.
ํ๋๊ฐ ์์ ํ ์๋ฃ๋์ด์ผ ๋ค์ ์ ๋๋ฉ์ด์
์ด ์คํ๋๋ค.
<Box>
{!clicked ? (
<Circle layoutId="circle" style={{ borderRadius: 50 }} />
) : null}
</Box>
<Box>
{clicked ? (
<Circle layoutId="circle" style={{ borderRadius: 0, scale: 2 }} />
) : null}
</Box>
๋๊ฐ์ Circle ์ปดํฌ๋ํธ๋ ๊ฐ๊ธฐ ๋ค๋ฅธ ์ปดํฌ๋ํธ์ง๋ง,
layoutId
๋ฅผ ๊ฐ๊ฒ ์ค์ ํด์ค์ผ๋ก์จ ์ด ๋๊ฐ๊ฐ ๊ฐ๋ค๊ณ ์๊ฐํ๊ฒ ๋๋ค.
์ฆ, ์ด๋ ๊ฒ ๊ฐ์ layoutId
๋ฅผ ์ฃผ๋ฉด, ๋๊ฐ์ box๋ค์ Circle์ด ์๋ค๊ฐ๋คํ๋ค..
์ฐ๋ฆฌ๊ฐ ์ถ๊ฐ๋ก scale
๊ณผ borderRadius
๊น์ง ์ฃผ์์ผ๋ฏ๋ก ์๋ค๊ฐ๋คํ๋๋์ ์ด ๋๋ ๋ณํํ๋ค.
function App() {
const [clicked, setClicked] = useState(false);
const toggleClicked = () => setClicked((prev) => !prev);
return (
<Wrapper onClick={toggleClicked}>
<Box>
{!clicked ? (
<Circle layoutId="circle" style={{ borderRadius: 50 }} />
) : null}
</Box>
<Box>
{clicked ? (
<Circle layoutId="circle" style={{ borderRadius: 0, scale: 2 }} />
) : null}
</Box>
</Wrapper>
);
}
function App() {
const [clicked, setClicked] = useState(false);
const toggle = () => setClicked((prev) => !prev);
return (
<Wrapper onClick={toggle}>
<Grid>
<Box layoutId="hello"/>
<Box/>
<Box/>
<Box/>
</Grid>
<AnimatePresence>
{clicked ? (
<Overlay
initial={{backgroundColor: "rgba(0,0,0,0.5)"}}
animate={{backgroundColor: "rgba(0,0,0,1)"}}
exit={{backgroundColor: "rgba(0,0,0,0)"}}
>
<Box layoutId="hello" style={{width: 400, height: 200}}/>
</Overlay>
) : null}
</AnimatePresence>
</Wrapper>
);
}
์ด๊ฒ์ ์ด๋ element๋ฅผ ํด๋ฆญํด๋ ์๋ํ๋๋ก ๋ง๋ค์.
function App() {
const [id, setId] = useState<null|string>(null);
return (
<Wrapper>
<Grid>
{["1","2","3","4"].map(n=>(
<Box onClick={()=> setId(n)} key={n} layoutId={n} />
))}
</Grid>
<AnimatePresence>
{id ? (
<Overlay
onClick={() => setId(null)}
initial={{backgroundColor: "rgba(0,0,0,0)"}}
animate={{backgroundColor: "rgba(0,0,0,0.5)"}}
exit={{backgroundColor: "rgba(0,0,0,0)"}}
>
<Box layoutId={id} style={{width: 400, height: 200}}/>
</Overlay>
) : null}
</AnimatePresence>
</Wrapper>
);
}