Animation(2)

์ดˆ์—ฐ2ยท2022๋…„ 11์›” 11์ผ
0

MUTSA_Front.archive

๋ชฉ๋ก ๋ณด๊ธฐ
16/16

๐Ÿ• MotionValues

์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ’์˜ ์ƒํƒœ(state)์™€ ์†๋„(velocity)๋ฅผ ์ถ”์ 

  • ๋ชจ๋“  ๋ชจ์…˜ ์ปดํฌ๋„ŒํŠธ๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ MotionValues๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ’์˜ ์ƒํƒœ์™€ ์†๋„๋ฅผ ์ถ”์ 

  • https://www.framer.com/docs/motionvalue/

  • 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


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์„ ์–ป๊ฒŒ ๋œ๋‹ค.






๐Ÿงฉ useScroll

๋ทฐํฌํŠธ๊ฐ€ ์Šคํฌ๋กค๋  ๋•Œ ์—…๋ฐ์ดํŠธ๋˜๋Š” 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>
  );
}





๐Ÿ• SVG Animation


  • 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>
  );
}

์ด๋ ‡๊ฒŒ ํ•ด์ฃผ๋ฉด~!



์™€์šฐ ๊ฑ ์‹ ๊ธฐํ•ด์„œ ๋„ฃ์–ด๋ดค๋‹ค.





๐Ÿ• AnimatePresence

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>
  );
}






๐Ÿงฉ slider


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));

์ด๋ฏธ ์ด๋ ‡๊ฒŒ ํ•ด์คฌ๊ธฐ ๋•Œ๋ฌธ!


- custom

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๋ฅผ ๋ฐ›๋Š”๋‹ค.





- mode="wait"

ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ๋ Œ๋”๋งํ•จ.
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> 

์ด๋ ‡๊ฒŒ ํ•ด์ฃผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋œ๋‹ค.
ํ•˜๋‚˜๊ฐ€ ์™„์ „ํžˆ ์™„๋ฃŒ๋˜์–ด์•ผ ๋‹ค์Œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹คํ–‰๋œ๋‹ค.





๐Ÿ• shared layout animation

      <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>
  );
}





๐Ÿ• Final Project

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>
  );
}

profile
๊ฐ•์˜์ˆ˜๊ฐ•๊ธฐ๋ก์šฉ

0๊ฐœ์˜ ๋Œ“๊ธ€