API canvas et boucle d'animation
Essayons d'effectuer une première animation simple avec un effet de parallaxe qui serait généré par le mouvement d'une caméra virtuelle manipulée par l'utilisateur. Nous allons créer des cercles dans un plan cartésien (modifié par la suite en tore plat) et les faire bouger selon les touches du clavier pressées par l'utilisateur.
Canvas
Ajoutez une balise canvas dans le HTML de votre page. Vous pouvez la styliser en CSS selon votre bon vouloir. Voici un exemple pour lui donner la taille de la fenêtre d'affichage (viewport en anglais) :
body {
margin: 0;
}
canvas {
display: block;
width: 100dvw;
height: 100dvh;
background: black;
}
Ensuite, récupérez le contexte graphique dans votre programme principal et donnez-lui la même taille que la viewport :
const ctx = document.querySelector('canvas').getContext('2d');
ctx.canvas.width = ctx.canvas.clientWidth;
ctx.canvas.height = ctx.canvas.clientHeight;
Cercles
Créez une classe Circle dans un dossier /class dans vos sources. Ajoutez un constructeur pour vos cercles. Les cercles seront dans un plan. Ils auront les propriétés suivantes : une position, un rayon, une couleur et une vélocité. Ajoutez une méthode draw qui prendra en paramètre un contexte de canvas (un paramètre nommé ctx par exemple).
Pour représenter la position et la vélocité, utilisez la classe Vector2.js fournie. Placez ce fichier dans votre dossier class et importez-le dans les modules qui en ont besoin. La vélocité est un vecteur dont la direction indique celle du mouvement et dont la longueur indique la vitesse. Dans ce TP, ses composantes seront exprimées en pixels par milliseconde, puisque le Δt sera fourni en millisecondes. Les méthodes add, multiply, normalize et setAngle modifient le vecteur sur lequel elles sont appelées. La méthode clone permet d'en créer une copie indépendante : pensez à l'utiliser pour calculer un déplacement sans modifier la vélocité du cercle.
Utilisez la méthode arc pour le dessin des cercles. Quelques repères pour votre méthode draw :
- beginPath commence un nouveau chemin pour éviter de réutiliser celui des cercles précédents.
- arc ajoute l'arc du cercle au chemin ; les angles sont exprimés en radians.
- fillStyle définit la couleur de remplissage.
- fill remplit la forme définie par le chemin.
- closePath ferme le chemin en reliant son dernier point à son premier point. Cette méthode ne dessine rien à elle seule et est facultative pour un cercle complet rempli avec fill.
Puis, ajoutez une méthode update pour appliquer un mouvement aux cercles dans le plan selon leur vélocité et un intervalle de temps en millisecondes (l'intervalle de temps sera reçu en paramètre).
Génération de cercles aléatoires
Dans votre programme principal, créez un tableau pour le stockage des cercles. Générez ~300 cercles de positions et de couleurs pseudo-aléatoires. Pour que vos couleurs ne soient pas trop ternes, vous pouvez utiliser HSL ou OKLCH. Pour les rayons, essayez de trouver une astuce pour que votre code génère plus de petits cercles que de grands cercles. Afin de tester tout cela, dessinez tous les cercles dans le canvas grâce à la méthode draw codée au point précédent.
Animation
La méthode requestAnimationFrame va nous permettre de faire notre boucle d'animation. Comme nous l'avons vu en cours, pour que la vitesse des mouvements soit indépendante du taux de rafraîchissement (framerate), il nous faut calculer la différence de temps (Δt) entre deux rafraîchissements (frames). Comme vous pouvez le lire dans la documentation, requestAnimationFrame nous donne déjà un timestamp en millisecondes. Il nous suffit donc de faire la différence avec le timestamp de la frame précédente.
Pour tester votre boucle d'animation, faites les opérations suivantes à chaque frame :
- Calculer le Δt
- Déplacer les cercles (méthode update) en fournissant le Δt
- Effacer le canvas en réaffectant ses dimensions comme ci-dessous
- Dessiner tous les cercles (méthode draw)
ctx.canvas.width = ctx.canvas.clientWidth;
ctx.canvas.height = ctx.canvas.clientHeight;
Ces affectations adaptent la taille du canvas à sa taille d'affichage et effacent son contenu, même si les dimensions n'ont pas changé. Elles réinitialisent aussi l'état du contexte graphique : chemins, couleurs, transformations, ombres, etc.
Note : Comme discuté en cours, si vous voulez un comportement déterministe, cette solution de boucle d'animation n'est pas adéquate. Nous utiliserons un meilleur système par la suite.
Pause et reprise automatiques
Lorsque l'onglet passe en arrière-plan, le navigateur suspend généralement les appels à requestAnimationFrame. À la reprise, la différence avec le dernier timestamp peut donc être très grande et provoquer un saut des cercles.
Ajoutez une pause et une reprise automatiques lors des changements de visibilité de l'onglet. Écoutez l'événement visibilitychange sur document et utilisez document.hidden pour connaître son état :
- Lorsque l'onglet devient invisible, suspendez la boucle. Vous pouvez annuler le prochain callback avec cancelAnimationFrame en utilisant l'identifiant renvoyé par requestAnimationFrame.
- Lorsque l'onglet redevient visible, reprenez l'animation en conservant la position des cercles. Réinitialisez le timestamp précédent pour que le temps passé en pause ne soit pas inclus dans le prochain Δt.
Testez en changeant d'onglet pendant quelques secondes : les cercles doivent reprendre leur mouvement sans saut lié à la durée de la pause.
Parallaxe
Deux choses simples vont nous permettre de créer l'illusion d'un effet de parallaxe. Premièrement, les cercles de grand rayon devraient être dans un plan plus proche de la "caméra" que ceux de petit rayon. Deuxièmement, plus un cercle est loin du premier plan, plus il devrait se déplacer lentement. Pour le premier point, il suffit de trier le tableau des cercles par ordre croissant de leur rayon. Pour le faire proprement, codez une nouvelle méthode dans la classe Circle permettant de comparer le rayon du cercle avec celui d'un autre. (Comme d'habitude, cette méthode doit retourner un nombre négatif si le cercle this est plus petit que celui avec lequel on le compare, 0 s'il est de taille identique et un nombre positif s'il est plus grand.) Utilisez ensuite cette méthode pour faire un tri (méthode sort) du tableau des cercles après leur génération aléatoire. Pour le deuxième point, il vous faut modifier votre code de génération aléatoire des cercles pour que la longueur de leur vecteur de vélocité soit proportionnelle à leur rayon. Testez à nouveau votre animation et observez l'effet parallaxe en action.
Univers en tore plat
Vous l'avez certainement remarqué lors de votre test d'animation : au bout d'un certain temps, plus aucun cercle n'est visible dans votre canvas puisque leur position dans le plan est en dehors de sa limite. Pour éviter ce problème et donner l'illusion d'une translation infinie, nous pourrions changer le plan en repliant les côtés du canvas sur lui-même dans un tore plat (ou tore bidimensionnel, ou, si vous préférez une référence ludique, un niveau du jeu Pacman). Pour le faire, créez un nouveau fichier CircleInFlatTorus.js dans votre dossier class. Puis créez une classe héritant de Circle (avec le mot-clé extends). Surchargez la méthode update pour faire que si le cercle venait à sortir des limites (haut, bas, gauche, droite), il revienne par l'autre bord associé (bas <-> haut, gauche <-> droite). Notez qu'il faudrait fournir la taille du plan (width, height) pour pouvoir tester les bords. Modifiez votre programme principal pour créer des cercles dans un tore plat à la place des cercles dans un plan "infini" et observez à nouveau votre animation.
Détection des touches du clavier
Afin de pouvoir laisser l'utilisateur contrôler la caméra, il nous faut savoir quelles touches de direction (WASD par exemple) sont actuellement appuyées. Afin d'en faire un module réutilisable, nous allons encapsuler la gestion du clavier dans une classe. Commencez donc par créer un nouveau fichier Keyboard.js dans votre dossier class. Le constructeur initialisera un Set pour stocker toutes les touches actuellement appuyées par l'utilisateur, sans doublons. Le constructeur écoutera aussi deux événements sur l'élément DOM window : keydown pour la détection de l'appui sur les touches, et keyup pour la détection des relâchements.
Il vous faut donc coder les méthodes onKeyDown et onKeyUp. Pour récupérer la touche actuellement appuyée par l'utilisateur, vous pouvez utiliser event.code. Lors de l'appui d'une touche, stockez-la dans votre structure de données et supprimez-la lors de son relâchement.
Finalement, ajoutez une méthode isKeyDown(key) qui retournera true si la touche fournie en paramètre est présente dans votre structure des touches actuellement appuyées et false dans les autres cas.
Mouvement de la "caméra" par l'utilisateur
Maintenant que nous sommes capables de savoir la combinaison de touches actuellement appuyée, nous allons pouvoir simuler un déplacement de la caméra par l'utilisateur plutôt que le mouvement automatique actuellement programmé. Comme nous n'avons pas réellement de caméra, nous allons simplement modifier la direction du vecteur de vélocité des cercles avec la méthode setAngle de Vector2, qui conserve sa longueur. Afin de simplifier les mouvements possibles, nous allons nous limiter aux 8 mouvements suivants, listés ici avec leurs touches :
- 'S' et 'A' simultanément appuyées : nord-est
- 'S' et 'D' simultanément appuyées : nord-ouest
- 'W' et 'A' simultanément appuyées : sud-est
- 'W' et 'D' simultanément appuyées : sud-ouest
- 'S' appuyée : nord
- 'A' appuyée : est
- 'W' appuyée : sud
- 'D' appuyée : ouest
Il ne reste plus qu'à modifier la direction du vecteur de vélocité de vos cercles selon les touches appuyées par l'utilisateur. Si l'utilisateur n'a appuyé sur aucune de ces touches, ne bougez simplement pas les cercles (vous n'avez d'ailleurs ni besoin d'effacer ni de redessiner tous vos cercles puisque rien ne bouge).
Mouvement sur mobile (touch)
Pour créer un joystick virtuel sur mobile et permettre de déplacer la caméra, vous pouvez utiliser les événements touchstart, touchmove et touchend pour capturer les interactions de l'utilisateur. Le point de contact initial sert de centre : tant que le doigt reste décalé de ce point, sa position définit une direction de déplacement.
Lors du touchstart, enregistrez la position initiale, puis lors du touchmove, calculez la direction du déplacement en comparant la position actuelle avec la position initiale (utilisez Math.atan2 sur les différences des y et des x).
Lors du touchend, vous réinitialiserez la direction à false.
Essayez d'encapsuler tout ceci dans une classe, comme pour la gestion du clavier. Elle conservera la position initiale et la direction du déplacement, et écoutera les événements tactiles sur le canvas.
Par exemple, votre méthode onTouchStart peut enregistrer la position du premier contact fourni par event.touches :
onTouchStart(event) {
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
}
La liste event.touches contient les contacts tactiles en cours. L'indice [0] sélectionne le premier contact de cette liste : pour ce TP, nous nous limitons à un seul doigt. Cet indice n'est pas un identifiant permanent du doigt.
Lors de l'enregistrement des écouteurs, veillez à conserver l'instance comme valeur de this, par exemple avec une fonction fléchée :
canvas.addEventListener('touchstart', (event) => this.onTouchStart(event));