npm i framer-motion
import { motion } from "framer-motion";
// 기존 방식
<div><div>
// 태그 앞에 motion. 붙여 쓰기!
<motion.div></motion.div>
// 기존 방식
const Box = styled.div``
// () 안에 motion. 붙여 쓰기!
const Box = styled(motion.div)``
<Box transition={{ delay: 3 }} animate={{ borderRadius: "100px" }} />
// scale 0에서 1로 바뀌는거!
<Box
initial={{ scale: 0 }}
animate={{ scale: 1, rotateZ: 360 }}
/>
// teen, spring 또는 inertia를 사용할 애니메이션 유형을 정의
<BoxSt
transition={{ type: "spring", bounce: 0.8 }}
initial={{ scale: 0 }}
animate={{ scale: 1, rotateZ: 360 }}
/>
많은 transition이 있음!
framer-morion transition
// 기존 코드
<Box
transition={{ type: "spring", bounce: 0.8 }}
initial={{ scale: 0 }}
animate={{ scale: 1, rotateZ: 360 }}
/>
// Variants 사용하기
// 이름은 중요하지 않음! 원하는 이름으로 하기!
// start, end도 마찬가지임! initial, final 등등 원하는 것으로!
const myVariants = {
start: {},
end: {}
}
// 코드 옮겨주기
const myVariants = {
start: { scale: 0 },
end: { scale: 1, rotateZ: 360, transition: { type: "spring", bounce: 0.8 } },
};
// variants props 사용하기.
// initial state(initial)랑 finishing state(animate) 지정해주기
// myVariants 오브젝트 내의 property 이름 적어주기 (start, end)
<Box variants={myVariants} initial="start" animate="end" />
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, //하위 컴포넌트의 애니메이션에 지속 시간(초)만큼 시차를 둘 수 있음. 4개의 서클 컴포넌트가 0.2초 간격으로 생겨남.
},
},
};
const circleVariants = {
start: {
opacity: 0,
y: 10, //x , y는 framer motion에만 있는거
},
end: {
opacity: 1,
y: 0,
},
};
// 부모로 부터 variant 상속됨.
// initial="start" animate="end" 를 자식까지 쓸 필요는 없음.
// start, end 이름은 꼭 같게! 그래야 부모가 자식 컴포넌트에 영향을 줄 수 있음.
<Box variants={boxVariants} initial="start" animate="end">
<Circle variants={circleVariants} />
<Circle variants={circleVariants} />
<Circle variants={circleVariants} />
<Circle variants={circleVariants} />
</Box>
const boxVariants = {
//hover,click,drag 이름은 원하는걸로.
hover: { scale: 1.5, rotateZ: 90 },
click: { scale: 1, borderRadius: "100px" },
drag: { backgroundColor: "rgb(46, 204, 113)", transition: { duration: 2 } },
};
// 컬러는 "blue" 이렇게 텍스트로 넣는것 보다는 rgb값으로 넣어주는게 좋음.
<Box
variants={boxVariants}
whileHover="hover" //호버 제스처가 인식되는 동안 애니메이션할 속성 또는 변형 레이블
drag //이 요소에 대해 끌기를 활성화
whileDrag="drag" //드래그 제스처가 인식되는 동안 애니메이션할 속성 또는 변형 레이블
whileTap="click" //컴포넌트를 누르고 있는 동안 애니메이션할 속성 또는 변형 레이블
/>
//drag
//특정 방향으로만 드래그하려면 "x" 또는 "y"를 설정
//< motion.div drag="x" / >
<BiggerBox>
<Box
variants={boxVariants}
whileHover="hover"
drag
dragConstraints={{ top: -200, bottom: 200, left: -200, right: 200 }} //허용된 드래그 가능 영역에 제약 조건을 적용, dragConstraints 에는 드래그 가능한 컴포넌트의 가장자리 거리를 정의
dragSnapToOrigin // 드래그를 놓을 때, 원점으로 다시 애니메이션
dragElastic={0.5} //외부 제약 조건에서 허용되는 이동 정도. 0 = 움직임 없음, 1 = 전체 움직임. 기본적으로 0.5로 설정
whileDrag="drag"
whileTap="click"
/>
</BiggerBox>
import { motion, useMotionValue, useMotionValueEvent } from "framer-motion";
const x = useMotionValue(0);
<Wrapper>
<button onClick={() => x.set(200)}>Click Me</button>
<Box style={{ x: x }} drag="x" dragSnapToOrigin />
</Wrapper>
// x.set(100)
// set 메서드로 업데이트할 수 있음 , 이것은 React 리렌더링을 트리거하지 않음
// x.get() // 100
// MotionValue는 문자열이나 숫자가 될 수 있음
// get 메소드로 값을 읽을 수 있음
: useTransform 훅을 통해 MotionValues를 연결
useTransform()는 한 값 범위에서 다른 값 범위로 매핑하여 다른 MotionValue의 output을 변환하는 MotionValue를 만듬
x(Motion Value)값을 다른 output값으로 변환함
ex) x: -400 => 1
import {
motion,
useMotionValue,
useMotionValueEvent,
useTransform,
} from "framer-motion";
const x = useMotionValue(0)
// Box돌리기
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))",
]
);
useMotionValueEvent(x, "change", (x) => {
console.log("x :", x);
});
<Wrapper style={{ background: gradient }}>
<Box style={{ x, rotateZ }} drag="x" dragSnapToOrigin />
</Wrapper>
: 뷰포트가 스크롤될 때 업데이트되는 MotionValues를 리턴
아래 값들은 모두 MotionValue< number >를 넘겨줌
scrollX: 실제 수평 스크롤 픽셀 ex) 500px
scrollY: 실제 수직 스크롤 픽셀 ex) 500px
scrollXProgress : 0 ~ 1 사이의 수평 스크롤
scrollYProgress : 0 ~ 1 사이의 수직 스크롤(가장 상단 0, 가장 하단 1)
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 { scrollY, scrollYProgress } = useScroll();
const scale = useTransform(scrollYProgress, [0, 1], [1, 5]);
useMotionValueEvent(scrollY, "change", (latest) => {
console.log("scrollY : ", latest);
});
useMotionValueEvent(scrollYProgress, "change", (latest) => {
console.log("scrollYProgress : ", latest);
});
return (
<Wrapper style={{ background: gradient }}>
<Box style={{ x, rotateZ, scale }} drag="x" dragSnapToOrigin />
</Wrapper>
svg 엘리먼트에 'pathLength', 'pathSpacing', 'pathOffset' 속성을 사용하여 Line drawing 애니메이션을 만들 수 있음
path SVG 엘리먼트는 모양을 정의하는 일반 엘리먼트
모든 기본 모양은 path 엘리먼트로 만들 수 있음
path의 속성 d는 경로의 모양을 정의함
motion.path 컴포넌트는 세 가지 강력한 SVG path 속성인 pathLength, pathSpacing 및 pathOffset을 가지고 있음
수동 경로 측정이 필요 없이 모두 0과 1 사이의 값으로 설정됨
const Svg = styled.svg`
width: 300px;
height: 300px;
path {
stroke: white;
stroke-width: 2;
}
`;
const svgVariants = {
start: { pathLength: 0, fill: "rgba(255,255,255,0" },
end: {
pathLength: 1,
fill: "rgba(255,255,255,1",
// transition: { duration: 3 }, // 이렇게 주면 모두 다 5초
},
};
<Svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<motion.path
variants={svgVariants}
initial={"start"}
animate={"end"}
transition={{ // 이렇게 주면 원하는 프로퍼티별로 각각 지정 가능
default: { duration: 5 },
fill: { duration: 2, delay: 2 },
}}
d="M224 373.1c-25.2-31.7-40.1-59.4-45-83.2-22.6-88 112.6-88 90.1 0-5.5 24.3-20.3 52-45 83.2zm138.2 73.2c-42.1 18.3-83.7-10.9-119.3-50.5 103.9-130.1 46.1-200-18.9-200-54.9 0-85.2 46.5-73.3 100.5 6.9 29.2 25.2 62.4 54.4 99.5-32.5 36.1-60.6 52.7-85.2 54.9-50 7.4-89.1-41.1-71.3-91.1 15.1-39.2 111.7-231.2 115.9-241.6 15.8-30.1 25.6-57.4 59.4-57.4 32.3 0 43.4 25.9 60.4 59.9 36 70.6 89.4 177.5 114.8 239.1 13.2 33.1-1.4 71.3-37 86.6zm47-136.1C280.3 35.9 273.1 32 224 32c-45.5 0-64.9 31.7-84.7 72.8C33.2 317.1 22.9 347.2 22 349.8-3.2 419.1 48.7 480 111.6 480c21.7 0 60.6-6.1 112.4-62.4 58.7 63.8 101.3 62.4 112.4 62.4 62.9 .1 114.9-60.9 89.6-130.2 0-3.9-16.8-38.9-16.8-39.6z"
/>
</Svg>
AnimatePresence는 안쪽에서 나타나거나 사라지는게 있으면 그것을 animate 할 수 있도록 해줌.
사용 규칙
import styled from "styled-components";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { useState } from "react";
const Wrapper = styled(motion.div)`
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
`;
const Box = styled(motion.div)`
width: 400px;
height: 200px;
background-color: rgba(255, 255, 255, 1);
border-radius: 40px;
position: absolute;
top: 100px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
const boxVariants: Variants = {
initial: {
opacity: 0,
scale: 0,
},
visible: {
opacity: 1,
scale: 1,
rotate: 360,
},
leaving: {
opacity: 1,
scale: 0,
y: 50,
},
};
const AnimatePresenceStudy = () => {
const [showing, setShowing] = useState(false);
const toggleShowing = () => setShowing((prev) => !prev);
return (
<Wrapper>
<button onClick={toggleShowing}>Click</button>
<AnimatePresence> //AnimatePresence의 내부에는 condition(조건문)이 있어야함.
{showing ? (
<Box
variants={boxVariants}
initial="initial"
animate="visible"
exit="leaving"
/>
) : null}
</AnimatePresence>
</Wrapper>
);
};
export default AnimatePresenceStudy;
const variants = {
visible: (custom) => ({
opacity: 1,
transition: { delay: custom * 0.2 }
})
}
< motion.div custom={0} animate="visible" variants={variants} />
< motion.div custom={1} animate="visible" variants={variants} />
< motion.div custom={2} animate="visible" variants={variants} />
import styled from "styled-components";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { useState } from "react";
const Wrapper = styled(motion.div)`
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
const Box = styled(motion.div)`
width: 400px;
height: 200px;
background-color: rgba(255, 255, 255, 1);
border-radius: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 28px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
position: absolute;
top: 100px;
`;
const boxVariants: Variants = {
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 },
}),
};
const SliderStudy = () => {
const [visible, setVisible] = useState(1);
const [back, setBack] = useState(false);
const prevSlider = () => {
setBack(true);
setVisible((prev) => (prev === 1 ? 1 : prev - 1));
};
const nextSlider = () => {
setBack(false);
setVisible((prev) => (prev === 10 ? 10 : prev + 1));
};
return (
<Wrapper>
<AnimatePresence mode="wait" custom={back}>
<Box
variants={boxVariants}
custom={back} // true냐 false냐에 따라 entry, exit이 달라짐.
initial="entry"
animate="center"
exit="exit"
key={visible}
>
{visible}
</Box>
</AnimatePresence>
<button onClick={prevSlider}>prev</button>
<button onClick={nextSlider}>next</button>
</Wrapper>
);
};
export default SliderStudy;
true인 경우 이 컴포넌트는 레이아웃이 변경될 때 새 위치에 자동으로 애니메이션을 적용함
크기나 위치가 변경될 때 모션 컴포넌트의 레이아웃에 자동으로 애니메이션을 적용하려면 레이아웃 prop을 제공함
부모 플렉스박스 방향, 너비, 상단/오른쪽 등 레이아웃 변경의 원인이 무엇이든 상관없이 애니메이션 자체는 최대 성능을 위해 변환으로 수행됨
ex)
< motion.div layout>< /motion.div>
import styled from "styled-components";
import { motion } from "framer-motion";
import { useState } from "react";
const Wrapper = styled(motion.div)`
height: 100vh;
width: 100vw;
display: flex;
justify-content: space-around;
align-items: center;
`;
const Box = styled(motion.div)`
width: 400px;
height: 400px;
background-color: rgba(255, 255, 255, 1);
border-radius: 40px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
const Circle = styled(motion.div)`
background-color: #00a5ff;
height: 100px;
width: 100px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
const LayoutStudy = () => {
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>
);
};
export default LayoutStudy;
import styled from "styled-components";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
const Wrapper = styled(motion.div)`
height: 100vh;
width: 100vw;
display: flex;
justify-content: space-around;
align-items: center;
`;
const Grid = styled.div`
width: 50vw;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
div:first-child,
div:last-child {
grid-column: span 2;
}
`;
const Box = styled(motion.div)`
height: 200px;
background-color: rgba(255, 255, 255, 1);
border-radius: 40px;
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 Modal2 = () => {
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>
);
};
export default Modal2;