A début de notre cours, nous avons annoncé qu’il y a quatre “concepts” fondamentaux de la programmation. Nous maintenons cette déclaration, mais néanmoins doivent rajouter deux ingrédients qui changent profondément la nature de ce qu’un programme peut faire : les listes et les objets.

Rassurez-vous : les listes et les objets se basent sur les quatre techniques fondamentales, mais ils les radicalisent à tel point que la programmation ne ressemble plus à ce que nous avons appris jusqu’ici. Par exemple, une liste prolonge le rôle de la variable en stockant plusieurs données à l’intérieur, à la place d’une seule donnée. Et les objets vont encore plus loin car ils peuvent stocker non seulement de multiples valeurs dans une seule variable, mais aussi des variables de différentes genres, et même toutes les fonctions (ou “méthodes”) nécéssaires pour manipuler ces variables. Les listes et les objets sont alors des prolongements de toutes les techniques que nous avons utilisé ici, mais des prolongements qui transforme ce que nous pouvons faire avec la programmation.

Attrochez-vous donc, car ce n’est pas pour rien que nous avons attendu longtemps avant d’introduire ces logiques dans ce cours . Par expérience, il s’agit de techniques que beaucoup d’étudiants ont du mal à maîtriser, au moins au début. même si on comprend le fonctionement, leur utilisation dans la pratique introduit une telle couche d’abstraction que même les artistes conceptuels ont un peu du mal à gérer la chose.

Par contre, c’est avec l’abstraction que la programmation devient enfin une matière profondément fluide. Jusqu’ici nous avons fait des programmes qui sont souvent appelés “ linéaires ”, c’est-à-dire ils effectuent une seule tâche ou suivent un seul déroulement que l’artiste doit gérer étape par étape : faites ceci, maintenant cela, et maintenant ce truc là, et puis… Avec les listes, et surtout avec les objets que nous traiterons dans un prochain cours, on ne travaille plus sur un seul élement à la fois, mais sur potentiellement des centaines, voire des milliers (ou millions, ou milliards, ou billions, ou billiards, …).

Avec les listes les médias deviennent enfin des hypermédias. Et les objets introduisent la même dynamique, mais à la programmation elle-même. Du coup, ces deux techniques incarnent, chacun à leur manière, la variabilité accrue de l’ordinateur, sa folie de la modulation non seulement infini, mais infiniment complexe et imbriqué.

Qu’est-ce qu’une liste ?

Nous allons commencer donc par le plus simple des ces deux techniques : la liste. Qu’est-ce qu’une liste? Comme nous venons de dire, il s’agit d’une variable qui ne contient pas une valeur, mais plusieurs. Eh? C’est ça votre complication super-mega-archi-compliqué-pour-nos-pauvres-petits-cerveaux-humains? Une liste contient plusieurs valeurs, qu’est qu’il y a de compliqué la-dedans? C’est effectivement une idée très simple, si simple que nous allons le répéter à plusieurs reprises ici:

une liste n’est pas faite pour contenir uniquement une seule valeur, mais plusieurs valeurs à la fois

Gardez-vous cette définition en tête : une liste est une variable qui contient plusieurs valeurs*.

*Petite note: en anglais, une liste s’appelle un “ array ”. Si vous voulez de la documentation de la fonction que nous allons décire dans cet article, cliquez sur le site de Processing ici : Array.

Fabriquer une liste

Le syntaxe le plus simple pour créer une liste en langage Java est le suivant:


int[] x = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};

println(x);

Écrivez ce code dans un nouveau sketch Processing et jouez-le. Vous aller voir que Processing a retenu vos dix valeurs dans une liste linéaire et qu’il a affiché en bas de la fenêtre les unes après les autres :

Je vous recommande fortement d’afficher de temps en temps les valeurs à l’intérieur d’une liste de cette façon, car comme nous allons voir, les listes vont vite se remplir de beaucoup de valeurs, et parfois il est difficile de visualiser ce qu’il y a dedans.

D’ailleurs, à ce sujet, vous pouvez également utiliser la fonction print(x); pour afficher une liste, ce qui évitera d’avoir un retour-chariot entre chaque valeur de votre liste. (depuis Processing 0125, on ne peut plus imprimer une liste de cette façon).

Dans le programme suivant, nous avons par exemple une série de chaînes de caractères (String) qui sont affichés de deux différentes manières à l’intérieur de la console Processing:


