DS

Animations

Patterns d'animation utilises dans Speedcube Master : Framer Motion, animations CSS, transitions de page, micro-interactions et performance.

Framer Motion

Framer Motion est la librairie principale pour les animations complexes. Elle permet des animations declaratives avec motion.div, des transitions de presence avec AnimatePresence, et des animations de layout.

"use client";

import { motion, AnimatePresence } from "framer-motion";

// Animation d'entree basique
function FadeInCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3, ease: "easeOut" }}
    >
      {children}
    </motion.div>
  );
}

// Animation de sortie avec AnimatePresence
function SolveList({ solves }: { solves: Solve[] }) {
  return (
    <AnimatePresence mode="popLayout">
      {solves.map((solve) => (
        <motion.div
          key={solve.id}
          initial={{ opacity: 0, scale: 0.95 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.95, transition: { duration: 0.15 } }}
          transition={{ duration: 0.25, ease: "easeOut" }}
          layout
        >
          <SolveCard solve={solve} />
        </motion.div>
      ))}
    </AnimatePresence>
  );
}

// Animation de layout (reordonnancement fluide)
function ReorderableList({ items }: { items: Item[] }) {
  return (
    <div className="space-y-2">
      {items.map((item) => (
        <motion.div
          key={item.id}
          layout
          transition={{
            layout: { duration: 0.3, ease: "easeInOut" },
          }}
        >
          <Card>{item.content}</Card>
        </motion.div>
      ))}
    </div>
  );
}

Animations CSS

Pour les animations simples et performantes, on prefere les animations CSS pures. Elles sont definies dans les styles globaux et appliquees via des classes utilitaires.

/* Animations CSS definies dans globals.css */

/* Fade in avec translation vers le haut */
@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-fade-in-up {
  animation: fade-in-up 0.3s ease-out forwards;
}

/* Effet de flottement (decoratif) */
@keyframes float {
  0%, 100% { transform: translateY(0px); }
  50% { transform: translateY(-6px); }
}

.animate-float {
  animation: float 3s ease-in-out infinite;
}

/* Shimmer pour les skeletons */
@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.animate-shimmer {
  background: linear-gradient(
    90deg,
    transparent 25%,
    hsl(var(--muted-foreground) / 0.1) 50%,
    transparent 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
}

/* Gradient anime (fond decoratif) */
@keyframes gradient-x {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

.animate-gradient-x {
  background-size: 200% 200%;
  animation: gradient-x 3s ease infinite;
}

/* Glow pulsant (focus, notifications) */
@keyframes glow {
  0%, 100% { box-shadow: 0 0 5px hsl(var(--primary) / 0.3); }
  50% { box-shadow: 0 0 20px hsl(var(--primary) / 0.6); }
}

.animate-glow {
  animation: glow 2s ease-in-out infinite;
}

Transitions de page

Les transitions entre les pages durent entre 250ms et 350ms avec un easing ease-out. On utilise un wrapper Framer Motion dans le layout pour animer les changements de route.

"use client";

import { motion } from "framer-motion";
import { usePathname } from "next/navigation";

// Variantes de transition de page
const pageVariants = {
  initial: {
    opacity: 0,
    y: 8,
  },
  animate: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.3,    // 300ms
      ease: "easeOut",
    },
  },
  exit: {
    opacity: 0,
    y: -8,
    transition: {
      duration: 0.25,   // 250ms
      ease: "easeIn",
    },
  },
};

// Wrapper de transition de page
export function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();

  return (
    <motion.div
      key={pathname}
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
    >
      {children}
    </motion.div>
  );
}

// Utilisation dans le layout
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <AnimatePresence mode="wait">
      <PageTransition>{children}</PageTransition>
    </AnimatePresence>
  );
}

Micro-interactions hover

Les etats hover utilisent des transitions de 120ms pour un retour instantane. On anime principalement les couleurs, opacites et transformations legeres (scale, translate).

// Transition de couleur sur hover (120ms)
<button className="bg-primary text-primary-foreground
  hover:bg-primary/90
  transition-colors duration-[120ms]">
  Action
</button>

// Scale subtil sur hover
<Card className="transition-transform duration-[120ms] hover:scale-[1.02]">
  {/* Contenu */}
</Card>

// Elevation sur hover (shadow)
<Card className="transition-shadow duration-[120ms]
  hover:shadow-lg hover:shadow-primary/5">
  {/* Contenu */}
