ReactJS 애니메이션 라이브러리
npm install framer-motion
<motion.div></motion.div> ⭕️
const Box = styled(motion.div)`
`;
애니메이션의 초기값 지정
<Box initial={{scale:0}} />
<Box transition={{type: 'spring'}} initial={{scale:0}} animate={{scale:1, rotateZ: 360}} />
화면 기록 2022-11-14 오후 8.32.43.mov
const myVars = {
start: { scale: 0 },
end: { scale: 1, rotateZ: 360, transition: { type: 'string' } },
};
<Box variants={myVars} initial="start" animate="end" />
⚠️ variants 객체 안의 이름과 property가 동일해야함
이번에는 variants를 사용해서 이걸 만들어주겠다
//기본세팅
const Box = styled(motion.div)`
width: 200px;
height: 200px;
background-color: rgba(255,255,255,0.2);
border-radius: 35px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
display: grid;
grid-template-columns: repeat(2,1fr);
`;
const Circle = styled(motion.div)`
background-color: white;
height: 70px;
width: 70px;
border-radius: 35px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
place-self: center;
`;
function App() {
return (
<Wrapper>
<Box>
<Circle />
<Circle />
<Circle />
<Circle />
</Box>
</Wrapper>
);
}
<Circle variants={circleVariants} initial="start" animate="end">
→ 굳이 이렇게 쓰지 않아도 자식 variant 객체에 똑같은 이름이 있다면 상속해준다
const Circle = styled(motion.div)`
background-color: white;
height: 70px;
width: 70px;
border-radius: 35px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
place-self: center;
`;
const boxVariants = {
start: {
opacity: 0,
scale: 0.5,
},
end: {
scale: 1,
opacity: 1,
transition: {
type: 'spring',
duration: 0.5,
bounce: 0.5,
delayChildren: 0.5,
staggerChildren: 0.2,
},
},
};
const circleVariants = {
start: {
opacity: 0,
y: 10,
},
end: {
opacity: 1,
y: 0,
},
};
circle variant와 box variant에 모두 start, end가 동일하게 존재하므로 굳이 쓰지 않아도 자동으로 적용해줌
<Box variants={boxVariants} initial="start" animate="end">
<Circle variants={circleVariants} />
<Circle variants={circleVariants} />
<Circle variants={circleVariants} />
<Circle variants={circleVariants} />
</Box>
화면 기록 2022-11-14 오후 9.11.46.mov
포인터가 컴포넌트 위로 이동하거나 컴포넌트를 떠날 때를 감지
const boxVariants = {
hover: {scale: 1.5, rotateZ: 90},
click: {scale: 1, borderRadius: '100px'}
};
<Box variants={boxVariants} whileHover='hover' whileTap='click' />
화면 기록 2022-11-14 오후 9.21.20.mov
const boxVariants = {
hover: { scale: 1.5, rotateZ: 90 },
click: { scale: 1, borderRadius: '100px' },
drag: { backgroundColor: "rgb(46, 204, 113)", transition: { duration: 5 } },
};
화면 기록 2022-11-14 오후 9.31.37.mov
<Box drag='x' />
x축으로만 드래그 가능
<Box drag dragConstraints={{top:-50, bottom:50, left:-50, right:50}} />
drag 가능한 영역 직접 지정
pixel 지정
ref 사용
const biggerBoxRef = useRef<HTMLDivElement>(null);
<BiggerBox ref={biggerBoxRef}>
<Box
drag
dragSnapToOrigin
dragElastic={0.5}
dragConstraints={biggerBoxRef}
variants={boxVariants}
whileHover="hover"
whileTap="click"
/>
</BiggerBox>
화면 기록 2022-11-14 오후 9.43.59.mov
애니메이션 값의 상태(state)와 속도(velocity)를 추적
MotionValue는 React State가 아니기 때문에 Motion Value값이 바뀌어도 리랜더링이 일어나지 않는다
import { motion, useMotionValue } from 'framer-motion';
function App() {
const x = useMotionValue(0);
return (
<Wrapper>
<Box style={{ x }} drag="x" dragSnapToOrigin />
</Wrapper>
);
}
한 값 범위에서 다른 값 범위로 매핑하여 다른 MotionValue의 output을 변환하는 MotionValue를 만들고 x(Motion Value)값을 다른 output값으로 변환해준다.
import { motion, useMotionValue, useTransform } from 'framer-motion';
const x = useMotionValue(0);
const scale = useTransform(x, [-800, 0, 800], [2, 1, 0.1]);
return (
<Wrapper>
<Box style={{ x, scale }} drag="x" dragSnapToOrigin />
</Wrapper>
화면 기록 2022-11-14 오후 10.04.25.mov
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={{ background: gradient }}>
<Box style={{ x, rotateZ, scale }} drag="x" dragSnapToOrigin />
</Wrapper>
);
}
화면 기록 2022-11-14 오후 10.16.44.mov
pathLength : path의 끝나는 지점 설정 가능
//variant 안에 넣어주기
const svg = {
start: { pathLength: 0, fill: 'rgba(255, 255, 255, 0)' },
end: {
fill: 'rgba(255, 255, 255, 1)',
pathLength: 1,
},
};
<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>
화면 기록 2022-11-15 오전 12.27.24.mov
reactJS에서 사라지는 컴포넌트를 animate
규칙) visible 상태여야함, animate condition 안에 조건문이 있어야함
//visible 상태 아님
{showing ? <AnimatePresence /> : null}
→ ❌
<AnimatePresence>
{showing ? <Box /> : null}
</AnimatePresence>
→ ⭕️
leaving: {
opacity: 0,
scale: 0,
y: 50,
},
사라질 때 animate 가능 → 댑악~
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>
);
}
화면 기록 2022-11-15 오전 12.43.00.mov
key를 변경하여 슬라이더 컴포넌트를 만들기
const box = {
invisible: {
x: 500,
opacity: 0,
scale: 0,
},
visible: {
x: 0,
opacity: 1,
scale: 1,
transition: {
duration: 1,
},
},
exit: { x: -500, opacity: 0, scale: 0, transition: { duration: 1 } },
};
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>
);
}
화면 기록 2022-11-15 오전 1.05.05.mov
각 애니메이션 컴포넌트에 대해 동적 variants를 다르게 적용할 때 사용할 수 있는 사용자 지정 데이터
const box = {
entry: (isBack: boolean) => ({
x: isBack ? -500 : 500,
opacity: 0,
scale: 0,
}),
center: {
x: 0,
opacity: 1,
scale: 1,
transition: {
duration: 0.3,
},
},
exit: (isBack: boolean) => ({
x: isBack ? 500 : -500,
opacity: 0,
scale: 0,
transition: { duration: 0.3 },
}),
};
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}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) =>
i === visible ? (
<Box
custom={back}
variants={box}
initial="entry"
animate="center"
exit="exit"
key={i}
>
{i}
</Box>
) : null
)}
</AnimatePresence>
<button onClick={nextPlease}>next</button>
<button onClick={prevPlease}>prev</button>
</Wrapper>
);
}
화면 기록 2022-11-15 오전 1.31.02.mov
Framer motion은 외부의 힘에 의해 바뀐 것 감지 → layout prop을 넣음으로써 css 변화가 자동으로 animate
< motion.div layout>< /motion.div>
import styled from 'styled-components';
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
const Wrapper = styled.div`
height: 100vh;
width: 100vw;
display: flex;
justify-content: space-around;
align-items: center;
`;
const Grid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
width: 50vw;
gap: 10px;
div:first-child,
div:last-child {
grid-column: span 2;
}
`;
const Box = styled(motion.div)`
background-color: rgba(255, 255, 255, 1);
border-radius: 40px;
height: 200px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
const Overlay = styled(motion.div)`
width: 100%;
height: 100%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
`;
const overlay = {
hidden: { backgroundColor: 'rgba(0, 0, 0, 0)' },
visible: { backgroundColor: 'rgba(0, 0, 0, 0.5)' },
exit: { backgroundColor: 'rgba(0, 0, 0, 0)' },
};
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
variants={overlay}
onClick={() => setId(null)}
initial="hidden"
animate="visible"
exit="exit"
>
<Box layoutId={id} style={{ width: 400, height: 200 }} />
</Overlay>
) : null}
</AnimatePresence>
</Wrapper>
);
}
export default App;
화면 기록 2022-11-15 오전 4.43.30.mov
각각의 box는 다른 컴포넌트
layout을 사용해서 연결해준 것이다