TECH

Framer Motion 애니메이션 라이브러리

ssund 2023. 3. 6. 12:27

애니메이션 라이브러리

Framer Motion

demo

api 

 

설치

npm i framer-motion

 

 

div에 animation을 적용하고 싶을 때 motion에 있는 html태그를 사용해줘야한다. 

import { motion } from "framer-motion";

export default function App() {
  return (
    <div className="App">
      <motion.div></motion.div>
    </div>
  );
}

 

 

styled-components에서 motion을 사용해야하는 경우

// 타입선언은 괄호 뒤에 한다.
const Box = styled(motion.div)<{ bg: string}>`
	....
`;

 

background-color를 변경해야하는 경우 red, blue가 아닌 rgba 컬러를 사용해야한다. 

 

기본 props

initial - 애니메이션 적용하기 전 초기스타일

animate - 애니메이션 적용 스타일 

transition- 트랜지션 속성

<Box transition={{ delay: 3 }} initial={{ borderRadius: "10px"}} animate={{ borderRadius: "100px" }} />

 

transform-origin - transition을 사용할 때 어느위치에서 애니메이션을 할지 지정할 수도있다. (기본 css 속성) 

transform-origin: right center;

 

Variant

variant를 사용하면 코드 정리가 된다.

animation에 관련된 props속성을 분리 된 Object로 옮긴 것.

오브젝트이름, 오브젝트 내 property 이름은 제약없다. 

적용할 컴포넌트에 variants props로 object이름 넣어준다.
필요한 props에 Object property 이름 넣어준다. 

const myVars = {
  start: { scale: 0 },
  end: { scale: 1, rotateZ: 360, transition: { type: "spring", delay: 0.5 } },
};

function App() {
  return (
    <Wrapper>
      <Box variants={myVars} initial="start" animate="end" />
    </Wrapper>
  );
}

 

부모 컴포넌트에 variants가 적용되어있으면 props 복사해서 적용해준다. 
자식 컴포넌트에 따로 적용하지 않아도 된다. 

아래 코드에서 Circle 컴포넌트에 initial="start" animate="end"가 자동으로 들어가있다. 

<Wrapper>
   <Box variants={myVars} initial="start" animate="end">
     <Circle /> //initial="start" animate="end"
     <Circle /> //initial="start" animate="end"
     <Circle /> //initial="start" animate="end"
     <Circle /> //initial="start" animate="end"
   </Box>
</Wrapper>

 


자식에서 다른 animation을 적용하려는 경우 
자식 Variants Object를 만들고 property 이름을 부모와 동일하게 해주면

자식 컴포넌트에서 variants props만 넘겨주면 된다. 

const boxVars = {
  start: { },
  end: { },
};

const circleVars = {
  start: { },
  end: { },
};

function App() {
  return (
    <Wrapper>
      <Box variants={boxVars} initial="start" animate="end">
        <Circle variants={circleVars} />
        <Circle variants={circleVars} />
        <Circle variants={circleVars} />
        <Circle variants={circleVars} />
      </Box>
    </Wrapper>
  );
}

 

 

부모 애니메이션 속성에서 자식 애니메이션을 컨트롤 할 수 있다. 

delayChildren - 자식 delay

staggerChildren - 자식들 간의 delay

const boxVars = {
  start: { scale: 0 },
  end: {
    scale: 1,
    transition: { 
      type: "spring", 
      bounce: 0.25, 
      delayChildren: 0.5, // 자식 delay
      staggerChildren: 0.2  // 자식들의 delay
    },
  },
};

const circleVars = {
  start: { opacity: 0, y: 10 },
  end: { opacity: 1, y: 0, transition: { type: "spring" } },
};

function App() {
  return (
    <Wrapper>
      <Box variants={boxVars} initial="start" animate="end">
        <Circle variants={circleVars} />
        <Circle variants={circleVars} />
        <Circle variants={circleVars} />
        <Circle variants={circleVars} />
      </Box>
    </Wrapper>
  );
}

 

Gesture

whileHover, whileTap

마우스 호버, 클릭 시 등등 다양한 제스처에 대한 애니메이션

const boxVars = {
  hover: { scale: 1.5, rotate: 180 },
  click: { borderRadius: "100px" },
};

function App() {
  return (
    <Wrapper>
      // whileHover: 마우스 호버, whileTap: 클릭
      <Box variants={boxVars} whileHover="hover" whileTap="click"></Box>
    </Wrapper>
  );
}

 

drag

드래그 애니메이션

드래그 시키고 싶은경우 drag props를 넣어주면 된다. (간편-)

// drag="x" x좌표로만 드래그가능
// drag="y" y좌표로만 드래그가능
// whilDrag 드래그할 때 적용할 애니메이션

<Box drag="x" variants={boxVars} whileDrag="drag" ></Box>

 

drag constraint (드래그 가능한 영역 제한)

1. 수치를 넣어서 영역을 제한하는 방법

