Framer Motion next level : des animations synchronisées et interactives

par

Lucie

Lucie Ory

8 minutes de lecture

Framer Motion next level : des animations synchronisées et interactives

Après notre premier article sur les bases du mouvement avec Framer Motion, qui couvrait les rudiments du mouvement (par ici pour la doc officielle framer motion), il est temps de s’attaquer à des animations plus complexes. Reprenons où nous en étions dans notre application de lecture de contes de fées et améliorons son aspect .

En ajoutant une page d'accueil et en combinant tous les effets d'animation que nous verrons dans cet article on devrait obtenir ceci : La démo de la bibliothèque merveilleuse.

C'est parti !

Coordonner les mouvements d’entrée et de sortie - Shared Layout Animation + AnimatePresence

La doc AnimateSharedLayout

Il est temps de modifier la page d’accueil pour proposer de nouveaux livres à nos lecteurs. Chaque livre sera représenté par une vignette qui s’ouvrira dans une popup pour afficher les détails de l’ouvrage et proposer au lecteur d’accéder aux chapitres.

On aura donc pour chaque livre un état fermé :

Framer Motion - Etat ouvert

et un état ouvert :

Framer Motion - Etat ouvert

L’un des grands avantages de Framer Motion est de proposer la synchronisation automatique des transitions entre différents composants Motion. C’est le rôle du composant . Dans notre cas, il nous faut un moyen d’animer l’ouverture du livre depuis sa vignette vers la popup, puis de le replier vers sa vignette lors de sa fermeture.

On commence par créer un composant <Library/> qui regroupera l’ensemble de nos ouvrages. Ce composant affichera :

  • les vignettes <BookCover/> de chaque livre disponible
  • et la popup <OpenBook/> qui sera affichée si un livre est sélectionné

Pour réaliser notre mouvement on entoure les vignettes et la popup

  • d’un composant <AnimateSharedLayout/> pour gérer automatiquement les transitions affiché / caché entre les composants
  • et d’un composant <AnimatePresence/> qui nous permettra de rendre plus fluide les effets de transition entre l’état ouvert et l’état fermé :

Library.tsx

<AnimateSharedLayout>
  <AnimatePresence>
    {selected !== null && (
      <OpenBook
        key={selected}
        bookColor={books[selected].color}
        exitBook={() => setSelected(null)}
        selectedBook={selected !== null && books[selected]}
      />
    )}
  </AnimatePresence>
  {books.map((book, index) => (
    <BookCover
      title={book.title}
      slug={book.slug}
      color={book.color}
      key={index}
      handleClick={() => {
        selected === index ? setSelected(null) : setSelected(index);
      }}
    />
  ))}
</AnimateSharedLayout>

On passe ensuite au composant vignette <BookCover/> qui est constitué d’une MotionBox sur lequel on ajoute l’attribut layoutId ‘book-+slug’.

BookCover.tsx

