Apresentando o Framer Motion
Publicados: 2022-03-10Neste artigo, veremos mais de perto como o Framer Motion nos ajuda a criar animações incríveis. Aprenderemos como os componentes de movimento funcionam e como encadear animações. Veremos como fazer animações acionadas por gestos, cronometradas e de rolagem com o movimento do Framer. Ao longo do caminho, usaremos o que aprendemos para criar cinco aplicativos de demonstração que configurei para nos mostrar como podemos integrar o Framer Motion em aplicativos do mundo real.
Este tutorial será benéfico para os leitores interessados em integrar animações em seu aplicativo React.
Nota: Este artigo requer uma compreensão básica de React e CSS.
O que é o movimento do Framer?
Framer Motion é uma biblioteca de animação que facilita a criação de animações. Sua API simplificada nos ajuda a abstrair as complexidades por trás das animações e nos permite criar animações com facilidade.
Componentes de movimento
Estes são os blocos de construção do movimento do Framer. Os componentes de movimento são criados prefixando motion
ao seu elemento HTML e SVG regular (por exemplo, motion.h1
). Os componentes de movimento podem aceitar vários adereços, sendo o básico o adereço animate
. Este prop recebe um objeto onde definimos as propriedades daquele componente que queremos animar. As propriedades que definimos serão animadas quando o componente for montado no DOM.
Vamos animar um texto h1 usando o Framer Motion. Primeiro, instalamos a biblioteca framer-motion e importamos motion
.
npm i framer-motion import { motion } from 'framer-motion';
Em seguida, convertemos o h1 em um componente de movimento.
<motion.h1 animate={{x: 20, y: -20}}> This is a motion component </motion.h1>
Isso fará com que o h1
deslize 20px para a direita e mova 20px para cima quando carregar. Quando as unidades não são adicionadas, os cálculos são feitos usando pixels. No entanto, você pode definir explicitamente as unidades nas quais deseja que os cálculos sejam baseados, animate={{x: "20rem", y: "-20rem"}}>
.
Por padrão, um componente de movimento será animado a partir do estado definido de seus estilos para os da prop animate
. No entanto, se quiséssemos, poderíamos sequestrar e definir o estado de animação inicial do componente usando o prop initial
. Enquanto a prop animate
é usada para definir o comportamento dos componentes quando eles são montados, a prop initial
define seu comportamento antes da montagem.
Se quisermos que nosso h1 venha da esquerda, controlamos isso usando a prop inicial.
<motion.h1 initial={{x: -1000}} animate={{x: 20}}> This is a motion component </motion.h1>
Agora, quando o h1
é montado, ele desliza da esquerda.
Não estamos limitados a uma única animação. Podemos definir uma série de animações chamadas keyframes
em uma matriz de valores. Cada valor será animado em sequência.
<motion.h1 initial={{x: -1000}} animate={{x: [20, 50, 0, -70, 40] }}> This is a motion component </motion.h1>
O prop de transition
nos permite definir como as animações ocorrem. Com ele, definimos como os valores se animam de um estado para outro. Entre outras coisas, podemos definir a duration
, o delay
e o type
de animação usando este prop.
<motion.h1 initial={{ x: -1000 }} animate={{ x: 0 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion component </motion.h1>
Digamos que deveríamos animar vários componentes de movimento simultaneamente, como no trecho de código abaixo.
<div className="App"> <motion.h1 initial={{ x: -1000 }} animate={{ x: 0 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion h1 </motion.h1> <motion.h2 initial={{ y: -1000 }} animate={{ y: 0 }} transition={{ type: "tween", duration: "1", delay: ".4" }}>This is a motion h2 </motion.h2> <motion.h3 initial={{ x: 100, opacity: 0 }} animate={{ x: 0, opacity: 1 }}> This is a motion h3 </motion.h3> <motion.h4 initial={{ scale: 0.7 }} animate={{ scale: 1.7 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion h4 </motion.h4> </div>
Enquanto isso funciona, o prop de variants
no Framer Motion nos permite extrair nossas definições de animação em um objeto de variantes. As variants
não apenas tornam nosso código mais limpo, mas também nos permitem criar animações ainda mais poderosas e complexas.
Extraindo nossas definições de animação em objetos variantes, temos isso:
const H1Variants = { initial: { x: -1000 }, animate: { x: 0 }, transition: { type: "tween", duration: 2, delay: 1 } } const H2Variants = { initial: { y: -1000 }, animate: { y: 0 }, transition: { type: "tween", duration: 1, delay: .4 } } const H3Variants = { initial:{ x: 100, opacity: 0 }, animate:{ x: 0, opacity: 1 } } const H4Variants = { initial:{ scale: 0.7 }, animate:{ scale: 1.7 }, transition:{ type: "tween", duration: "2", delay: "1" } }
Em vez de passar as definições de animação diretamente para as props initial
e animate
de um componente, extraímos essas definições em objetos variantes independentes. Nos objetos variantes, definimos nomes de variantes que descrevem o nome de cada animação como variantes.
<div className="App"> <motion.h1 variants={H1Variants} initial='initial' animate='animate' > This is a motion h1 </motion.h1> <motion.h2 variants={H2Variants} initial='initial' animate='animate' > This is a motion h2 </motion.h2> <motion.h3 variants={H3Variants} initial='initial' animate='animate' > This is a motion h3 </motion.h3> <motion.h4 variants={H4Variants} initial='initial' animate='animate' > This is a motion h4 </motion.h4> </div>
Na prop variants
, passamos o nome dos objetos variantes para cada componente de movimento e, em seguida, passamos as animações para as props initial
e animate
.
Podemos levar nossa configuração atual com variantes ainda mais para reduzir a repetição. Usando variantes, podemos propagar atributos de animação pelo DOM a partir de um componente de movimento pai. Para que isso funcione, criamos variantes para o motion.div
pai com nomes de animação semelhantes em seu objeto variante como seus filhos. Ao fazer isso, não teremos que passar os nomes das animações para cada componente filho. Nos bastidores, o elemento pai cuida disso para nós.
const ContainerVariants = { initial: {}, animate: {} }; const H1Variants = { initial: { x: -1000 }, animate: { x: 0 }, transition: { type: "tween", duration: 2, delay: 1 } }; //more variants below <motion.div className="App" variants={ContainerVariants} initial="initial" animate="animate" > <motion.h1 variants={H1Variants}>This is a motion h1</motion.h1> <motion.h2 variants={H2Variants}>This is a motion h2</motion.h2> <motion.h3 variants={H3Variants}>This is a motion h3</motion.h3> <motion.h4 variants={H4Variants}>This is a motion h4</motion.h4> </motion.div>
Agora temos um código mais limpo sem repetições. Transformamos a div do contêiner em um componente de movimento para que pudéssemos passar o objeto ContainerVariants
que definimos. Como não definimos nenhuma animação no contêiner, passamos objetos vazios para initial
e animate
. Seus nomes de animação devem ser os mesmos em cada objeto variante para que a propagação funcione.
Agora entendemos o básico do Framer Motion. Vamos começar a construir nosso punho de 5 aplicativos de demonstração.
Loja de ícones
Podemos criar animações interativas baseadas em gestos. Atualmente, os componentes de movimento são capazes de ouvir a detecção de gestos de passar o mouse, tocar, deslocar e arrastar. Estaremos construindo este aplicativo Icon Shop usando o suporte whileHover
.
Componentes
-
App.js
: contém os textos do cabeçalho. -
Card.jsx
: aqui, definimos as animações para os cartões de ícones. -
CardContainer.jsx
: importamos e percorremos os ícones. -
styles.js
: cria, estiliza e exporta os componentes de movimento. Eu usei styled-components para estilizar os componentes.
Vamos começar com App.js
.
import { H1, H2 } from "./Styles"; import CardContainer from "./CardContainer"; return ( <div> <H1 initial={{ y: -100 }} animate={{ y: 0, transition: { delay: 1 } }}> Icon Shop </H1> <H2 initial={{ x: -1000 }} animate={{ x: 0, transition: { delay: 1 } }}> Hover over the cards to see the motion magic </H2> <CardContainer /> </div> );
Importamos os componentes de movimento H1
e H2
que criamos no arquivo Styles.js
. Como eles são componentes de movimento, usamos os adereços initial
e animate
para definir seu comportamento antes e quando eles são montados. Aqui, também importamos e exibimos o componente CardContiner
.
Agora, o CardContainer.js
.
import { Container } from "./Styles"; import Card from "./Card"; import { ReactComponent as AddIcon } from "./assets/add.svg"; import { ReactComponent as AirplaneIcon } from "./assets/airplane.svg"; import { ReactComponent as AlarmIcon } from "./assets/alarm.svg"; //more svg imports below... const icons = [ <AddIcon />, <AirplaneIcon />, <AlarmIcon />, //more icons below ]; const CardContainer = () => { return ( <Container initial={{ x: -1000 }} animate={{ x: 0 }}> {icons.map((icon) => ( <Card icon={icon} /> ))} </Container> ); };
Aqui, importamos os SVGs, o componente de movimento Container
e o componente Card
.
Semelhante a H1
e H2
em App.js
, definimos animações do Container
usando as props initial
e animate
. Quando carregar, criará um efeito legal de deslizar da esquerda do navegador.
Agora, Card.js
import { CardBox, IconBox } from "./Styles"; const CardVariants = { beforeHover: {}, onHover: { scale: 1.1 } }; const IconVariants = { beforeHover: { opacity: 0, y: -50 }, onHover: { opacity: 1, y: 0, scale: 1.5, transition: { type: "tween" } } }; const Card = ({ icon }) => { console.log(icon); return ( <CardBox variants={CardVariants} initial="beforeHover" whileHover="onHover"> <IconBox variants={IconVariants}>{icon}</IconBox> </CardBox> ); };
Aqui, criamos dois objetos variantes com animações beforeHover
e onHover
. No objeto CardVariants
, não queremos fazer nada inicialmente, então beforeHover
é um objeto vazio. onHover
aumentamos a escala da caixa de cartão.
No objeto IconVariants
, definimos o estado inicial do IconBox
em seu beforeHover
. Definimos sua opacidade como 0 e a empurramos para cima em 50px. Então, em onHover
, definimos a opacidade de volta para 1, empurramos de volta para sua posição padrão e alteramos o tipo de transição para tween
. Em seguida, passamos as variantes para seus respectivos componentes de movimento. Fazemos uso da propagação, então não precisamos definir explicitamente as props initial
e animate
para o componente IconBox
.
Barra de navegação animada
Construiremos um componente de navegação simples e veremos como podemos criar relações de tempo entre os componentes de movimento pai e filho.
Componentes
-
App.js
: contém os textos do cabeçalho. -
Styles.js
: crie, estilize e exporte os componentes de movimento. Os componentes são estilizados usando styled-components.
Vamos dar uma olhada no arquivo App.js
import { Header, Nav, Link, SvgBox } from "./Styles"; function App() { const [isOpen, setIsOpen] = useState(false); const iconVariants = { opened: { rotate: 135 }, closed: { rotate: 0 } }; const menuVariants = { opened: { top: 0, transition: { when: "beforeChildren", staggerChildren: 0.5 } }, closed: { top: "-90vh" } }; const linkVariants = { opened: { opacity: 1, y: 50 }, closed: { opacity: 0, y: 0 } };
Criamos um estado isOpen
que será usado para verificar se a Navbar está aberta ou não. Criamos 3 objetos variantes, iconVariants
, menuVariants
e linkVariants
, onde definimos as animações para os componentes de movimento SvgBox
, Nav
e Link
respectivamente. O iconVariants
é usado para girar o SvgBox
135deg quando passa o mouse sobre ele. Não precisamos adicionar “graus” ao valor. No menuVariants
, controlamos a posição superior do Nav
como você faria usando a propriedade position
no CSS. Alternamos a posição superior do Nav
com base no estado isOpen
.
Com variantes, podemos criar relações de tempo entre componentes de movimento pai e filho. Definimos o relacionamento entre o pai Nav
e seu filho, Link
, usando a propriedade when
no objeto de transição. Aqui, defina-o como beforeChildren
, para que as animações do componente pai terminem antes do início da animação do filho.
Usando a propriedade staggerChildren
, definimos uma ordem de tempo para cada link. Cada link levará 0,5 segundos para aparecer um após o outro. Isso cria uma boa dica visual quando o Nav
é aberto. No linkVariants
animamos a opacidade e a posição vertical de cada link.
<div className="App"> <Header> <SvgBox variants={iconVariants} animate={isOpen ? "opened" : "closed"} onClick={() => setIsOpen(!isOpen)} > <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="https://www.w3.org/2000/svg" > <path d="M12 4C11.4477 4 11 4.44772 11 5V11H5C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13H11V19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V13H19C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11H13V5C13 4.44772 12.5523 4 12 4Z" fill="#fff" /> </svg> </SvgBox> </Header> <Nav initial={false} variants={menuVariants} animate={isOpen ? "opened" : "closed"} > <Link variants={linkVariants}>home</Link> <Link variants={linkVariants}>about</Link> <Link variants={linkVariants}>gallery</Link> </Nav> </div>
Aqui, passamos as variantes para seus respectivos componentes. No SvgBox
, alternamos o estado de isOpen
sempre que ele é clicado e o animamos condicionalmente com base no estado. Como o SvgBox
, animamos condicionalmente o Nav
e o Link
s com base no estado de isOpen
.
Modal Animado
Construiremos um componente modal e aprenderemos sobre o AnimatePresence
do Framer Motion e como ele nos permite animar elementos à medida que saem do DOM.
Componentes:
-
App.js
: configuramos o estadoshowModal
aqui. -
Modal.jsx
: o trabalho de animação real ocorre aqui. -
Styles.js
: crie, estilize e exporte os componentes de movimento. Os componentes são estilizados usando styled-components.
Vamos analisar o App.js
import { ToggleButton, Container } from "./Styles"; import Modal from "./Modal"; function App() { const [showModal, setShowModal] = useState(false); const toggleModal = () => { setShowModal(!showModal); }; return ( <Container> <ToggleButton initial={{ x: -700 }} animate={{ x: 0, transition: { duration: 0.5 } }} onClick={toggleModal} > Toggle Modal </ToggleButton> <Modal showModal={showModal} /> </Container> ); }
Criamos um estado showModal
que será usado para renderizar condicionalmente o modal. A função toggleModal
alternará o estado sempre que o ToggleButton
for clicado. ToggleButton
é um componente de movimento, então podemos definir animações para ele. Quando é montado, ele desliza da esquerda. Esta animação é executada por 0,5 segundos. Também passamos do estado showModal
para o Modal
através de props.
Agora, Modal.jsx
import { AnimatePresence } from "framer-motion"; import { ModalBox, ModalContent, Container } from "./Styles"; <Container> <AnimatePresence> {showModal && ( <ModalBox initial={{ opacity: 0, y: 60, scale: 0.3 }} animate={{ opacity: 1, y: 0, scale: 1, transition: { type: "spring", stiffness: 300 } }} exit={{ opacity: 0, scale: 0.5, transition: { duration: 0.6 } }} > <ModalContent initial={{ y: -30, opacity: 0 }} animate={{ y: 0, opacity: 1, transition: { delay: 1 } }} > Modal content!!!! </ModalContent> </ModalBox> )} </AnimatePresence> </Container>
Importamos AnimatePresence
do framer-motion
. Ele nos permite definir animações de saída para componentes quando eles saem do DOM. Renderizamos condicionalmente o Modal
com base no estado showModal
. Definimos as animações para ModalBox
e ModalContent
através de seus props initial
e animate
. Há também um novo adereço aqui, exit
. Ter AnimatePresence
como um wrapper nos permite adicionar animações de saída ao ModalBox
na prop de exit
.
Animação de rolagem
Usaremos uma combinação do gancho useAnimation
e react react-intersection-observer
para criar animações acionadas por rolagem.
Componentes
-
App.js
: configuramos as animações para o componenteBox
e renderizamos noApp
-
Styles.js
: crie, estilize e exporte os componentes de movimento. Os componentes são estilizados usando styled-components.
import React, { useEffect } from "react"; import { useAnimation } from "framer-motion"; import { useInView } from "react-intersection-observer"; import { Container, H1,StyledBox } from "./Styles"; const BoxVariants = { visible: { opacity: 1, x: 0, transition: { duration: 1 } }, hidden: { opacity: 0, x: 300 }, }; const Box = () => { const controls = useAnimation(); const [ref, inView] = useInView(); useEffect(() => { if (inView) { controls.start("visible"); } }, [controls, inView]); return ( <StyledBox ref={ref} animate={controls} initial="hidden" variants={BoxVariants} /> ); };
O gancho useAnimation
nos permite controlar as sequências em que nossas animações ocorrem. Temos acesso aos métodos controls.start
e controls.stop
que podemos usar para iniciar e parar manualmente nossas animações. Passamos a animação hidden
inicial para StyledBox
. Passamos os controles que definimos com o método start
para StyledBox
animate prop.
O gancho useInView
do react react-intersection-observer
nos permite rastrear quando um componente está visível na viewport. O hook useInView
nos dá acesso a ref
, que passamos para o componente que queremos assistir, e o boolean inView
, que nos diz se aquele elemento é inView
ou não. Usamos o useEffect
para chamar controls.start
sempre que o elemento que estamos observando, StyledBox
, estiver em exibição. Passamos controls
e inView
como dependências de useEffect
. Além disso, passamos as variantes que definimos, BoxVariants
para StyledBox
.
Animação de herói
Construiremos uma animação de banner de herói legal usando o gancho useCycle
. Vamos entender como useCycle
nos permite percorrer animações.
import React, { useEffect } from "react"; import { useCycle } from "framer-motion"; import { Container, H1, HeroSection, Banner, TextBox } from "./Styles"; import { ReactComponent as BannerIllustration } from "./bighead.svg"; const H1Variants = { initial: { y: -200, opacity: 0 }, animate: { y: 0, opacity: 1, transition: { delay: 1 } }, }; const TextVariants = { initial: { x: 400 }, animate: { x: 0, transition: { duration: 0.5 } }, }; const BannerVariants = { animationOne: { x: -250, opacity: 1, transition: { duration: 0.5 } }, animationTwo: { y: [0, -20], opacity: 1, transition: { yoyo: Infinity, ease: "easeIn" }, }, };
Definimos 3 variantes, H1Variants
, TextVariants
e BannerVariants
. No entanto, nosso foco é BannerVariants
. Definimos 2 animações, animationOne
e animationTwo
em BannerVariants
. Estas são as animações que passamos para o useCycle
para percorrer.
const [animation, cycleAnimation] = useCycle("animationOne", "animationTwo"); useEffect(() => { setTimeout(() => { cycleAnimation(); }, 2000); }, []);
useCycle
funciona de maneira semelhante ao gancho useState
. Na matriz desestruturada, a animation
representa a animação que está ativa, seja animationOne
ou animationTwo
. A função cylceAnimation
que alterna entre a animação que definimos. Passamos as animações que queremos percorrer em useCycle
e chamamos cylceAnimation
após 2 segundos em useEffect
.
<div className="App"> <Container> <H1 variants={H1Variants} initial="initial" animate="animate"> Cool Hero Section Anmiation </H1> <HeroSection> <TextBox variants={TextVariants} initial="initial" animate="animate"> Storage shed, troughs feed bale manure, is garden wheat oats at augers. Bulls at rose garden cucumbers mice sunflower wheat in pig. Chainsaw foal hay hook, herbs at combine harvester, children is mallet. Goat goose hen horse. Pick up truck livestock, pets and storage shed, troughs feed bale manure, is garden wheat oats at augers. Lamb. </TextBox> <Banner variants={BannerVariants} animate={animation}> <BannerIllustration /> </Banner> </HeroSection> </Container> </div>
No final de tudo, passamos as variantes para seus respectivos componentes e vemos a mágica acontecer. Com isso, o Banner
deslizará inicialmente da direita com base nas animações que definimos em animationOne
, e após 2 segundos, cycleAnimation
será chamado que acionará animationTwo
.
Como um porco sábio disse uma vez, “isso é tudo pessoal”.
Conclusão
Passamos pelo básico do Framer Motion e vimos alguns projetos de demonstração que nos dão um vislumbre da variedade de animações que podemos criar. No entanto, você pode fazer muito mais com ele. Eu encorajo você a mergulhar nos documentos e enlouquecer.
Recursos
- Documentos de API do Framer Motion, Framer Motion
- react-intersection-observer, npm
- Framer Motion para React, NetNinja