Ça dépend ; c'est un problème populaire, y'a des tas d'approches. Ce que je t'ai proposé, c'est :
- On a une fonction isElemInViewport qui renvoie true si l'élément passé est visible par l'utilisateur, faux sinon
- On a une fonction checkAnimation qui, lorsqu'elle est appelée, appelle la fonction précédente. Si elle renvoie faux, on ne fait rien.
---- Là, on vérifie si l'élément a une classe arbitraire, qui signifie que cet élément a déjà été animé. Par défaut donc, l'élément ne dispose pas de cette classe, et cette classe ne fait rien en elle-même (enfin, rien dans ce qui nous concerne pour l'instant, après t'en fais ce que tu veux de cette classe post-animation). Si cet élément a cette classe, c'est qu'il a déjà été animé, et donc on ne fait rien.
---- Si cet élément n'a pas cette classe, c'est qu'il n'a pas été animé. On peut donc l'animer. C'est donc ici que tu animes l'élément, soit tout en JS, soit en lui ajoutant, cette fois-ci, une classe qui est animée en CSS (qui accroche l'élément visé à des animations @keyframes, par exemple). En lui ajoutant cette classe animée, l'élément va s'animer à ce moment-là, et voilà. On oublie pas d'ajouter à la fin la classe arbitraire qui permet d'identifier que l'élément a été animé. (et comme tu t'en doute, il est possible de fusionner ces 2 classes (la classe marqueur présente si l'élément a été animé, et la classe qui anime l'objet) en une seule (si l'élément a déjà la classe qui anime, c'est qu'il a été animé), à condition que tu ne te serves pas de la classe marqueur (comme, par exemple, d'une classe 'post-animation').) C'était mon cas dans l'exemple que je t'ai donné donc on avait bien 2 classes différentes.
- On ajoute tout ça au gestionnaire d’événement scroll.