</Card>

// Translation sur hover
<Link className="inline-flex items-center gap-1 group">
  En savoir plus
  <ArrowRight className="h-4 w-4 transition-transform duration-[120ms]
    group-hover:translate-x-0.5" />
</Link>

// Combinaison avec Framer Motion
<motion.button
  whileHover={{ scale: 1.02 }}
  whileTap={{ scale: 0.98 }}
  transition={{ duration: 0.12 }}
>
  Bouton interactif
</motion.button>

Animations en cascade (stagger)

Les animations en cascade revelent les elements un par un avec un delai incremental. Elles sont utilisees pour les listes, les grilles de cartes et les menus.

// Stagger avec Framer Motion
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.08, // 80ms entre chaque enfant
      delayChildren: 0.1,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 15 },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.3,
      ease: "easeOut",
    },
  },
};

function StaggeredGrid({ items }: { items: Item[] }) {
  return (
    <motion.div
      className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.div key={item.id} variants={itemVariants}>
          <Card>{item.content}</Card>
        </motion.div>
      ))}
    </motion.div>
  );
}

// Stagger avec CSS (classes utilitaires)
// Utiliser animation-delay via les classes stagger-N
<div className="space-y-2">
  <div className="animate-fade-in-up stagger-1">Element 1</div>
  <div className="animate-fade-in-up stagger-2">Element 2</div>
  <div className="animate-fade-in-up stagger-3">Element 3</div>
  <div className="animate-fade-in-up stagger-4">Element 4</div>
  <div className="animate-fade-in-up stagger-5">Element 5</div>
</div>

// Definition des classes stagger dans globals.css
.stagger-1 { animation-delay: 0.05s; }
.stagger-2 { animation-delay: 0.10s; }
.stagger-3 { animation-delay: 0.15s; }
.stagger-4 { animation-delay: 0.20s; }
.stagger-5 { animation-delay: 0.25s; }

Performance

Les animations doivent rester fluides a 60fps. Voici les regles de performance a respecter pour eviter les janks et re-paints couteux.

Bonne pratiqueAnimer uniquement transform et opacity (composites, GPU-accelerees)
Bonne pratiqueUtiliser will-change: transform sur les elements animes frequemment
Bonne pratiquePromouvoir les couches GPU avec transform: translateZ(0) si necessaire
A eviterAnimer width, height, top, left (declenchent un reflow)
A eviterAnimer box-shadow directement (utiliser un pseudo-element avec opacity a la place)
// Bon : animation GPU-acceleree
<motion.div
  animate={{ x: 100, opacity: 0.5 }}
  transition={{ duration: 0.3 }}
/>

// Bon : will-change pour les elements animes en continu
<div className="will-change-transform animate-float">
  {/* Element decoratif qui flotte en continu */}
</div>

// Mauvais : animation de proprietes layout
<motion.div
  animate={{ width: "200px", height: "100px" }}  // Reflow !
/>

// Alternative : utiliser scale au lieu de width/height
<motion.div
  animate={{ scaleX: 2, scaleY: 1.5 }}  // GPU, pas de reflow
/>

Mouvement reduit (accessibilite)

Les utilisateurs qui ont active prefers-reduced-motion dans leurs preferences systeme doivent voir les animations desactivees ou reduites. Framer Motion le gere automatiquement, et pour le CSS on utilise la media query.

/* Desactiver les animations CSS pour prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

// Avec Tailwind (media query integree)
<div className="animate-fade-in-up motion-reduce:animate-none">
  {/* Animation desactivee si prefers-reduced-motion */}
</div>

// Framer Motion respecte automatiquement prefers-reduced-motion
// Mais on peut aussi le gerer manuellement
import { useReducedMotion } from "framer-motion";

function AnimatedComponent() {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
    >
      Contenu anime
    </motion.div>
  );
}

Reference des durees

120msHover, focus, micro-interactions (instantane)
150msTooltips, menus, popovers (apparition rapide)
250-350msTransitions de page, modales, panneaux (perceptible)
500ms+Animations decoratives, loaders, animations en boucle
ease-outEasing par defaut pour les entrees (deceleration naturelle)
ease-inEasing pour les sorties (acceleration vers la disparition)
ease-in-outEasing pour les animations en boucle (mouvement naturel)