Aller au contenu

Utilisations avancées : la classe Sketch#

L'utilisation de p5.js décrite jusqu'à présent repose sur un module python qui profite du mode de travail "par instances" de la bibliothèques JavaScript, pour simuler son mode de fonctionnement "global" du côté des utilisateurs dans pyodide (c'est-à-dire que les fonctions setup, draw, ... sont déclarées au niveau de base du module).

Comme discuté dans les pages précédentes, ce mode hybride couvre la plupart des cas. Mais il peut provoquer des problèmes lorsque l'on cherche à mettre plusieurs animations dans la même page, ou pour éviter de partager des états globaux.

Pour palier à ce type de problèmes, le thème offre une syntaxe alternative reposant intégralement sur de la programmation objet. Celle-ci met complètement à profit le mode "par instances" de p5, et permet ainsi de travailler avec des objets et une logique parfaitement encapsulés.
Les codes résultants semblent plus logiques et moins "magiques", par certains côtés, mais ils s'éloignent donc sensiblement des syntaxes habituelles de p5.js.

Idée générale#

  1. Créer une classe qui étend la classe p5.Sketch.
    La classe parente comporte toute la logistique pour mettre en place les liens entre la classe enfant et p5.js.

  2. Le constructeur de la classe peut être implanté à la convenance du rédacteur, pour initialiser chaque animations avec des paramètres différents, selon ses besoins.

  3. Les fonctions essentielles à définir (setup, draw, gestionnaires d'évènements, ...) sont alors des méthodes de la classe qui doivent être décorées avec @p5.hook.
    Le décorateur permet de marquer ces méthodes pour que la classe parente puisse savoir sans ambiguïté quelle méthode est une fonction p5 et laquelle ne l'est pas.

  4. Les méthodes décorées avec @p5.hook doivent prendre l'argument self, suivi des mêmes arguments que les fonctions d'origine dans p5.

  5. Les fonctions et variables provenant de p5 doivent maintenant être extraites/utilisées avec la syntaxe self.p5.xxx au lieu de p5.xxx.
    C'est ce qui permet de garantir l'encapsulation pour chaque animation.

  6. Créer une instance, puis appeler sa méthode run(...), en lui passant l'id html de la figure où placer l'animation via l'argument target.
    Il n'est plus nécessaire de passer les fonctions/méthodes en argument, puisque la classe parente connaît déjà les méthodes à utiliser grâce au décorateur @p5.hook

Exemples#

Animations concurrentes#

Voici une mise en application concrète, avec deux animations de "gouttes tombantes", pouvant se dérouler en parallèle. L'encapsulation avec les instances permet d'éviter les interactions malheureuses entre les animations :


###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

class2

Votre tracé sera ici

class1

Votre tracé sera ici

Animation complexe#

Construction dynamique d'une courbe de Bézier, avec réactivité à la souris.
(sur une proposition de Nicolas Revéret)


###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

Courbe de Bézier récursive

Votre tracé sera ici

Contrats#

Quelques points d'attentions :

Constructeur#

Il n'est pas nécessaire d'appeler la super() méthode, car la classe parente ne l'implante pas.

L'utilisateur peut donc utiliser la méthode __init__ à sa convenance, pour initialiser proprement une animation.


self.p5 n'est pas utilisable depuis le constructeur

La propriété self.p5 est définie seulement après que l'appel à la méthode run ait été effectué. Cela implique qu'il n'est pas possible de l'utiliser depuis le constructeur ou dans une méthode appelée depuis ele constructeur, à moins d'appeler la méthode run auparavant, depuis le constructeur lui-même.

Méthodes à ne pas écraser#

La classe enfant ne doit surtout pas implanter les méthodes suivantes :

  • run
  • start
  • step
  • stop
  • remove
  • __call__

La méthode run#

Sa signature est la suivante :

sketch.run(
    target: Optional[str] = None,
    stop_others: bool = True,
    **_
) -> None


Elle prend deux arguments, tous deux optionnels :

Argument Défaut Rôle
target None Id html de la figure où tracer l'animation.
Comme pour la fonction p5.run, si la valeur est None, c'est la valeur par défaut de l'argument div_id de la macro {{figure()}} qui est utilisé.
stop_others True Si laissé à True, toutes les animations en cours seront stoppées avant de mettre en place celle pour le présent appel.

Les arguments nommés restant (c'est-à-dire, **_) ne devraient jamais être utilisés.
(C'est un détail d'implantation lié à la machinerie interne du module p5 de PMT).


Les instances ne peuvent appeler run qu'une seule fois

Si un second appel est tenté sur une même instance, ValueError est levée.

Ceci permet de garantir que l'animation est toujours créée avec un état initial "propre", tout en gardant l'interface de création des objets (__init__) séparée de celle du lancement des animations (run). L'utilisateur n'a ainsi pas à se préoccuper d'appeler les super méthodes (...et ne risque donc pas d'oublier ces appels).

Le décorateur @p5.hook#

Il permet donc d'indiquer à la classe parente, p5.Sketch, quelles sont les méthodes qui correspondent à des fonctions à utiliser pour/via le module original de p5.js, comme par exemple les fonctions setup, draw, preload, les gestionnaires d'évènements comme mousePressed, ...

Les méthodes décorées devraient respecter les noms des fonctions équivalentes dans la bibliothèque p5.js d'origine, c'est-à-dire qu'ils devraient être écrits en camelCase.

Les personnes qui se sentiraient des poussées d'urticaire à cette idée ont la possibilité de donner un nom arbitraire à chaque méthode, et de passer un argument au décorateur p5.hook. Cet argument sera alors le nom de la fonction d'origine dans p5.js.


Concrètement, la déclaration suivante :

class Animation(p5.Sketch):

    @p5.hook
    def setup(self):
        ...

    @p5.hook
    def mouseClicked(self, event):
        ...


Peut être transformée en... :

class Animation(p5.Sketch):

    @p5.hook('setup')
    def preparation(self):
        ...

    @p5.hook('mouseClicked')
    def celui_qui_ecoute_a_l_oreille_des_souris(self, event):
        ...