int[] x = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
String[] s = {"Wow!", "", "Ce", "n'est", "pas", "si", "compliqué"};

println(s);
println("----");
print(s);

println("\n");
print(x);

Ce qui est pratique avec la fonction print(); c’est qu’elle permet de lire notre liste (3, 1, 4, 1, 5, 9, 2, 6, 5, 3) un peu mieux que l’exemple précédent. En ajoutant juste des espaces entre les valeurs, on peut du coup lire encore plus facilement le contenu d’une liste dans la console de Processing. (depuis Processing 0125, on ne peut plus imprimer une liste de cette façon).

Accéder à une seule valeur

Ecrire plusieurs valeurs dans une liste ne sert à rien si nous pouvons pas nous servir de ces valeurs individuellement, car dans la programmation - telle qu’elle est pratiquée aujourd’hui, au moins - on ne travaillent que sur des éléments individuellement. même si la machine traite les instructions rapidement, elle les execute l’une après l’autre (pixel no.1, pixel no.2, pixel no.3, etc).

Si vous voulez accéder à une valeur de la liste, et non pas l’ensemble des valeurs, il suffit d’écrire le nombre de l’élement de la liste entre crochets [ ] :


int[] x = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
String[] s = {"Wow!", "", "Ce", "n'est", "pas", "si", "compliqué"};

println("voici la première valeur de la liste : " + x[0]);
println("voici la deuxième valeur de la liste : " + x[1]);

println("voici mot numéro 0 : " + s[0]);
println("voici mot numéro 1 : " + s[1]);
println("voici mot numéro 2 : " + s[2]);

Attention! Attention! Attention! Vous avez noté quelque chose? La programmation est une histoire de geeks! Et les geeks ne comptent pas à partir du numéro 1. Non… C’est trop… commun ça. Un vrai geek compte à partir du chiffre 0. Le premier élement se trouve à la position 0 dans une liste. Un programmeur ne compte pas “ 1, 2, 3, … ”. Un programmeur compte “ 0, 1, 2, … ”.

Pourquoi on compte à partir de 0 ? Pour plusieurs raisons, trop longues à expliquer ici. mais disons qu’il est plus pratique, surtout quand on travaille dans des langages comme le C, de compter à partir de 0 - surtout quand on transfert une liste à une autre partie du programme. Techniquement une liste est référencé à l’intérieur de la machine par son début + x : c’est-à-dire début + 0, début + 1, début + 2, etc). mais passons. Ça c’est pour une autre fois. Souvenez-vous simplement que maListe[0] corréspond à la première valeur dans cette liste.

modifier la liste

Une liste peut changer de valeurs, car il s’agit d’une variable comme n’importe quelle variable, sauf qu’elle contient plusieurs valeurs (tiens, j’ai déjà entendu ça quelque part). Et comme n’importe quelle variable, elle peut changer ses élements indépendament des autres élements de la liste :


int[] x = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};

int resultat = x[0] + x[1];

println("#1. Voici l'addition des deux premiers chiffres: " + resultat);

// changer les deux premières chiffres
x[0] = 999;
x[1] = 666;

resultat = x[0] - x[1];

println("#2. Voici une soustraction avec les deux premiers chiffres: " + resultat);

Au lieu de dire x = y + z, on dit x[0] = x[1] + x[2]. Chaque emplacement de la liste contient une valeur et peut être utilisé pour calculer une nouvelle valeur, ou stocker une valeur. Je sais que cela peut paraître étrange pour un débutant, mais il est important de comprendre qu’un casier dans la liste (genre x[0]) est presque la même chose qu’une variable avec son nom propre (genre x). La seule différence, c’est que les crochets [ ] sont utilisés pour accéder ou modifier un ingrédient de la liste de façon discrète, c’est-à-dire individuellement, c’est-à-dire tout seul sans emmerder ses voisins.

Tout le monde y passe

Vous avez remarqué (j’espère) que nous pouvons non seulement mettre des valeurs numériques dans une liste (int), mais également des chaînes de caractères (String). Dans Java (et donc dans Processing), n’importe quelle type de variable peut être transformée en une liste : float, int, String, char, PFont, PImage, … On peut même mettre une liste dans une liste (…plus tard…), et dans les chaptires suivants nous verrons des listes d’” objets ”.