<Box
  drag
  variants={boxVars}
  dragConstraints={{ top: -50, bottom: 50, left: -50, right: 50 }}
  whileDrag="drag"
></Box>

 

2. dom으로 영역을 제한하는 방법

function App() {
  const boxRef = useRef<HTMLDivElement>(null);

  return (
    <Wrapper>
      <BoxWapper ref={boxRef}>
        <Box
          drag
          variants={boxVars}
          dragConstraints={boxRef}
          whileDrag="drag"
        ></Box>
      </BoxWapper>
    </Wrapper>
  );
}

 

 

어디로 드래그 해도 제자리로 돌아오게 하는 법

// 1. 수치로 넣어주는 방법
dragConstraints={{ top: 0, bottom: 0, left: 0, right: 0 }}

// 2. 간단한 props로 적용하는 방법
dragSnapToOrigin

 

 

드래그 할때 잡아끄는 힘 조정하기 

dragElastic = {1} 잡아당기는 힘 0-1까지

 

motionValue

애니메이션 했을 때 계산되는 값 

motionValue가 업데이트 될 때 react rendering cycle을 발동시키지 않는다. 

그래서 컴포넌트는 다시 렌더링 되지 않는다. 

 

useMotionValue

function App() {
  const x = useMotionValue(0);
  console.log(x); // x가 변경되도 console로그는 로드할때 한번밖에 찍히지 않는다. 

  return (
    <Wrapper>
      <Box
        style={{ x: x }} // 키와 value가 같아서 x하나만 써줘도 된다. 
        drag="x"
        variants={boxVars}
        whileDrag="drag"
      ></Box>
    </Wrapper>
  );
}

 

값을 출력하고 싶은경우 useEffect를 사용해서 확인한다. 

useEffect(() => {
  x.onChange(() => {
  	console.log(x.get());
  });
}, [x]);

 

useTransform

motionValue값에 따른 애니메이션을 적용할 때 사용

예를 들어 

x값이 -800인 경우 scale값을 2로 

x값이 0인 경우 scale값을 1로

x값이 800인 경우 scale값을 0로 

useTransform(x, [-800, 0, 800], [2, 1, 0])// 어떤값, 값의 위치, output
// 값의 위치, output 길이 같아야한다
function App() {
  const x = useMotionValue(0);
  const scale = useTransform(x, [-400, 0, 400], [2, 1, 0]);

  return (
    <Wrapper>
      <Box
        style={{ x, scale }}
        drag="x"
        variants={boxVars}
        whileDrag="drag"
      ></Box>
    </Wrapper>
  );
}

 

useScroll

scrollY - 스크롤 값

scrollProgress 스크롤값에 따라 달라지는 값 , 0-1사이의 값

 

SVG

fill, pathLength로 애니메이션을 적용할 수 있다. 

const SvgWrapper = styled.svg`
  width: 200px;
  height: 200px;

  path {
    stroke-width: 5;
    stroke: white;
  }
`;

function App() {
  const svgVar = {
    start: { pathLength: 0, fill: "rgba(255, 255, 255, 0)" },
    end: { pathLength: 1, fill: "rgba(255, 255, 255, 1)", transition: { duration: 3 } },
  };

  return (
    <Wrapper>
      <SvgWrapper xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
        <motion.path
          variants={svgVar}
          initial="start"
          animate="end"
          d="..."
        />
      </SvgWrapper>
    </Wrapper>
  );
}

 

 

fill, pathLength 애니메이션을 따로 transition을 주고싶은경우 transition props를 써주고
모든 요소에 들어가는 건 default로 써주면 되고 
특정요소에 들어가는건 property 이름 써주고 하면 된다.

<Wrapper>
  <SvgWrapper xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
    <motion.path
      variants={svgVar}
      initial="start"
      animate="end"
      transition={{
        default: { duration: 5 }, // 모든요소에 적용
        fill: { duration: 3, delay: 2 }, // fill요소에만 적용
      }}
      d="..."
      />
  </SvgWrapper>
</Wrapper>

 

animatePresence

나타나고, 사라지는 애니메이션 적용 

animatePresence 컴포넌트 내부에는 display 조건문이 있어야한다. 

조건문안에 animationPresence 컴포넌트가 위치하면 안된다.

 

exit props - element가 사라질때 적용하는 애니메이션 

function App() {
  const [isVisible, setVisible] = useState(false);
  const toggleClick = () => {
    setVisible((prev) => !prev);
  };

  const BoxVars = {
    initial: { scale: 0, opacity: 0 },
    visible: { opacity: 1, scale: 1 },
    exit: { y: 50 },
  };

  return (
    <Wrapper>
      <AnimatePresence>
        {isVisible ? (
          <Box variants={BoxVars} initial="initial" animate="visible" exit="exit" />
        ) : null}
      </AnimatePresence>
      <div>
        <button onClick={toggleClick}>click me</button>
      </div>
    </Wrapper>
  );
}

 

onExitComplete - exit가 끝나고 실행되는 것 

