Processing avec python

Initiation à la programmation en python avec Processing.py

L'activité proposée consiste à s'initier à la programmation avec le langage python en exploitant un contexte particulier favorisant la réalisation de créations artistiques numériques. Processing est un projet né de la volonté de faciliter la création d'oeuvres numériques interactives à destination d'artistes, pas nécessairement informaticiens. Ce projet a été développé avec le langage Java, mais suite à son succès et au besoin d'utiliser les créations dans différents contextes (particulièrement web), des déclinaisons dans d'autres langages de programmation sont apparues, comme P5.js (JavaScript) et plus récemment Processing.py (python).

python étant utilisable de manière plus souple que Java (concepts orientés objects incontournables pour ce dernier), il nous a semblé plus judicieux d'opter pour ce langage pour une initiation aux bases de la programmation. Le choix de Processing repose sur la volonté de présenter l'activité de programmation comme un outil fantastique d'expression permettant de créer des objets numériques (interactifs).

L'atelier proposé est structuré en deux parties:

  • un premier temps permettant d'introduire progressivement les concepts fondamentaux de la programmation,
  • un second temps orienté vers la résolution de problèmes en s'attaquant à la réalisation de différentes “oeuvres numériques” :)

Premier temps: initiation aux bases de la programmation avec Processing

Plutôt qu'une présentation formelle et conceptuelle, nous vous proposons de procéder à un apprentissage dirigé par des exemples afin de favoriser la mise en oeuvre pratique dès l'introduction des premiers concepts.

Notion d'instruction, de séquence d'instructions, de variable et d'affectation

Voici donc un premier exemple de programme avec Processing.py:

Ce premier exemple permet déjà d'illustrer de nombreuses notions de programmation :

  • d'abord, on constate que différentes couleurs embellissent le code source. En particulier, les lignes apparaissant en vert correspondent à des phrases en langage naturel et donc à destination des humains et pas de la machine. Ce sont des commentaires et la machine les détectent car il y a le caractère # en début de ligne et les ignorent totalement.
  • en tout début de programme, plusieurs lignes débutent par le mot from. Cela correspond à l'importation de fonctions existantes (ici, de fonctions mathématiques de base et de celles liées à l'usage de Processing). Dans la suite de cette activité, il faudra toujours débuter vos programmes par ces deux lignes :)
  • à l'opposé, tout à la fin du programme, il y a l'expression run(). Cela correspond à l'appel d'une fonction existante (dans Processing). Les parenthèses ont leur importance : c'est ce qui permet de transmettre ou pas des informations à une fonction. Ici la fonction run ne nécessite pas d'information particulière, elle ne possède donc pas de paramètre. Dans la suite de cette activité, il faudra toujours terminer vos programmes par cet appel à la fonction run qui déclenche l'exécution de Processing, ou plus précisément qui va appeler la fonction setup.
  • concentrons-nous maintenant sur ce qui nous permet de définir une nouvelle fonction : def setup():. Le mot-clé def permet d'indiquer la définition d'une nouvelle fonction qui se nommera ici setup et qui ne nécessite pas de paramètre. ATTENTION : les : font partie de la syntaxe de définition d'une fonction, il ne faut pas oublier de les mettre après la parenthèse fermante !
    • les lignes suivantes constituent le corps de la fonction et décrivent le traitement qu'elle réalise.
    • ATTENTION : contrairement à d'autres langages utilisant des symboles particuliers pour indiquer le début ou la fin d'une fonction (comme { et } en Java par exemple), python repose juste sur l'indentation c'est-à-dire l'introduction d'un décalage permettant d'identifier les instructions appartenant au corps de la fonction.

Assez de théorie : appuyez sur le bouton Run afin de voir ce que produit ce premier programme :)

Sans connaître Processing, le nommage des fonctions permet d'en déduire assez facilement leur comportement. Par contre, il peut-être plus délicat de déduire la signification des différents paramètres. Pour rendre l'algorithme plus lisible, nous allons introduire quelques variables qui expliciteront ce à quoi correspondent les différents paramètres des différentes fonctions.

Une variable permet de stocker une information et comme vous le constatez en python, il n'est pas nécessaire de préciser la nature de l'information (i.e. le type). Contrairement à une variable mathématique, il est possible de changer la valeur de la variable lors du déroulement de l'algorithme et surtout le symbole = ne correspond pas à la notion d'égalité, mais à l'affectation, c'est-à-dire l'opération permettant de stocker une valeur dans une variable.

