145 lines
5.7 KiB
Markdown
145 lines
5.7 KiB
Markdown
---
|
||
name: motion-spring-physics
|
||
description: >-
|
||
Implement physical, tactile micro-interactions and layout transitions using Spring Physics
|
||
(Motion / Framer Motion, FLIP technique via layoutId, CSS @starting-style, View Transitions API)
|
||
with strict compliance with prefers-reduced-motion.
|
||
---
|
||
|
||
# Motion & Spring Physics Architecture
|
||
|
||
Ce skill régit l'animation interactive haute fidélité. Il remplace les animations rigides basées sur des durées fixes (`transition: all 0.3s ease`) par des **ressorts physiques réels** (*Spring Physics*), conférant aux interfaces un toucher organique, réactif et interruptible caractéristique des applications comme Linear, Raycast ou macOS.
|
||
|
||
---
|
||
|
||
## 🎯 Déclencheurs d'Activation
|
||
|
||
Activez ce skill dès que :
|
||
1. Vous animez des éléments interactifs (boutons, modales, tiroirs, onglets, menus accordéons).
|
||
2. Vous concevez des transitions d'état de liste ou de réorganisation (glisser-déposer, suppression, filtrage).
|
||
3. Vous implémentez un indicateur de sélection flottant ou un curseur d'onglets partagé (`layoutId`).
|
||
4. Vous créez des micro-interactions au survol ou à la pression tactile (`whileHover`, `whileTap`).
|
||
5. Vous devez garantir l'accessibilité des animations face au critère WCAG 2.3.3 (`prefers-reduced-motion`).
|
||
|
||
---
|
||
|
||
## 🏎️ 1. Les Principes de la Physique de Ressort (*Springs*)
|
||
|
||
Contrairement aux courbes de Bézier cubiques (`ease-in-out`) qui forcent un temps fixe indépendamment de la distance parcourue, un ressort physique calcule la trajectoire en temps réel :
|
||
- **Stiffness (Raideur)** : Vitesse de rappel du ressort. Une raideur élevée (300–500) rend le contrôle vif et immédiat.
|
||
- **Damping (Amortissement)** : Friction qui freine le mouvement. Un amortissement bien réglé (20–30) évite les oscillations oscillatoires excessives (« effet cartoon »).
|
||
- **Mass (Masse)** : Inertie de l'élément (0.5 à 1.0 par défaut).
|
||
|
||
### Les 3 Profils de Ressort Standards
|
||
|
||
| Profil | Cas d'Usage | Paramètres Recommandés |
|
||
| :--- | :--- | :--- |
|
||
| **Snappy / Micro-tactile** | Boutons, toggles, badges, chips, icones | `{ type: 'spring', stiffness: 500, damping: 30, mass: 0.5 }` |
|
||
| **Smooth / Layout** | Modales, volets latéraux, accordéons, cartes | `{ type: 'spring', stiffness: 350, damping: 28, mass: 0.8 }` |
|
||
| **Gentle / Page Transition** | Entrées de page, vues séquentielles | `{ type: 'spring', stiffness: 200, damping: 24, mass: 1.0 }` |
|
||
|
||
---
|
||
|
||
## 💫 2. Transitions Partagées avec `layoutId` (Technique FLIP)
|
||
|
||
Pour un indicateur d'onglet ou un sous-lignage qui glisse d'un bouton à l'autre sans saut visuel :
|
||
|
||
```tsx
|
||
import { motion } from 'framer-motion';
|
||
import { useState } from 'react';
|
||
|
||
const tabs = ['Vue d\'ensemble', 'Analytique', 'Paramètres'];
|
||
|
||
export const TabNavigation = () => {
|
||
const [activeTab, setActiveTab] = useState(tabs[0]);
|
||
|
||
return (
|
||
<nav className="flex gap-2 p-1.5 rounded-xl bg-[oklch(0.18_0.01_260)] border border-[oklch(1_0_0/0.08)]">
|
||
{tabs.map((tab) => {
|
||
const isActive = activeTab === tab;
|
||
return (
|
||
<button
|
||
key={tab}
|
||
onClick={() => setActiveTab(tab)}
|
||
className="relative px-4 py-1.5 text-sm font-medium rounded-lg text-white/80 hover:text-white transition-colors"
|
||
>
|
||
{/* Pilule d'arrière-plan animée en FLIP automatique */}
|
||
{isActive && (
|
||
<motion.div
|
||
layoutId="active-indicator"
|
||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||
className="absolute inset-0 rounded-lg bg-[oklch(0.28_0.02_260)] shadow-sm"
|
||
/>
|
||
)}
|
||
<span className="relative z-10">{tab}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## ♿ 3. Respect Impératif de `prefers-reduced-motion`
|
||
|
||
Chaque animation doit se désactiver instantanément pour les utilisateurs sensibles au mal des transports ou ayant activé la réduction de mouvement dans leur OS :
|
||
|
||
```tsx
|
||
import { motion, useReducedMotion } from 'framer-motion';
|
||
|
||
export const AccessibleCard = ({ children }: { children: React.ReactNode }) => {
|
||
const shouldReduceMotion = useReducedMotion();
|
||
|
||
return (
|
||
<motion.div
|
||
initial={shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
|
||
transition={
|
||
shouldReduceMotion
|
||
? { duration: 0 }
|
||
: { type: 'spring', stiffness: 300, damping: 25 }
|
||
}
|
||
whileHover={shouldReduceMotion ? {} : { scale: 1.015 }}
|
||
whileTap={shouldReduceMotion ? {} : { scale: 0.985 }}
|
||
>
|
||
{children}
|
||
</motion.div>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 4. Alternatives CSS Modernes (Sans Dépendance JS)
|
||
|
||
Pour les transitions simples au chargement d'éléments, privilégiez `@starting-style` et la View Transitions API :
|
||
|
||
```css
|
||
/* Entrée fluide pure CSS d'un dialog ou popover */
|
||
dialog[open] {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
transition: opacity 0.2s ease-out, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), display 0.2s allow-discrete;
|
||
}
|
||
|
||
@starting-style {
|
||
dialog[open] {
|
||
opacity: 0;
|
||
transform: scale(0.96);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 Checklist de Conformité Motion
|
||
|
||
- [ ] Aucun mouvement décoratif continu en boucle (pas de floating infini qui distrait).
|
||
- [ ] Les micro-interactions au clic utilisent `active:scale-[0.98]` ou un ressort réactif.
|
||
- [ ] `useReducedMotion()` (ou la media query `@media (prefers-reduced-motion: reduce)`) ramène la durée à 0.
|
||
- [ ] Les animations de changement de layout (`layoutId`) ne causent aucun reflow de texte brutal.
|
||
- [ ] La performance d'animation cible 60fps/120fps en animant exclusivement `transform` et `opacity`.
|