Interface utilisateur morphing élastique
Ce skill produit des interfaces où chaque changement d'état morphe : un indicateur d'onglet s'étire comme un ver en route vers l'onglet suivant, un bouton se compresse en cercle de chargement puis regonfle, une image de produit se soulève de sa carte, rétrécit en pastille et tombe dans le panier. Il a été distillé à partir de la construction d'une vitrine de 24 composants (dans assets/showcase.html) et des bugs et corrections de design découverts en cours de route.
L'apparence repose sur une idée : l'étirement a une cause. Un élément se déforme parce que ses parties ne sont pas d'accord (un bord arrive avant l'autre), parce qu'il se déplace rapidement (il s'allonge dans sa trajectoire), parce qu'il est comprimé (il gonfle de l'autre côté pour conserver son volume), ou parce qu'il vient d'être touché (il oscille). Un rebondissement aléatoire en boucle semble bon marché ; un étirement avec une cause ressemble à un matériau.
Étape 0, à chaque fois : auditer le code de l'utilisateur et confirmer
Avant de construire ou de modifier quoi que ce soit, découvrez ce qui, dans l'interface de l'utilisateur, peut devenir élastique, et laissez-le choisir. Les détails, critères de jugement et modèles de messages se trouvent dans references/audit.md ; la version courte :
- Trouvez le code : code collé, fichiers joints (sur claude.ai, listez
/mnt/user-data/uploads), le répertoire de travail ou le dépôt, un lien vers une artifact (lisez-le avec l'outil artifact), ou une source indiquée par l'utilisateur. S'il n'y en a pas, dites ce que vous avez cherché et proposez deux chemins : partager le code, ou construire quelque chose de nouveau à partir de zéro. - Scannez-le :
python scripts/audit_ui.py <chemins>rapporte la pile, le mouvement existant, la gestion manquante du mouvement réduit, et les candidats par catégorie d'interface avecfichier:ligne, chacun associé à un composant de la vitrine et une technique. - Lisez chaque résultat dans son contexte et ne conservez que les vrais candidats. Pour chacun, notez où il se trouve, ce qui se passe maintenant, la phrase de morphing qu'il deviendrait, l'effort (petit, moyen, grand) et tout risque (markup de bibliothèque de composants, une bibliothèque d'animation existante, un contrôle natif dont le sélecteur mobile serait perdu, des listes très longues).
- Priorisez par visibilité × fréquence × adéquation, regroupez les doublons (tous les boutons utilisant un
Boutonpartagé ne forment qu'un seul élément), limitez la liste à environ dix éléments, et listez tout ce que vous déconseillez avec la raison. - Demandez confirmation dans la langue de l'utilisateur : les propositions numérotées, puis une question les invitant à choisir les numéros, à prendre toutes les recommandations, ou à taper les parties supplémentaires qu'ils souhaitent voir élastiques et qui ne sont pas dans la liste. Utilisez un outil de choix cliquable si l'environnement en a un, sinon du texte brut. Terminez votre tour ; ne faites aucune modification avant la réponse.
- Construisez ce qu'ils ont choisi plus tout ce qu'ils ont ajouté, puis suivez le Flux de travail ci-dessous et faites un rapport par élément.
Si l'utilisateur a déjà nommé les parties qu'il souhaite, auditez quand même : mettez ses parties en premier, proposez le reste en option, et gardez la question courte.
Fichiers
Tous les chemins sont relatifs au dossier de ce skill (le dossier contenant ce SKILL.md), pas au projet de l'utilisateur. Résolvez-les par rapport au dossier du skill lorsque vous les lisez ou les exécutez.
| Chemin | Ce que c'est | Quand l'ouvrir |
|---|---|---|
assets/stretchy-core.js | Le moteur de ressort plus les helpers (paires d'arêtes, indicateur segmenté, échelle de volume, arêtes SVG gonflantes, réordonnancement FLIP, objets lancés) | À coller dans chaque page que vous construisez. Lisez-le une fois pour connaître les helpers. |
assets/starter.html | Squelette de page : tokens clair/sombre, police variable compatible thaï, gestion du mouvement réduit, le coeur intégré, un exemple de composant | Pour démarrer une nouvelle page |
assets/showcase.html | Les 24 composants fonctionnels de la construction originale. Chaque section JS commence par une bannière comme /* ---------- onglets ---------- */ | Quand une demande correspond à un composant du catalogue, faites grep -n "/\* ---------- " assets/showcase.html et lisez juste cette section plus son CSS |
references/audit.md | Comment trouver, scanner, juger et prioriser le code de l'utilisateur, et les modèles de messages de confirmation (thaï et anglais) | À l'étape 0, à chaque fois |
references/techniques.md | Les 11 techniques d'étirement en détail, avec code et réglages | Avant de construire quelque chose que vous n'avez jamais construit auparavant |
references/components.md | Catalogue : la recette de chaque composant, valeurs de ressort et pièges | Quand la demande nomme ou ressemble à un composant du catalogue |
references/new-components.md | Comment concevoir une version élastique de n'importe quel composant, plus 14 designs travaillés non présents dans la vitrine | Quand la demande est en dehors du catalogue |
scripts/audit_ui.py | Scanner pour le code de l'utilisateur : pile, mouvement existant, et candidats élastiques avec fichier:ligne | À l'étape 0, à chaque fois |
scripts/check_page.py | Vérification en navigateur sans tête : erreurs JS, débordement latéral sur téléphone et bureau, éléments invisibles volant les clics, et feuilles de contact des images d'animation | Après chaque construction, avant de remettre quoi que ce soit |
Si un skill de design visuel est disponible (par exemple frontend-design ou ui-ux-pro-max), lisez-le aussi. Ce skill décide de l'apparence de la page (palette, typographie, mise en page) ; celui-ci décide de son mouvement. Un polissage du mouvement ne peut pas sauver une page d'apparence générique.
Flux de travail (après la confirmation de l'utilisateur)
- Écrivez la phrase de morphing pour chaque composant. "Quand déclencheur, forme A devient forme B, et elle s'étire parce que cause." Exemple : "Quand un onglet est cliqué, l'indicateur se déplace vers lui, et il s'étire parce que son bord avant est plus rigide que son bord arrière." Si vous ne pouvez pas remplir la cause, choisissez-en une dans le tableau ci-dessous. Cette phrase est le design ; tout le reste est implémentation.
- Choisissez la/les technique(s) dans le tableau. La plupart des composants utilisent un étirement principal plus un petit rebond de feedback.
- Définissez les états de repos sous forme de nombres : largeurs, hauteurs, rayons, positions, progression de 0 à 1. Les ressorts animent des nombres et vous les écrivez dans
styledansonUpdate. Les transitions CSS ne gèrent que la couleur et l'opacité (et les fondus courts de contenu). - Planifiez le contenu. Le texte et les icônes ne doivent pas se redistribuer ou se comprimer pendant que le conteneur morphe. Donnez à chaque état de contenu sa propre couche à sa taille finale, ancrée (généralement centrée), et faites un fondu : sortie rapide (≈150 ms), entrée après un délai de ≈100-150 ms avec une légère mise à l'échelle ou un flou. Le
overflow:hiddendu conteneur révèle le contenu en grandissant. - Construisez à partir de
assets/starter.htmlavec le coeur intégré. Écrivez les styles directement dans les callbacks de ressort ; ne déclenchez jamais un rendu de framework par image. - Passez en revue l'accessibilité et l'interruption (liste de contrôle ci-dessous).
- Testez et regardez. Exécutez
scripts/check_page.py, puis capturez des feuilles de contact des interactions importantes et regardez-les réellement. L'animation d'ajout au panier dans la construction originale "fonctionnait" mais avait l'air bon marché jusqu'à ce que les images soient inspectées ; les correctifs sont dans "Pour un rendu de qualité".
D'où vient l'étirement
| Situation | Technique (détails dans references/techniques.md) | Observé dans |
|---|---|---|
| Quelque chose se déplace entre des emplacements | Paire d'arêtes : deux ressorts pour les bords avant et arrière, le bord avant rigide, inversé selon la direction | Onglets, points de carrousel, toggles, pastilles de choix, point radio, stepper, surlignage de dropdown, capsule d'évaluation, nav de barre latérale |
| Un conteneur change de taille ou de forme | Axes séparés : ressorts distincts pour la largeur, la hauteur, le rayon avec différentes rigidités, de sorte qu'un axe mène | Île dynamique, FAB vers formulaire, modale depuis un bouton, dropdown, bouton d'ajout au panier |
| Quelque chose devient plus étroit ou plus large rapidement | Préservation de volume : mettre à l'échelle l'autre axe par la vélocité, scaleY = clamp(1 − v/k) | Bouton Envoyer, champ de recherche, largeur de dropdown |
| Quelque chose se déplace rapidement | Étirement par vélocité : allonger dans la direction du mouvement, amincir perpendiculairement | Curseur de slider, élément de panier lancé, cartes de carrousel (inclinaison), infobulle |
| Une grande surface se déplace | Bords gonflants : dessiner la surface comme un chemin SVG dont le point de contrôle du bord est décalé par −vélocité | Barre latérale, feuille inférieure |
| Une partie pend d'une partie en mouvement | Suiveur en retard : un ressort plus mou chasse le principal ; utiliser l'écart pour l'inclinaison ou le décalage | Bulle de valeur de slider, texte intérieur d'accordéon |
| Feedback pour un tap ou une arrivée | Coup de pied : ajouter de la vélocité à un ressort au repos à 0, il oscille et s'installe | Case à cocher, pastilles, panier, étoiles, badge, secousse de limite de quantité |
| Des blobs se rejoignent ou se séparent | Filtre Gooey : flou plus seuil alpha, source nette composée par-dessus | Menu Gooey |
| Un élément devient un autre élément | Transformation de conteneur : commencer au rectangle du déclencheur, ressortir vers le rectangle cible, fondu de contenu | Modale, FAB, lancer dans le panier |
| Le changement nécessite une pause entre | Morphing séquencé à travers une forme intermédiaire porteuse de sens (spinner, coche) | Bouton Envoyer, bouton d'ajout au panier |
| Glissement au-delà d'une limite, ou réorganisation de liste | Bande élastique (diviser le dépassement par ≈3) et FLIP avec ressorts qui conservent la vélocité | Carrousel, feuille, liste de produits triée |
Le moteur en un paragraphe
Spring est un oscillateur harmonique amorti intégré avec Euler semi-implicite en 8 sous-étapes par image (delta d'image plafonné à 1/30 s pour qu'un onglet en arrière-plan ne puisse pas l'exploser). to(cible) recible sans réinitialiser la vélocité, ce qui rend les clics rapides continus au lieu de redémarrer. set(x) saute sans animation, drag(x, v) suit un pointeur tout en enregistrant la vélocité, kick(v) injecte une impulsion. L'amortissement est multiplié par un facteur bounce global à chaque tick, de sorte qu'un seul contrôle au niveau de la page (doux 1.8, rebondissant 1, élastique 0.5) re-règle chaque composant en direct. Sous prefers-reduced-motion: reduce, to() saute à la cible et déclenche onRest immédiatement, donc la logique qui attend une animation s'exécute quand même.
Rapport d'amortissement ζ = c / (2√k). Les changements d'état de l'interface se sentent bien autour de ζ 0.6-0.8 ; feedback ludique à 0.3-0.5 ; au-dessus de 1 rien ne dépasse. Les préréglages utilisés dans la vitrine :
| Rôle | k | c | Notes |
|---|---|---|---|
| Bord avant d'une paire d'arêtes | 420-520 | 30-34 | Arrive en premier |
| Bord arrière | 140-170 | 17-19 | Retarde, ce qui crée l'étirement |
| Largeur du conteneur (morphing) | 240-320 | 18-24 | |
| Hauteur du conteneur (morphing) | 150-170 | 15-17 | Plus lent que la largeur, donc la forme s'étire latéralement d'abord |
| Suiveur de pointeur (slider) | 650 | 42 | Serré, mais a encore de la vélocité à lire |
| Suiveur en retard (bulle) | 150 | 11 | Volontairement lâche |
| Feedback par coup de pied | 380-420 | 12-16 | Coup de pied avec 6-10 |
| Hauteur avec dépassement visible (accordéon) | 210 | 14 |
Règles empiriques
- Limitez chaque déformation. Contrôles : préservation de volume dans ±7-15 %. Petits objets rapides (une pastille lancée, un curseur de slider) : jusqu'à 35-70 % d'étirement. Taille toujours fixée à un minimum positif, car un ressort sous-amorti peut descendre en dessous de zéro.
- Échangez la rigidité selon la direction. Dans une paire d'arêtes, le bord faisant face à la direction du déplacement reçoit le ressort rigide. Sans l'échange, l'indicateur s'étire correctement dans un sens et se comprime dans l'autre.
- Minuterie opposée au retour. Si la largeur mène la hauteur à l'ouverture, laissez la hauteur mener la largeur à la fermeture. La forme retrace alors un chemin différent, ce qui se lit comme élastique plutôt que comme un rembobinage.
- Ressorts pour les changements d'état, courbes temporelles pour les chemins dont la forme compte. Un objet lancé a besoin d'un arc prévisible (Bézier quadratique, progression ease-in-out, environ 800 ms). Des ressorts x/y indépendants donnent des chemins errants. Dérivez l'étirement de la vélocité image par image dans les deux cas.
- Gardez le contenu droit. Pour étirer quelque chose dans une direction arbitraire sans faire pivoter son image ou son texte, utilisez
rotate(a) scale(1+s, 1−s·0.45) rotate(−a). - Un mouvement audacieux par composant. Le reste du composant reste calme : un changement de couleur, un court fondu.
- Calme au repos. Rien n'oscille tout seul. Au plus un moment orchestré au chargement de la page (la vitrine ouvre l'île une fois après 900 ms).
Pour un rendu de qualité (leçons de la construction)
La première version d'ajout au panier faisait voler une boule de couleur unie le long de deux ressorts indépendants, rétrécissait le bouton en un ovale violet vide et écrasait le panier. L'utilisateur a dit que ce n'était pas joli. Ce qui l'a corrigé se généralise :
- Déplacez la vraie chose. Clonez l'image du produit, soulevez-la légèrement avec une ombre, puis rétrécissez-la en une pastille ronde qui montre encore l'image pendant qu'elle vole. Des points abstraits ressemblent à une sortie de débogage.
- Les formes intermédiaires doivent porter un sens. Un bouton rétréci est un blob vide à moins qu'il n'affiche quelque chose : un spinner, une coche qui se dessine, un compteur. Le bouton d'ajout devient maintenant un cercle vert avec une coche, puis grandit en stepper − n +.
- Volume subtil sur les contrôles. Un renflement vertical de ±18 % sur un bouton ressemblait à un œuf en forme de pilule. ±7 % se lit comme un matériau souple.
- L'arrivée a besoin d'un receveur. L'icône du panier plonge et s'incline, un anneau pulse vers l'extérieur, le badge surgit. Quelque chose doit visiblement attraper ce qui a été lancé.
- Ne coupez pas les indicateurs d'état. Un badge à l'intérieur d'une pilule
overflow:hiddenen morphing est coupé ; mettez-le dans un wrapper à l'extérieur du clip. - Ne répétez pas l'information. La pilule du panier affichait "3 ชิ้น 195 บาท" à côté d'un badge indiquant 3. Elle n'affiche plus que le total.
- Dessinez les illustrations à un format fixe. Les formes CSS positionnées en pourcentage à l'intérieur d'une tuile s'étirent lorsque la tuile s'élargit. Utilisez un SVG en ligne avec un viewBox, centré dans la tuile.
Liste de contrôle pour l'accessibilité et la robustesse
- Respectez le mouvement réduit (le coeur le fait ; assurez-vous que les keyframes et transitions CSS sont également coupés dans une requête média
prefers-reduced-motion). - Utilisez de vrais
button,input,role="switch",role="tab",role="slider", etc. avec le bon étataria-*, mis à jour dans le même appel qui démarre l'animation. - Le contenu caché par un morphing reçoit
inert(et ne doit pas intercepter les clics : donnez aux couches invisiblespointer-events:none, puis réactivez-le avec l'état visible). - Déplacez le focus délibérément lorsque le contrôle ciblé disparaît dans un morphing : vers le contrôle de remplacement, de retour au déclencheur après la fermeture d'une boîte de dialogue, et seulement après que l'élément est à nouveau visible (un élément
visibility:hiddenne peut pas recevoir le focus). - Testez avec le clavier : Tab, Entrée, Échap doivent fonctionner à chaque étape.
- Vérifiez avec
scripts/check_page.pyen mode mobile (320 px large) et bureau (1280 px).