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#
-
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 etp5.js
. -
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.
-
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. -
Les méthodes décorées avec
@p5.hook
doivent prendre l'argument self, suivi des mêmes arguments que les fonctions d'origine dansp5
. -
Les fonctions et variables provenant de
p5
doivent maintenant être extraites/utilisées avec la syntaxeself.p5.xxx
au lieu dep5.xxx
.
C'est ce qui permet de garantir l'encapsulation pour chaque animation. -
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'argumenttarget
.
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 :
class2
class1
Animation complexe#
Construction dynamique d'une courbe de Bézier, avec réactivité à la souris.
(sur une proposition de Nicolas Revéret)
# Tests
(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Courbe de Bézier récursive
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):
...
# Tests
(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)