initial - 초기에 애니메이션을 적용할 건지 ( true로 하면 로드될 때 애니메이션이 동작한다. )

<AnimatePresence onExitComplete={toggleLeaving} initial={false}>

 

 

 

animatePresence 로 Slider구현

function App() {
  const [currentPage, setCurrentPage] = useState(1);

  const clickPrev = () => {
    setCurrentPage((current) => (current === 1 ? 1 : current - 1));
  };
  const clickNext = () => {
    setCurrentPage((current) => (current === 10 ? 10 : current + 1));
  };

  const BoxVars = {
    initial: { x: 500, scale: 0, opacity: 0 },
    visible: { x: 0, opacity: 1, scale: 1 },
    exit: { x: -500, scale: 0 },
  };

  return (
    <Wrapper>
      <AnimatePresence>
        {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) =>
          currentPage === num ? (
            <Box variants={BoxVars} initial="initial" animate="visible" exit="exit" key={num}>
              {num}
            </Box>
          ) : null
        )}
      </AnimatePresence>

      <div style={{ position: "fixed", bottom: 200 }}>
        <button onClick={clickPrev}>prev</button>
        <button onClick={clickNext}>next</button>
      </div>
    </Wrapper>
  );
}

 

custom 

variants에 데이터를 보낼수있게 해주는 property

// variants에서 매개변수로 받아서 분기처리 해줄수있다.
const BoxVars = {
    initial: (direction: boolean) => ({ x: direction ? -500 : 500, scale: 0, opacity: 0 }),
    visible: { x: 0, opacity: 1, scale: 1, transition: { duration: 1 } },
    exit: (direction: boolean) => ({
      x: direction ? 500 : -500,
      scale: 0,
      transition: { duration: 0.5 },
    }),
};

// AnimatePresence와 해당컴포넌트에 custom props를 넣어준다.
<AnimatePresence custom={direction}>
  <Box
    custom={direction}
    variants={BoxVars}
    initial="initial"
    animate="visible"
    exit="exit"
    key={currentPage}
  >
  	{currentPage}
  </Box>
</AnimatePresence>

 

next 버튼을 누르면 1카드가 exit 되는 animation과 2카드 나타나는 animation이 동시에 일어난다.
mode="wait" 를 사용하면 1카드가 exit된 다음 2카드 나타나는 애니메이션이 일어난다.

<AnimatePresence  mode="wait" >
</AnimatePresence>

 

layout

부모요소가 조건에 따라 속성이 변경되서 애니메이션 되어야할때 layout 속성을 준다.
framer motion은 무언가 외부의 힘에 의해 바뀐것을 감지한다.

<Box
  style={{
  alignItems: isClick ? "center" : "flex-start",
  justifyContent: isClick ? "center" : "flex-start",
  }}
>
	<Circle layout />
</Box>

layout 적용 전
layout 적용 후

layoutId 

다른 컴포넌트지만 framer motion에게 같은 컴포넌트라고 지정해서 애니메이션을 줄 수 있다. 

<Box>{isClick ? <Circle layoutId="circle1" /> : null}</Box>
<Box>{!isClick ? <Circle layoutId="circle1" /> : null}</Box>

layoutId 적용 전
layoutId 적용 후

 

useAnimation

다른역활을 하는 함수안에서 애니메이션을 제어하고 싶은경우 사용한다. 
(ex 토글하는 클릭 이벤트에서 애니메이션을 제어하는 경우)

inputAnimation = useAnimation();
  // 클릭했을때 state active시키는 코드에서 애니메이션을 실행하고 있다.
  const toggleSearchActive = () => {
    if (searchActive) {
      inputAnimation.start({
        scaleX: 0,
      });
    } else {
      inputAnimation.start({
        scaleX: 1,
      });
    }
    setSearchActive((prev) => !prev);
  };
  
  return (
  	<Input animate={inputAnimation}/>  
  )
}

 

useAnimation을 사용할 때에도 variants를 사용할 수 있다. 

const navVariants = {
  top: {
    backgroundColor: "rgba(0, 0, 0, 1)",
  },
  scroll: {
    backgroundColor: "rgba(0, 0, 0, 0)",
  },
};

function Header() {
  const navAnimation = useAnimation();
  const { scrollY } = useScroll();
  
  useEffect(() => {
    scrollY.onChange(() => {
      if (scrollY.get() > 80) {
        navAnimation.start("scroll"); // variants의 property이름
      } else {
        navAnimation.start("top");
      }
    });
  }, [scrollY, navAnimation]);
  
  return (
  	<Nav variants={navVariants} animate={navAnimation}>
  )
}

 

간단한 animation 변경은 useAnimation hook을 이용하는것보다

animate props를 수정하는게 더 효율적이다. 
 

animate keyframe

const logoVariants = {
  normal: {
    fillOpacity: 1,
  },
  active: {
    fillOpacity: [0, 1, 0], // 0, 50%, 100%
    transition: {
      repeat: Infinity,
    },
  },
};