- Le lissage exponentiel est une technique simple qui peut être largement utilisée pour les animations qui doivent suivre en douceur une valeur cible, comme les boutons à bascule, les caméras, les éléments d’UI ou le volume audio
- La formule clé est
position += (target - position) * (1 - exp(- speed * dt)), et elle réagit naturellement aux changements de cible en ne s’appuyant que sur la position actuelle et la position cible
- Les déplacements linéaires et les easing classiques nécessitent souvent de gérer des clics en cours d’animation, des entrées caméra rapides, de grands
dt, des sauts, des tremblements ou des files d’attente d’entrée, alors que le lissage exponentiel réduit cette complexité avec une structure identique
1 - exp(- speed * dt) provient de la solution d’une équation différentielle résolue par la formule de mise à jour proportionnelle ; pour de petits dt, cela reste presque identique à la formule existante, tout en évitant les dépassements pour de grands dt
- On n’atteint pas mathématiquement la valeur cible de manière exacte, mais à cause de la précision en virgule flottante et du seuil minimal de changement perceptible par l’utilisateur, cela se comporte en pratique comme une animation terminée
Le problème visible avec un bouton à bascule
- La position du commutateur d’un bouton à bascule peut se calculer simplement, par exemple avec
turned_on ? max_x : min_x, mais quand l’état change, la position se téléporte instantanément et manque de vie
- Une animation linéaire s’implémente en mettant à jour la position à vitesse constante puis en limitant sa plage
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
- Un déplacement à vitesse constante rend la position linéaire en fonction du temps, ce qui peut donner une impression de mouvement raide
- On peut ajouter une fonction d’easing pour rendre le mouvement plus fluide
- smoothstep cubique classique :
3t² - 2t³
- easing par racine carrée :
sqrt(t)
smoothstep possède la symétrie 1 - f(t) = f(1 - t), ce qui permet d’utiliser le même code pour l’animation aller et retour
sqrt, lui, demande une formule différente selon la direction
- à l’activation :
sqrt(t)
- à la désactivation :
1 - sqrt(1 - t)
sqrt démarre vite puis ralentit en douceur à l’approche de la cible, mais même pour un simple toggle à deux états, il faut gérer de l’état supplémentaire comme t, la direction et le calcul d’easing
- Si l’utilisateur clique à nouveau au milieu de l’animation, une approche classique par easing peut créer une discontinuité avec un saut brusque de position
La formule du lissage exponentiel
- Le lissage exponentiel consiste à définir une position cible puis à tirer progressivement la position actuelle vers elle
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
- La quantité mise à jour est déterminée à partir de
target - position.x, c’est-à-dire l’écart entre la position actuelle et la position cible
1 - exp(- dt * speed) est le coefficient d’interpolation qui décide de la distance parcourue vers la cible à chaque frame
- Même si la cible change en cours de route, le mouvement repart immédiatement vers la nouvelle cible sans temps de progression séparé ni état de direction d’animation
- Dans l’exemple du bouton à bascule, on retrouve un comportement semblable à l’easing
sqrt — démarrage rapide puis ralentissement à l’approche de la cible — tout en réduisant les problèmes de saut lors d’un clic intermédiaire
Des avantages encore plus nets pour le déplacement de caméra
- Le même problème apparaît aussi avec une caméra qui se déplace sur une carte
- Une interpolation à vitesse constante implémentée simplement utilise le signe de la direction cible sur chaque axe
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
- Au point de fin d’animation, le signe de
target - position peut alterner entre positif et négatif, ce qui provoque un tremblement
- Pour l’éviter, il faut une fonction de mise à jour séparée qui limite
delta à une valeur maximale max_delta
float update(float & value, float target, float max_delta)
{
float delta = target - value;
delta = min(delta, max_delta);
delta = max(delta, -max_delta);
value += delta;
}
- Appliquer un easing cubique au déplacement de caméra complique la structure, car il faut mettre les événements de déplacement demandés dans une file et les traiter un par un
- Ignorer les entrées utilisateur pendant l’animation est très frustrant du point de vue de l’utilisateur
- Avec le lissage exponentiel, on peut gérer le déplacement de caméra avec presque le même code que pour le bouton à bascule
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
- Si l’utilisateur clique rapidement, l’écart entre la cible et la position actuelle grandit, donc la caméra accélère naturellement
Pourquoi 1 - exp(- speed * dt) ?
- Une mise à jour proportionnelle simple peut s’écrire ainsi
position += (target - position) * speed * dt;
- Cette formule fait se déplacer l’objet d’autant plus vite que l’écart entre la cible et la position actuelle est grand, sans nécessiter d’état supplémentaire au-delà de ces deux positions
- Écrite avec
lerp, on obtient la même chose
position = lerp(position, target, speed * dt);
- Quand
speed * dt est proche de 0, le mouvement est lent ; quand il est proche de 1, on s’approche rapidement de la cible
- Si
speed * dt dépasse 1, l’interpolation produit un dépassement de la cible
- Dans l’exemple, quand
speed = 220 et dt = 1 / 125, speed * dt devient supérieur à 1, ce qui fait apparaître des tremblements
- Si
speed * dt < 2, l’écart absolu peut encore diminuer, mais pour speed * dt > 2, le comportement cesse d’être utile
- On peut limiter le coefficient d’interpolation à
min(1, speed * dt), mais cela ne résout pas élégamment les cas où dt devient grand
- le code peut ralentir et faire chuter le framerate
- l’utilisateur peut changer d’onglet ou de fenêtre, le code se met alors en pause puis reprend avec un
dt de plusieurs secondes
- En simulation physique, on peut limiter les grands
dt ou les découper en plusieurs mises à jour, mais pour l’animation, il vaut mieux que les caméras et les boutons continuent de se comporter naturellement même avec un grand dt
La solution vue comme une équation différentielle
- Une expression de la forme
A += B * dt correspond en général à une résolution numérique de l’équation différentielle dA/dt = B
- La formule de mise à jour simple revient à résoudre l’équation suivante
d(position) / dt = (target - position) * speed
- En posant
x = position, a = target et c = speed, on obtient
dx / dt = (a - x) * c
- En résolvant directement cette équation, on obtient la forme suivante
x = x0 + (a - x0) * (1 - exp(-c * t))
- Donc si la formule correcte pour de petits
dt est position += (target - position) * speed * dt, alors la formule valable pour un dt arbitraire devient
position += (target - position) * (1 - exp(- speed * dt));
- Avec le développement de Taylor,
exp(x) ≈ 1 + x, donc pour de petits dt, on a 1 - exp(-speed * dt) ≈ speed * dt, ce qui redonne la formule simple d’origine
- Même si
speed * dt devient très grand, exp(-speed * dt) se rapproche de 0 et 1 - exp(...) se rapproche de 1, ce qui permet d’aller de manière stable vers une valeur proche de la cible
- La même formule peut aussi s’écrire avec
lerp
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));
Choisir la valeur de vitesse
- On pense souvent une animation en termes de durée, par exemple « se déplacer en 0,125 seconde », mais le lissage exponentiel n’atteint techniquement la cible exacte qu’après un temps infini
exp(- speed * time) diminue avec le temps sans jamais atteindre 0 ; ainsi, tant que la valeur initiale et la cible diffèrent, position n’est jamais mathématiquement exactement égale à target
- En pratique, on atteint soit les limites de précision des nombres à virgule flottante, soit une différence de position trop petite pour être visible, et l’animation semble alors terminée
- La signification de
speed est qu’en un temps 1 / speed, position s’est rapprochée de target d’un facteur exact de e = 2.71828...
- En usage réel, on choisit souvent
speed dans une plage de 5..50, et pour retrouver une sensation proche d’une animation linéaire ou cubique de même rapidité, une valeur d’environ 2 * speed pour le lissage exponentiel peut sembler appropriée
Lien avec le traitement du signal
- En recherchant « exponential smoothing » ou « exponential moving average », on tombe sur l’article Exponential smoothing de Wikipedia
- Quand
dt est fixe et que target change à chaque itération, la valeur se met à jour en fonction de l’indice d’itération d’une manière proche de celle-ci
factor = 1 - exp(- speed * dt)
- en général,
factor est choisi directement comme une valeur entre 0 et 1
- Le lissage exponentiel discret est l’analogue discret de la méthode utilisée en animation
- En traitement du signal aussi, on l’utilise parce qu’il fonctionne avec la seule moyenne courante, sans conserver une liste de valeurs passées ni un état complexe
- En audio numérique,
dt est généralement fixé à l’inverse de la fréquence d’échantillonnage, soit 1 / freq
1 commentaires
Avis de Hacker News
Il me semble que l’essentiel n’est pas assez couvert ici. Ce n’est pas simplement une autre courbe d’easing entre 0 et 1, ni
smoothstep(), mais une approche sans état qui traite régulièrement presque n’importe quelle entrée, ce qui la rend vraiment utile.Si vous avez déjà utilisé les transitions CSS, ce problème vous sera familier. Si l’on fixe la durée à 400 ms, pourquoi précisément 400 ms ? Cela ne devrait-il pas dépendre de la distance à parcourir ?
Comme d’autres l’ont dit, le lissage exponentiel a le problème de se rapprocher asymptotiquement de la destination sans jamais l’atteindre. Il existe la solution évidente consistant à arrêter l’animation quand le pas passe sous un seuil, mais ce n’est pas très élégant.
Quand j’ai utilisé une approche similaire pour le défilement inertiel, il m’a été utile d’ajouter un terme de pseudo-friction. Ce terme compense le terme exponentiel et se comporte en pratique comme une vitesse minimale. Exemple Desmos ici : https://www.desmos.com/calculator/98ufbuzxhj
Dà la destination. Le lissage exponentiel est une mise à jour d’Euler dedD/dt=-C*D, dont la solution estD(t)=A*exp(-C*t), donc elle tend asymptotiquement vers 0 sans l’atteindre.Il suffit de remplacer cela par une équation qui atteint 0 en un temps fini, par exemple
dD/dt=-C*sqrt(D). La solution se déplace comme une demi-quadratique, puis reste en place une fois 0 atteint. La mise à jour d’Euler de cette équation est elle aussi sans état, comme souhaité.En tant que développeur de jeux, je pense que pour la plupart des UI, un tween d’easing avec une durée prédéfinie est préférable. Mais quand on veut lisser un mouvement continu et imprévisible, dont le point de départ et le point d’arrivée ne sont pas clairs, cet autre type d’animation est très utile.
Par exemple lorsqu’un joueur fait glisser à la souris une tuile sur une grille et qu’on veut l’aligner sur celle-ci, ou lorsqu’on déplace une caméra comme dans l’exemple de l’article.
Dans ces cas-là, l’astuce d’interpolation exponentielle est très utile, mais pas largement connue. Beaucoup de jeux utilisent une interpolation linéaire plus incorrecte, puis l’animation se met à paraître complètement étrange quand quelqu’un l’exécute sur un moniteur 240 Hz, contrairement à l’époque où 60 fps était la norme.
Je suis donc content de voir cet article. Ce genre de savoir très spécifique est souvent transmis oralement, façon compagnonnage, d’un senior à un junior dans une équipe, ce qui le rend difficile d’accès.
J’aime l’article, mais je dirais que le jugement de l’auteur selon lequel
sqrtest meilleur qu’une fonction cubique pour un interrupteur à bascule est objectivement faux. Quand on regarde la manière dont un véritable interrupteur à bascule fonctionne généralement, une fonction cubique est un meilleur choix dans ce cas.Pensez à un disjoncteur électrique à la maison, ou aux interrupteurs qu’on voit souvent sur des synthés analogiques et du matériel audio. Ce sont des appareils qui recherchent une certaine esthétique ; même mon petit ampli guitare Hughes & Kettner a deux interrupteurs très agréables à actionner.
Ce type d’interrupteur présente d’abord une légère résistance, puis, à cause de la mécanique à ressort, bascule soudainement dans la nouvelle position. Ce comportement est mieux modélisé par une fonction cubique que par
sqrtou par un lissage exponentiel.Mis à part cette remarque mineure, l’article était très bon. Il montre bien que des animations bien utilisées, comme une fonction d’easing appropriée, peuvent améliorer l’expérience utilisateur, tandis qu’une implémentation peu soigneuse, comme dans l’exemple d’interpolation linéaire, peut être gênante et nuire à l’expérience.
[0] Cela dépend bien sûr du type d’interrupteur à bascule. Mais même le type que l’on voit sur des appareils comme le Minimoog présente ce comportement de « résistance puis bascule nette vers la nouvelle position », ce qui le rend agréable à manipuler. Pour être clair, ce n’est pas une façon de frimer avec mon matériel : je ne possède pas de Minimoog.
Je suis toujours étonné de voir à quel point de simples astuces non linéaires ajoutent souvent du plaisir aux interactions en ligne. Dans le cas de la perception des couleurs, elles sont aussi essentielles pour comprendre pourquoi deux couleurs ne sont pas suffisamment distinguables par certaines personnes.
Ce qui est étrange, c’est que les humains ne comprennent pas toujours bien l’accélération. Il ne faut pas fuir vers le haut d’une colline en croyant qu’un feu se déplace presque à vitesse constante comme sur un terrain plat. Le feu accélère en montant une pente.
Les enfants apprennent vite à quelle vitesse une balle lancée se déplace le long du sol, mais ne comprennent pas toujours à quelle vitesse elle ira au moment de leur frapper la main à cause de la gravité.
Je trouve intéressant que la majeure partie de cet article finisse par se ramener à l’easing. On dirait que chaque nouvelle génération doit le redécouvrir par elle-même.
Je me souviens avoir été fasciné à la fin des années 90 par les sites web expérimentaux de Yugo Nakamura. C’était l’un des premiers sites web que j’aie vus à utiliser librement l’easing pour donner une sensation organique : https://www.youtube.com/watch?v=NLt7Gwnt3WY
Pour une raison ou une autre, j’aimerais bien avoir un toggle de ce genre : quand on le touche ou qu’on clique dessus, il se déplace lentement jusqu’à environ 75 % de la cible, puis quand on relâche, il parcourt le reste d’un coup.
Je ne sais pas très bien ce que cela signifierait du point de vue UX. Cela pourrait vouloir dire que le réglage n’est réellement appliqué ou enregistré qu’à la fin.
Ou bien cela pourrait faire partie d’une boîte de dialogue « Êtes-vous vraiment sûr ? ». Le réglage serait appliqué pendant que l’on maintient le bouton, mais on pourrait revenir en arrière avec Escape avant qu’il ne s’enclenche complètement.
input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }J’aime cet article. J’avais écrit une technique presque exactement identique il y a une dizaine d’années ; à l’époque, je l’appelais
lazy-easy, et je l’utilise encore aujourd’hui.Parfois, on veut simplement une animation fluide sans avoir à gérer tout l’état : https://www.hailpixel.com/articles/lazy-animation-with-lazy-...
L’article lui-même est vraiment bon. La démo semble bien fonctionner dans Chrome, mais dans Firefox elle se fige pendant le défilement et le rendu de la page s’arrête complètement.
C’est en fait une assez bonne approche, et aussi une bonne preuve de concept pour les techniques d’animation et d’easing. Ça me fait beaucoup penser à Flickity.
https://metafizzy.co/blog/initial-demos/
https://metafizzy.co/blog/math-time-resting-position/
https://metafizzy.co/blog/particle-to-slider/
https://metafizzy.co/blog/flickity-begins/
En particulier cette démo : https://codepen.io/desandro/pen/myXdej
Cette technique n’est pas utile uniquement pour les interrupteurs. En réalité, on n’utiliserait même pas ce genre d’élément pour un interrupteur, et on ne lancerait pas non plus 20 boucles
requestAnimationFrameen parallèle sur tout le site. On n’y mettrait pas non plus volontairement un élément cassé.Il n’y a pas non plus d’optimisation pour arrêter le rendu quand le delta devient suffisamment petit, et il y a probablement des dizaines de petits ajustements supplémentaires à faire pour en faire quelque chose de niveau production.
À voir les réactions ici, on dirait que les gens n’ont pas lu l’article et font des suppositions, qu’ils ne voient pas la forêt à force de regarder les arbres, ou qu’ils sont tellement biaisés et cyniques qu’ils partagent des jugements non experts pour paraître intelligents.
Depuis quand HN est-il devenu comme Reddit ?
Quant à l’idée que HN soit devenu comme Reddit, il y a peut-être une ambiance de cynisme diffus envers la tech en général. Elle repose sur une méfiance vis-à-vis de la manière dont les gouvernements et les entreprises comptent utiliser les technologies puissantes qui émergent, et sur la crainte qu’elles soient construites dans l’intention de limiter et de gérer les individus plutôt que d’accroître les libertés individuelles.
On est tout près de l’essence du design émotionnel (https://en.wikipedia.org/wiki/Emotional_Design). Il y a beaucoup à dire même derrière une toute petite animation.