Что такое Framer Motion?

Framer Motion - это библиотека для react-приложений, которая дает возможность создавать анимированные jsx-компоненты.

Преимущество Framer Motion в том, что управление свойствами анимации происходит прямо в jsx-разметке. Кроме того, с его помощью можно писать компоненты-обертки, которые будут анимировать вложенные в них компоненты.

С помощью Framer Motion можно создавать целые кастомные библиотеки анимационных компонентов и применять соответствующий компонент для соответствующей анимации.

Библиотека бесплатная и легка в освоении для новичков, в то же время дает широкие возможности для создания сложных анимаций.

Как начать работу с Framer Motion?

Чтобы начать использовать Framer Motion в react-приложении, нужно установить Framer Motion в проект:

npm i framer-motion

Затем импортировать в компонент:

import {motion} from 'framer-motion';

Но в качестве примера проще импортировать Framer Motion CDN. Для этого нужно добавить Babel CDN:

<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>

И импортировать Framer в компонент по ссылке:

import {motion} from 'https://cdn.skypack.dev/framer-motion@7';

Обратите внимание на соответствие версий Framer Motion и React: для Framer версии 8 нужен React версии 18, для Framer версии 7 и ниже можно использовать React версии 17.

К практике!

В качестве примера, давайте анимируем изменение высоты карточки с изменяющемся контентом.

Без Framer решать такую задачу было бы непросто. С помощью css можно сделать транзишн по высоте через свойство max‑height, но таким способом не получиться проанимировать изменение высоты в обратную сторону — с большей карточки на меньшую. Пришлось бы добавлять js‑скрипт. Также, так как речь идет про реакт, скорее всего пришлось бы завязываться на рефах, что тоже добавляет неудобств.

С Framer эта задача решается красиво - без костылей. Анимацию можно выделить в отдельный компонент. Последнее очень удобно, если нужно впоследствии заменить, убрать или переиспользовать анимацию.

Развернем тестовый проект, используя Babel CDN для поддержки импортов и jsx-разметки и Sass CDN для использования scss.

Каскадные стили в нашем примере будут только нужны для объявление статических стилей - всю динамику будем делать с Framer.

Рекомендую использовать VS Code с расширением Live Server для поднятия проекта на локальном хосте.

Сетап проекта

Объявим index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <!-- Babel -->
    <script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
    <!-- Sass -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sass.js/0.9.12/sass.sync.min.js"></script>

    <script defer src="index.jsx" type="text/babel" data-type="module"></script>

    <script sass>
      ['default.css', 'index.scss'].forEach(async path => {
        const file = await fetch(path);
        const sass = await file.text();
        Sass.compile(sass, ({text: css, status, line: l}) => {
          const s = Object.assign(document.createElement('style'), {innerHTML: css});
          status !== 1
            ? document.head.appendChild(s)
            : console.error(`${path} ${file.ok ? `(line: ${l})` : ''}`);
        });
      });
    </script>

    <title>Framer Motion</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Добавим стили карточек и кнопки, которые мы используем в разметке:

.card {
  overflow: hidden;
  box-sizing: border-box;
  width: 300px;
  border-radius: 8px;
  box-shadow: /*r*/ 0 /*b*/ 2px /*spread*/ 5px /*solid*/ 0 rgba(0, 0, 0, 0.3);

  .title {
    padding: 16px 8px;
    background: linear-gradient(30deg, orange 30%, navy 100%);
    color: white;
    font-weight: bold;
  }

  .text {
    padding: 8px;
    font-size: 12px;
  }
}

.app {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica';

  .wrap {
    display: flex;
    flex-direction: column;
    align-items: center;

    button {
      padding: 4px 8px;

      &.has-margin {
        margin-top: 12px;
      }
    }
  }
}

Теперь объявим index.jsx, импортируем реакт и создадим разметку изменяющейся карточки и кнопки переключения:

import React, {useState, useRef} from 'https://cdn.skypack.dev/react@17';
import ReactDOM from 'https://cdn.skypack.dev/react-dom@17';

const Card = ({text, idx}) =>
  !text ? (
    <></>
  ) : (
    <div className={'card'}>
      <div className='title'>Card {{0: 'One', 1: 'Two', 2: 'Three'}[idx]}</div>
      <div className='text'>{text}</div>
    </div>
  );