const BookCover = ({
 title,
 slug,
 color,
 handleClick,
}: IBookCover) => {
 return (
   <Box py={4} px={1} onClick={handleClick}>
     <MotionBox
       layoutId={`book-${slug}`}
       height="300px"
       width="200px"
       shadow="xl"
       bg={`${color}.700`}
       borderRadius="md"
       cursor="pointer"
       p={2}
     >

Sur le composant OpenBook on ajoute le même layoutId. Grâce à <AnimateSharedLayout/> si on ajoute un composant avec le même layoutId, alors que l’ancien est toujours affiché il est automatiquement caché, et le nouveau composant s’animera en partant de la position de l’ancien. En ajoutant <AnimatePresence/> on aura même une belle animation de sortie lors de la suppression de la popup qui se repliera automatiquement vers son composant vignette d’origine.

OpenBook.tsx

const OpenBook = ({ bookColor, exitBook, selectedBook }: IOpenBook) => {
 const AnimationComponent = selectedBook.homeAnimation
 const { slug, chapters, title, homeAnimation, author, abstract } = selectedBook

 return (
   <MotionCenter
     height="100vh"
     position="fixed"
     top={0}
     left={0}
     right={0}
     zIndex={1}
     onClick={exitBook}
   >
     <MotionBox
       layoutId={`book-${slug}`}
  	[...]
     >

En l’état, la transition entre le livre ouvert et le livre fermé fonctionne mais est un peu trop directe. Le texte apparaît quelques instants déformé avant de disparaître. Pour fluidifier le passage entre l’aspect ouvert et l’aspect fermé on ajoute type=”crossfade” sur le composant <AnimatedSharedLayout/>. Ainsi la transition entre les deux états sera entrecoupée d’un fondu enchaîné et le rendu sera plus progressif.

Library.tsx

const Library = () => {
 const [selected, setSelected] = useState<number | null>(null)

 return (
   <AnimateSharedLayout type="crossfade">

Et voilà notre animation est prête. En utilisant juste 2 composants AnimatedSharedLayout et AnimatePresence la transition se fait toute seule :

Animer un path SVG - motion.path

La doc sur les composants motion svg

Une fonctionnalité intéressante sur framer motion est la possibilité d’animer un tracé svg. Chaque page de détail des livres affiche un logo, animons donc la pantoufle de cendrillon :

PantoufleSvg.tsx

const pantoufleVariants = {
  initial: {
    pathLength: 0,
  },
  animate: {
    stroke: gold,
    pathLength: 1,
    transition: { duration: 2 },
  },
  hover: { scale: 1.15, strokeWidth: 20 },
  pressed: { fill: gold },
};

const PantoufleSvg = () => (
  <Center cursor="pointer">
    <motion.svg
      height="150"
      width="150"
      viewBox="1 -52 511.99905 511"
      animate="animate"
      initial="initial"
      whileHover="hover"
      whileTap="pressed"
    >
      <motion.path
        d="m83.53443,213.53755c1.69938,8.36176 4.58344,16.22896 8.57455,23.38304c12.99121,23.29966 19.08714,47.56258 19.08714,69.5453l0,90.86664c0,4.06299 3.29526,7.36112 7.36112,7.36112l29.44734,0c4.06299,0 7.36112,-3.29813 7.36112,-7.36112l0,-82.1282c0,-16.59414 3.82433,-33.24867 11.06756,-48.18656l0.67285,-1.08403c34.85603,29.01315 62.75926,65.99413 80.97515,107.40903c18.21589,41.4149 26.94858,31.35087 47.31817,31.35087l60.203,0c29.06778,0 51.91312,-0.60096 68.92132,-3.52816c8.44515,-1.45497 34.14292,-5.87452 34.14292,-25.91917c0,-11.33497 -9.39117,-27.79397 -23.9265,-41.92961c-9.75923,-9.49757 -30.31284,-26.03995 -58.2707,-30.15182c-2.17383,-0.31917 -4.37354,0.34793 -6.00391,1.81728c-7.29786,6.58762 -21.60027,11.36947 -34.00203,11.36947c-17.6293,0 -36.29662,-10.06402 -43.41908,-23.40892c-0.1409,-0.26167 -0.29617,-0.5147 -0.46583,-0.75912l-49.1412,-70.11175c-30.41349,-43.38745 -66.72161,-82.28347 -107.91224,-115.60403c-2.68565,-2.17383 -6.51861,-2.1882 -9.21864,-0.0345c-11.49025,9.15826 -21.07695,20.11367 -28.49557,32.56143c-14.30817,24.01852 -18.91462,51.40704 -14.32255,74.3214c0.01438,0.07476 0.02875,0.14952 0.04601,0.22141z"
        fill="transparent"
        strokeWidth="10"
        stroke="#ddd"
        strokeLinecap="round"
        strokeLinejoin="round"
        variants={pantoufleVariants}
      />
    </motion.svg>
  </Center>
);

Framer motion met à notre disposition un composant motion.path qui vient remplacer la balise <path> habituellement présente dans les svg et permet d’accéder à 3 propriétés des path svg :

  • pathLength
  • pathSpacing
  • pathOffset Pour donner l’impression d’un dessin progressif de la pantoufle on fait simplement varier le pathLength de 0 (aucun path) à 1 (longueur du path finale) dans notre animation entre l’état initial et animé. Et pour un effet bling bling princier on fait en même temps varier la couleur du path vers le doré. Avec motion.svg on peut remplacer la balise svg et passer les noms des variants à utiliser au hover / tap / animate. Et le tracé s'anime :

Scroller pour animer - useElementScroll

La doc sur le scroll

Les animations prennent tout leur sens quand elles répondent à une action de l’utilisateur sur la page. Framer motion propose un hook permettant de tracker le scroll de l’utilisateur sur la page useViewportScroll ou sur un élément en particulier useElementScroll. Ce hook renvoie plusieurs informations intéressantes mise à jour à chaque mouvement de scroll de l’utilisateur :

  • scrollX : la distance de scroll horizontal parcourue en pixel
  • scrollY : la distance de scroll vertical parcourue en pixel
  • scrollXProgress : le pourcentage de la distance scrollée sur la distance horizontale totale (entre 0 et 1)
  • scrollYProgress : le pourcentage de la distance scrollée sur la distance verticale totale (entre 0 et 1)

Comme nos animations sont incluses dans l’espace de lecture nous utiliserons useElementScroll pour activer une animation sur l’une des pages de Cendrillon. Nous allons faire apparaître et disparaître un nuage de fumée en fonction de l’état du scroll sur une zone de notre écran. Il nous faut donc une zone scrollable et une ref à passer à notre hook useElementScroll pour savoir où en est l’utilisateur dans son scroll on passe ensuite cette information à notr composant contenant l’animation :

CendrillonScroll.tsx

const CendrillonScroll = () => {
  const boxRef = useRef();
  const { scrollYProgress } = useElementScroll(boxRef);

  return (
    <>
      <Box
        ref={boxRef}
        style={{ overflow: 'auto', height: '55vh', width: '100%', position: 'absolute', zIndex: 1000 }}
        border="10px solid"
        borderColor="red.600"
        borderRadius={10}
        p={6}
      >
        <Text fontFamily="Dancing Script" fontSize="2xl">
          Scrollez pour activer la magie
        </Text>
        <ArrowDownIcon />
        <Box
          style={{
            display: 'flex',
            justifyContent: 'center',
            height: '300vh',
          }}
        ></Box>
      </Box>
      <Box>
        <Magic scrollYProgress={scrollYProgress} />
      </Box>
    </>
  );
};

L’animation fera ici directement varier le style de notre composant contenant notre image. Comme on souhaite faire apparaître puis disparaître notre image il faut convertir la valeur de scrollYProgress pour qu’elle passe de 0 à 1 puis 0. C’est l’intérêt du hook useTransform(input, transformer), on passe un array de valeurs pour scrollYProgress et un array de valeurs correspondantes à retourner pour notre constante opacity. On passe ensuite directement opacity dans le style de notre composant.

CendrillonScroll.tsx

const Magic = ({ scrollYProgress }) => {
  const opacity = useTransform(
    scrollYProgress,
    [0, 0.25, 0.35],
    [0, 1, 0],
  )
  return (
    <MotionCenter
      style={{
        opacity,
      }}
    >
      <Image src="/cendrillon/transformation.png" height="300px" />
    </Wrapper>
  )
}

En dupliquant le principe sur d’autres images on obtient une scène qui s’anime au scroll

Déplacer nos éléments avec drag

La doc sur le drag

Un autre moyen d’interaction intéressant pour l’utilisateur consiste à lui donner la possibilité de déplacer des éléments sur la page (drag). Là encore, Framer Motion simplifie la chose à merveille. Nous avons choisi de publier la version originale de Cendrillon qui est un peu plus dure que la version Disney. Pour ne pas choquer nos plus jeunes lecteurs nous allons donc créer un composant <Censored/> qui permettra de cacher les passages un peu trop violents en laissant la possibilité de dévoiler le texte en dessous en le déplaçant sur la page.

Censored.tsx

const MotionBox = motion.custom(Box);

const Censored = ({ content }: Iparagraph) => {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);
  useEffect(() => {
    if (ref?.current) {
      setHeight(ref.current.offsetHeight);
    }
  }, [ref]);

  return (
    <Box>
      <MotionBox
        p={4}
        drag
        dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
        bg="white"
        borderRadius="lg"
        border="4px solid"
        borderColor={gold}
        style={{
          cursor: 'grab',
        }}
        position="absolute"
        whileTap={{ cursor: 'grabbing' }}
        width="100%"
        minH={height}
      >
        <Circle>
          <Image src="/shocked.png" height="80px" />
        </Circle>
        <Box p={6} color={gold}>
          <Text fontSize="35px" fontFamily="Dancing Script">
            Attention
          </Text>
          <Text maxW="500px" margin="0 auto">
            Le texte original contient des passages à ne pas mettre sous les yeux de tous les conteurs... Déplacez ce
            bloc si vous l'osez !
          </Text>
        </Box>
      </MotionBox>
      <Box ref={ref}>
        <Text paddingY={2} textAlign="left">
          {content}
        </Text>
      </Box>
    </Box>
  );
};

Ici on utilise une MotionBox en précisant l’attribut drag. Framer motion donne la possibilité de forcer un drag vertical (drag=”y”) ou horizontal (drag=”y”), sans précisions le drag reste multidirectionnel. On souhaite également que notre composant de censure retrouve sa place originale quand l’utilisateur relâche le clic.

C’est le but de dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }} qui permet de spécifier l’espace dans lequel on peut faire glisser et déposer notre composant. Ici il est nul puisque l’on veut qu’il revienne automatiquement au centre une fois lâché. Et voilà on obtient une censure facilement contournable :

En plus d’être animée, notre application a maintenant gagné en interactivité. Framer Motion démontre de nouveau son efficacité en permettant de construire des animations synchronisées, répondant aux actions de l’utilisateur en deux temps trois props. Une facilité d’utilisation appréciable qui ouvre aux développeurs des possibilités infinies d’animation directement via React. Vous avez le bon outil, il n’y a plus qu’à créer !

Continuer la discussion sur Twitter

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub

Naviguez à vue