La seule chose que vous devez respectez (pour l’instant), c’est le genre des ingrédients de la liste. Les ingrédients d’une liste - telle que nous l’utilisons ici - doivent tous être de la même nature. Il existe en Java la possibilité de listes multi-genre (un int, suivi d’un float, etc), mais pour aujourd’hui respectez plutôt la distinction des genres.

Le plus important à retenir ici, c’est que n’importe quelle chose peut être transformé en une liste qui contient un ensemble de choses, par exemples une liste d’images.

De la chronophotographie

Pour mettre enfin en pratique les listes, nous allons maintenant construire une liste qui contient plusieurs images. Nous allons utiliser cette liste pour passer d’image en image, à la Étienne-Jules Marey (cf. chronophotographie). Pour avancer dans notre liste, nous allons utiliser une variable complémentaire à notre liste, comunément connu sous le nom de “ index ” ou de “ compteur ”.

Préparation des ingrédients

Commençons par donner un nom à notre Sketch en sélectionnant file > Save As. Nous avons utilisé le nom “ marey ” mais vous pouvez choisir un autre si vous voulez :

Nous allons maintenant créer un sous-dossier dans notre Sketch qui contiendra toutes nos images. Si vous avez oublié comment travailler avec des images dans Processing, je vous recomande un petit retour vers le chaptire 06 : Images Photographiques. Pour retrouver le dossier de votre Sketch, sélectionnez dans le menu Processing Sketch > Show Sketch Folder :

… ce qui devrait afficher votre Sketch dans le finder (sur mac) ou l’Explorateur (sous Windows) :

Dans ce dossier, créez un “ nouveau dossier ” :

…et donnez-lui le nom de “ data ”. C’est dans ce dossier que nous allons copier les dix images de notre séquence. Voici l’apparence du dossier de notre Sketch quand nous aurons terminé :

Voici dix images prises avec le Fusil Chronophotographique de marey. Copiez chaque image l’une après l’autre dans votre dossier data en respectant la nomenclature marey_0.png, marey_1.png, marey_2.png, … :

Sur macintosh, ce procédé est relativement sans difficulté. Il suffit de saisir chacune des images avec la souris et de les trainer dans le dossier data de votre Sketch. Vous pouvez également sauvegarder chaque image sur le bureau via clic-droit ou ctrl-clic. Il y a tellement de façons de peupler un dossier d’image, que je ne veux pas vous ennuyer en les illustrant toutes ici. Une bonne base du fonctionnement de votre ordinateur devrait suffire. mais si toutefois vous rencontrez des difficultés - ou si vous êtes de nature impatient(e) -, vous pouvez toujours télécharger une copie de ce Sketch tout à la fin de ce cours.

Pour le côté folklore, voici une image du fusil qui a servi d’appareil de capture de ces images :

Nous mentrons aussi cet appareil car nous allons nous inspirer de sa forme circulaire : Avant la linéarité cinématographique, l’image en mouvement était bouclé, circulaire. Dans notre code, ce bouclage s’executera via les index des images, c’est-à-dire leur placement dans la liste : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, … Une fois l’image no.9 affichée, nous revennons sur l’image no.0 et notre séquence est bouclée.

Construire la liste

maintenant, nous sommes prêts pour programmer notre suite d’images. Nous avons un Sketch avec son dossier data remplis de 10 images.

mais cette fois-ci, nous allons utiliser une nouvelle méthode pour la déclaration de notre liste. On va d’abord construire une liste d’emplacements vides, puis les remplir plus tard dans le setup(). Pourquoi? Dans le cas des images, cette procédure en deux parties est essentielle car nous ne pouvons pas mettre directement entre accolades { } tous les pixels des images dont nous avons besoin. Les images ne peuvent être chargés qu’avec la fonction loadImage("nom_du_fichier.jpg"); et celle-ci ne peut pas être écrite entre accolades. Il faut dans ce cas créer une “ nouvelle ” liste avec le nombre d’emplacements reservés pour les contenus d’images, et puis charger chacune de ces images dans le setup() avec la fonction loadImage().

Au lieu d’écrire:

int[] x = {1, 2, 3, 4, 5};

Nous allons écrire plutôt:

PImage[] images = new PImage[5];

Ce qui nous créera une liste vide avec 5 emplacements prêts à recevoir des images :