const App = () => {
  const [activeIdx, setActiveIdx] = useState(-1);

  const text = getText(activeIdx);

  return (
    <div className='app'>
      <div className='wrap'>
        <Card idx={activeIdx} text={text} />
        <button
          className={`${activeIdx !== -1 ? 'has-margin' : ''}`}
          onClick={() => setActiveIdx(activeIdx => (activeIdx < 2 ? activeIdx + 1 : 0))}
        >
          {activeIdx === -1 ? 'Start' : 'Next'}
        </button>
      </div>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('#root'));

Создание анимации

Теперь давайте объявим компонент анимации. 

Чтобы это сделать надо импортировать компонент motion из framer-motion cdn, и через точку создать соответствующий тег:

import {motion} from 'https://cdn.skypack.dev/framer-motion@6';

const ChangeHeightMotion = ({
  children,
}) => {
  return (
    <motion.div>
      {children}
    </motion.div>
  );
};

По дефолту, motion компонент проигрывает анимации только при маунте компонента. Чтобы компонент реанимировался, нужно в него передать переменную, при изменении которой он будет снова проигрывать анимацию:

const ChangeHeightMotion = ({
  children,
  reanimate,
}) => {
  return (
    <motion.div
      key={reanimate}
    >
      {children}
    </motion.div>
  );
};

Теперь перейдем к анимированию высоты. Параметр initial содержит состояние начала анимации, а параметр animate - конца.

Добавим возможность передавать начальную высоту блока и сделаем ее 0 по дефолту. Конечное значение высоты выставим auto - по величине контента. После того как отработает animate, воспользуемся onAnimationComplete, чтобы запомнить новое начальное значение отсчета высоты:

const ChangeHeightMotion = ({
  children,
  reanimate,
  initialHeight = 0,
}) => {
  const motionElemRef = useRef(false);
  const initialHeightRef = useRef();

  return (
    <motion.div
      key={reanimate}
      ref={motionElemRef}
      initial={{height: initialHeightRef.current || initialHeight}}
      animate={{height: 'auto'}}
      onAnimationComplete={(x, y) => {
        initialHeightRef.current = getComputedStyle(motionElemRef.current).height;
      }}
    >
      {children}
    </motion.div>
  );
};

Теперь настроим саму анимацию. За нее отвечает параметр transition. Имена и логика свойств transition коррелируют с именами css свойства transition: duration, ease, delay. Также есть и множество дополнительных свойств, как например свойство type, которое отвечает за добавление “отскока” в конце анимации. Подробнее о различных свойствах transition можно прочесть в документации.

Готовый компонент анимации будет выглядеть так:

const ChangeHeightMotion = ({
  children,
  reanimate,
  initialHeight = 0,
  duration = 0.5, // sec
  ease = 'lenear',
  easeWithSpring = true,
  delay = 0,
}) => {
  const motionElemRef = useRef(false);
  const initialHeightRef = useRef();

  return (
    <motion.div
      key={reanimate}
      ref={motionElemRef}
      initial={{height: initialHeightRef.current || initialHeight}}
      animate={{height: 'auto'}}
      onAnimationComplete={(x, y) => {
        initialHeightRef.current = getComputedStyle(motionElemRef.current).height;
      }}
      transition={{duration, ease, type: easeWithSpring ? 'spring' : 'tween', delay}}
    >
      {children}
    </motion.div>
  );
};

Теперь нам осталось только обернуть в ChangeHeightMotion компонент, который меняет высоту, и передать переменную для реанимации. В нашем случае изменяющийся по высоте компонент - это div с текстом:

      <ChangeHeightMotion reanimate={text} duration={0.8} ease={'easeOut'}>
        <div className='text'>{text}</div>
      </ChangeHeightMotion>

Бывает, что доступа к коду компонента Card нет, скажем, если мы получаем его из внешней библиотеки - все равно можно использовать компонент ChangeHeightMotion, обернув Card в ChangeHeightMotion. 

Правда, в таком случае может возникнуть коллизия стилей, но изменение высоты вложенного компонента все равно проанимируется. Для того, чтобы свойство анимации отображалось корректно, конечно, лучше стараться оборачивать именно ту часть разметки, которая меняет высоту.

В заключение

На простом примере мы показали, как в небольшое количество строк кода можно “оживить” наше приложение.

Framer Motion мощный фреймворк, и мы не затронули всех возможностей настройки анимаций и задания динамических анимаций.

В документация Framer Motion вы можете найти определение всех свойств и посмотреть примеры их использования. Советую в частности обратить внимание на AnimatePresense и свойство exit - с их помощью можно создать анимацию на анмаунте компонента, которая будем задерживать изменения отображение виртуального дома, пока анимация не завершиться.

На этом, собственно, можно закончить обзор базового функционала - желаю вам удачи в использовании Framer Motion!

Документация:

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

Посмотреть пример (codepen):

https://codepen.io/karmacan/pen/VwVKmrv

спасибо

Комментарии (0)