Il existe aussi un certain nombre de variables prédéfinies dans Processing auxquelles vous pouvez accéder directement. Par exemple, la taille de l'écran définie grâce à la fonction size(width, height) initialise des variables correspondantes width et height que vous pouvez utiliser ensuite dans votre algorithme pour paramétrer vos dessins par rapport à la taille de la zone de dessin.

Nous allons maintenant examiner l'algorithme ci-dessous. A votre avis, que va-t-il se produire ? Vérifiez votre intuition en exécutant le programme.

Surprise ?! L'algorithme appelle trois fois la fonction ellipse mais seules deux ellipses apparaissent ! Pourquoi ? Modifiez l'algorithme ci-dessus afin que trois disques soient dessinés, chacun décalé de 50 pixels vers le bas et la droite ?

Ceci illustre une utilisation importante et récurrente de la notion de variable : la notion d'accumulateur. En effet, il est souvent pratique de réutiliser la même variable pour la faire évoluer en fonction de sa valeur actuelle. Assurez-vous d'avoir exploité cette notion d'accumulateur (c'est le cas si les variables x et y sont les seules à apparaître pour les deux premiers paramètres de la fonction ellipse.

Première structure de contrôle : notion de répétition

Même si votre algorithme fonctionne, un informaticien ressentirait sûrement une certaine frustration face à une solution faisant appel trois fois à la même fonction. Plus précisément, c'est le fait d'avoir une forte redondance qui est frustrant et il est probable que l'on puisse supprimer cette redondance. Pour cela, il faudrait pouvoir effectuer le dessin d'un disque trois fois (en faisant varier l'origine du disque). Afin de pouvoir effectuer de tels répétitions, nous avons besoin d'exprimer ce qui doit être répété et le nombre de fois qu'il faut effectuer la ou les actions.

La structure de contrôle for x in range(from, to[, step]): permet de répéter (to-1-from) fois les instructions contenues dans le corps de la boucle (éventuellement avec un pas (step) différent de 1). En fait, la fonction range énumère les éléments à partir de la valeur from jusqu'à la valeur to exclue. Il est aussi possible d'énumérer en extension les éléments à parcourir avec la syntaxe suivante: for x in [0, 50, 100]:. Les éléments à parcourir sont définies à l'intérieur d'une liste, une des structures de données les plus utilisées avec les dictionnaires (presque le même sens que l'usage usuel ;)).

Maintenant que les principales notions d'algorithmique ont été présentées (instruction, séquence d'instructions, variable et affectation, répétition… il ne nous manque plus que l'alternative et la notion de fonction !), passons à la création de quelques dessins.

Dessinons (le début d')un quadrillage

On souhaite dessiner le début d'un quadrillage comme l'illustre la figure ci-dessous (solution):

Dessinons un quadrillage

Comment faut-il modifier légèrement l'algorithme ci-dessus afin de produire un quadrillage en deux dimensions comme l'illustre la figure ci-dessous (solution) ?

Processing : passage en mode cinéma pour les animations

Nous avons pour l'instant réalisé des dessins non animés. Or, Processing est plutôt destiné aux oeuvres numériques interactives. Afin de produire ces animations, Processing repose sur le même principe que l'animation classique. Chaque plan est dessiné séparément et les plans sont affichés successivement à raison de 25 images par seconde. Pour ce faire, Processing définit la fonction draw qui est appelée automatiquement tous les 25ième de seconde (par la fonction run), ce qui permet de rafraîchir la zone graphique et produire une animation. Voici un exemple simple :

Dans cet exemple, on demande que la fonction draw soit appelée tous les 1/4 de seconde (cf. frameRate(4)), et l'on dessine un carré avec pour origine (le coin supérieur gauche) la position en temps réel du pointeur de la souris déterminée grâce aux fonctions mouseX et mouseY. Comme l'on ne demande pas à effacer la zone graphique, les carrés s'affichent les uns au dessus des autres. Que suffirait-il de modifier dans le programme ci-dessus pour donner l'illusion du déplacement d'un carré en fonction du positionnement de la souris ?

Cet exemple nous amène à expliquer une nuance éludée précédemment concernant la notion de variable. En effet, maintenant que nous disposons de deux fonctions (setup et draw) pour nos créations, la question d'un partage d'information entre ces deux fonctions devient nécessaire. Ainsi, en supposant que l'on souhaite faire varier la couleur du carré dans l'exemple précédent de manière “jolie”, il faut disposer de la couleur précédente pour calculer la couleur que prendra le carré afin de produire un dégradé. Si l'on définit une variable rect_color dans la fonction setup, cette variable n'existera que lorsque la fonction setup sera en cours d'exécution, dès que l'on passera à la fonction draw, elle sera détruite. En effet, une variable n'existe que dans le bloc dans lequel elle est définie. Du coup, si l'on déplace la variable à l'extérieur des fonctions setup et draw, cela devient une variable globale, accessible depuis les deux fonctions ! L'exemple ci-dessous illustre ce principe:

Si vous regardez ce qui se passe dans la console (petite zone située en dessous de la zone graphique), vous remarquerez que des nombres défilent : ils correspondent aux valeurs que prend la variable rect_color. En attendant suffisamment longtemps, vous constaterez que l'on arrive à des valeurs négatives, ce qui n'est pas l'idéal pour une composante de couleur qui est censée être définie dans l'intervalle [0,255]… Ce problème va nous permettre d'introduire la deuxième structure de contrôle : l'alternative. En effet, il serait préférable de détecter la situation où une composante devient négative pour repartir dans l'autre sens (et réciproquement lorsque l'on dépasse 255). Pour cela, nous avons besoin de la notion de condition (une expression booléenne, c'est-à-dire ne pouvant prendre que la valeur true ou false) et de la notion d'alternative, if condition: else: , permettant d'exprimer des expressions du type “si condition est vraie alors exécuter la première blanche, sinon exécuter la seconde branche”. L'exemple ci-dessous illustre l'utilisation d'une alternative afin de garantir que l'on reste dans l'intervalle valide pour une composante RGB :

Comme vous le constatez, lorsque la composante r arrive à 0, elle est réaffectée à 255 et l'on perçoit le changement de couleur qui revient au jaune.

La notion de fonction

Il ne manque nous manque plus maintenant que la notion de fonction pour disposer de la boîte à outils de base permettant déjà de nombreuses créations (il restera ensuite la programmation orienté objets ;)). Nous utilisons déjà deux fonctions prédéfinies par Processing setup et draw, que l'on redéfinit pour chaque programme. Lorsqu'un traitement doit être effectué plusieurs fois, il peut être judicieux de le définir comme une nouvelle instruction, cela permet d'enrichir le langage dont nous disposons pour exprimer nos algorithmes. La notion de fonction permet la définition d'algorithmes réutilisables dans différents contextes.

Une fonction est définie par son nom, sa liste de ses paramètres (si elle en a besoin) et son corps (les instructions qui décrivent ce que réalise cette fonction). Supposons que l'on souhaite disposer d'une instruction permettant de retourner une couleur choisit aléatoirement. Cette fonction ne nécessite pas de paramètre pour effectuer son traitement, qui repose sur le tirage aléatoire de trois entiers dans l'intervalle [0,255] pour chacune des composantes RGB.

Cet algorithme définit deux fonctions en plus des classiques setup et draw : la fonction rnd(max) et rainbow(). La fonction rnd possède un paramètre indiquant la borne supérieure maximale (exclue) et a pour comportement de retourner un entier tiré aléatoirement dans l'intervalle [0, max[. La fonction rainbow n'a pas de paramètre et utilise la fonction rnd afin de générer une couleur aléatoirement en tirant au hasard les trois composantes R, G et B. Finalement, la fonction rainbow est utilisé dans la fonction draw afin de changer la couleur lors de l'affichage de chaque carré.

Pour terminer sur cet exemple, voyons comment nous pourrions réinitialiser l'affichage lorsque l'utilisateur appuie sur une touche. Pour cela, il nous faut définir une fonction bien spécifique qui sera appelée automatiquement par Processing lorsque l'utilisateur sollicitera une des touches du clavier : la fonction keyPressed() ou keyPressed(event).

Deuxième temps: vos propres créations !

Vous disposez maintenant de tous les éléments pour vous lancer dans vos propres créations :)

Si vous souhaitez explorer les fonctions prédéfinies de Processing, il y a la référence disponible ici.

A vous de jouer !