Je note au passage que vous allez peupler la plupart de vos listes de cette façon : 1) déclarer le genre de la liste, 2) déclarer le nom de la liste, 3) demander à l’ordinateur d’aller créer cette nouvelle liste dans la mémoire, 4) réserver dans cette mémoire un certain nombre d’emplacements avec chacun la taille nécessaire pour contenir ce genre de chose. Voici le syntax:

Comme vous pouvez voir dans l’illustration précédente, cette liste sera remplie d’emplacements vides, signifiés par le mot null. En informatique, null signifie un emplacement dans la mémoire qui n’a pas encore reçu de valeur. Du point de vue de l’ordinateur, il y a une grande différence entre un emplacement avec une valeur “ 0 ” et un emplacement avec une valeur “ null ”. Dans le premier cas nous avons une valeur comme une autre (0), alors que dans le deuxième cas, il n’y a rien, du vide, ou null (je pourrais vous référencer ici à Anaximandre, Nietzsche ou Heidegger, mais vous épargnerai ce détour — pour l’instant).

Nous avons donc une liste avec 10 emplacements, chacuns prêts pour recevoir une image. Whew!

Peuler la liste

Comment maintenant peupler cette liste avec nos images? Une façon de le faire serait d’écrire un loadImage() pour chaque emplacement de la liste :


PImage[] images = new PImage[10];

void setup() {

    images[0] = loadImage("marey_0.gif");
    images[1] = loadImage("marey_1.gif");
    images[2] = loadImage("marey_2.gif");
    images[3] = loadImage("marey_3.gif");
    images[4] = loadImage("marey_4.gif");
    images[5] = loadImage("marey_5.gif");
    images[6] = loadImage("marey_6.gif");
    images[7] = loadImage("marey_7.gif");
    images[8] = loadImage("marey_8.gif");
    images[9] = loadImage("marey_9.gif");

}

mais c’est trop fatiguant, et puis que feriez vous si vous vouliez ajouter une centaine d’images plutôt que dix ? Nous n’allons pas écrire 100 fois la même phrase en changeant juste le chiffre! C’est trop de travail, et de toute façon on m’a déjà expliqué que je peux demander à l’ordinateur de faire ce travail à ma place. Non, nous allons plutôt utiliser une boucle comme nous avons appris dans le chaptire sur les boucles, car les listes et les boucles sont absoluement faits l’une pour l’autre. Si vous avez une centaine d’images à traiter tour-à-tour dans une liste, c’est une boucle qu’il vous faut pour les manipuler.


PImage[] images = new PImage[10];

void setup() {

  for(int i=0; i<10; i++) {
    images[i] = loadImage("marey_" + i + ".gif");
  }

}

Jouer la séquence

Enfin, nous sommes prêts pour lire notre séquence. Nous avons un Sketch avec un dossier rempli d’images. Nous avons un setup() qui a chargé ces images dans une liste et cette liste a rangé chacune de ces images par l’index de leur emplacement (0, 1, 2, 3, ...).

Essayer d’entrer le programmer suivant, et observer ce qui se passe:


PImage[] images = new PImage[10];

void setup() {

  size(319,240);

  for(int i=0; i<10; i++) {
    images[i] = loadImage("marey_" + i + ".gif");
  }

}

void draw() {
  image( images[0], 0, 0);
}

Alors? Content? Déçu?

Bien sûr, ce programme ne marche pas tout à fait comme nous avons voulu. merde. En plus c’est nul. Il affiche effectivement une image, mais uniquement la première de la liste! Pourquoi?

Si vous regardez de nouveau le programme, vous verrez que c’est notre faute si la liste affiche en permanence la première image de notre liste (images[0]). Nous avons appris plus haut que tout numéro entier à l’intérieur des accolades nous donnera accès dans la mémoire à la valeur qui se trouve à cet “index”. mais si le programme demande toujours l’image à l’index 0 (c’est-à-dire la première de la liste), on ne verrra que cette même image, à l’infini. Il faudrait donc faire varier l’image si nous voulons qu’elle change.

Une possibilité, serait d’essayer quelque chose comme ceci:


void draw() {
  image( images[mouseX], 0, 0);
}

Du coup quand la souris change de valeur, l’index de l’image récupérée par la liste change. La souris anime la séquence. mais quand nous jouons notre animation - merde encore! - cette méthode affiche presque tout de suite l’erreur suivant :

ArrayIndexOutOfBoundsException

Voici une capture d’écran de mon ordinateur, quand j’ai placé la souris la position (10,15):

Hmmm… ” Array Index Out Of Bounds ”, je me demande si l’ordinateur veut dire que j’ai essayé d’accéder au onzième élements de la liste, alors qu’il n’y en a que 10. Eh bien, oui. Effectivement, si vous tentez de lire en dehors du nombre réservé pour la liste, votre programme va planter. J’ai placé ma souris à la position mouseX = 10 et il n’existe pas d’image à la positionimages[10]! J’ai cherché à lire dans un endroit de la mémoire qui n’existe pas. Et comme Java ne veut pas que je finisse par déborder mon programme et lire dans les variables d’un programme voisin (c’est comme ça d’ailleurs que beaucoup de virus s’opèrent), il a préféré tout simplement faire planter mon Sketch.

Si on ne veut pas planter le Sketch, on doit rester toujours à l’intérieur de la liste en ne dépassant jamais la valeur 9.

Il y a de multiples façons d’y arriver, mais en voici une :


PImage[] images = new PImage[10];

void setup() {

  size(319,240);

  for(int i=0; i<10; i++) {
    images[i] = loadImage("marey_" + i + ".gif");
  }

}

void draw() {
  image( images[mouseX/32], 0, 0);
}

void mousemoved() {
   println("mouseX / 32 = " + mouseX/32);
}

Je profite de la largeur de notre Sketch pour ensuite diviser la position X de la souris par 32 pour que le résultat rentre toujours dans une valeur entre 0 et 9. Regardez d’ailleurs en bas de la fenêtre Processing le résultat de cette division pour comprendre le rapport entre mouseX/32 et le numéro de l’image qui est jouée (il faut déplacer la souris).

Lecture automatique

Voici une autre technique de lecture de notre séquence, avec l’utilisation cette fois-ci d’une variable qui changera automatiquement (i.e. de façon autonome) l’index de l’image que nous voulons afficher à partir de notre liste. Dans notre programme, nous avons appelé cet index le compteur:


int compteur = 0;
PImage[] images = new PImage[10];

void setup() {

  size(319,240);
  frameRate(15);

  for(int i=0; i<10; i++) {
    images[i] = loadImage("marey_" + i + ".gif");
  }

}

void draw() {
  image( images[compteur], 0, 0);
  compteur++;
  if (compteur >= 10) compteur = 0;
}

Ce programme devrait jouer automatiquement la séquence de l’oiseau. En plus, il devrait le jouer en boucle, comme s’il suivait la forme du Fusil Chronophotographique :

Notez que j’ai ralenti le Sketch un petit peu à 15 lectures par seconde du draw(). Normalement un Sketch appelle le draw() 60 fois par seconde, ce qui aurait donné une animation extremement rapide. Essayez-le d’ailleurs, cela vous permettrez peut-être de comprendre encore mieux le procédé.

liste.length

Il existe un mot très pratique en Java qui facilite le travail avec des listes et que je recommande fortément : length.

Que feriez-vous si vous vouliez changer le nombre d’images dans notre exemple précédent? Auriez-vous changé chaque ligne qui contient la valeur 10? Et si notre programme était très longue, comment être sûr d’avoir changé toutes ces valeurs fixes?

Autant laisser l’ordinateur faire le travail à notre place. L’ordinateur connait la largeur de notre liste, laissez-lui gérer sa largeur en utilisant la formule uneListe.length à la place du chiffre fixe “ 10 ”.


int compteur = 0;
PImage[] images = new PImage[10]; // définir le nombre d'élements

void setup() {

  size(319,240);
  frameRate(15);

  for(int i=0; i<images.length; i++) { // laisser l'ordinateur déterminer combien
    images[i] = loadImage("marey_" + i + ".gif");
  }

}

void draw() {
  image( images[compteur], 0, 0);
  compteur++;
  if (compteur >= images.length) compteur = 0; // si on est à la fin de la liste, revenons à 0
}

Nous recommanons d’ailleurs de ne jamais mettre des valeurs fixes dans votre programme quand il s’agit de listes. A part bien sûr la déclaration première où vous définissez le nombre d’elements : Type[] variable = new Type[combien]. L’expérience démontre que les listes sont souvent des choses que vous aurez envie de modifier plus tard dans votre programme : les listes sont par définition l’exploration de la multiplicité dans la programmation; autant explorer toutes les variations possibles de cette multiplicité sans rendre illisible/ingérable votre programme.