Ateliers Processing de l'OA

L'idée de ce premier atelier était d'implémenter un algorithme d'art contemporain proposé par Sol LeWitt.

L'idée a été trouvée sur le site de Pol Guezennec

Bon, c'est vrai qu'on commence sur des chapeaux de roues, avec l'utilisation des boucles `for` et des listes, mais j'essaierai de garder un niveau de complexité constant, afin de ne pas pénaliser ceux·elles qui raccrocheraient le wagon en cours d'année. Si ce premier sketch vous semble compliqué (et il l'est lorsqu'on débute), les suivants devraient vous paraître de plus en plus simples, à force de répétition.

FloatList liste_x = new FloatList();
FloatList liste_y = new FloatList();
 
void setup() {
  // Dans la fonction setup on mets les instructions qui n'ont
  // besoin d'être exécutés qu'une seule fois, au démarrage
 
  size(500, 500);
  background(255);
 
  for (int i = 0; i < 50; i = i+1) {
    liste_x.append(random(width));
    liste_y.append(random(height));
  }
}
 
void draw() {
  // La fonction draw s'exécute à chaque rafraichissement de l'écran (60 fois/secondes par défaut)
 
  stroke(random(255), random(255), random(255));  // Couleur des contours
  strokeWeight(0.1);                              // Épaisseur des countours
  int i0 = int(random(50));
  int i1 = int(random(50));
  line(liste_x.get(i0), liste_y.get(i0), liste_x.get(i1), liste_y.get(i1));
}

Ici nous abordons les boucles “for” pour répéter un bloc d'instructions. Nous imbriquons deux boucles “for” pour créer la grille sur deux dimensions.

int diametre = 40;  // Diamètre des cercles
 
void setup() {
  size(500, 500);
  noStroke(); // Désactive le countour des formes
  fill(#FFE990); // Couleur de remplissage des cercles
}
 
void draw() {
  background(#90A5FF); // On repeint le fond
 
  for (int j = 0; j < height; j += diametre) {
    // A chaque tour de la boucle externe on descend d'une ligne
    for (int i = 0; i < width; i += diametre) {
      // A chaque tour de la boucle interne on décalle d'une colonne
      int posx = i + diametre/2;
      int posy = j + diametre/2;
      // On calcule la distance entre le centre de chaque cercle et le curseur de la souris
      float d = dist(posx, posy, mouseX, mouseY);
      circle(posx, posy, d * 0.18);
    }
  }
}

Pour sortir de la monotonie des lignes droites, essayons-nous aux courbes !

Ce sketch est interactif. Cliquez dans la fenêtre pour rajouter des points d'ancrages à la courbe.

ArrayList<PVector> points = new ArrayList();
 
void setup() {
  size(500, 500);
  noFill();
}
 
void draw() {
  background(255);
  beginShape();
  curveVertex(0, 0);  // On rajoute un premier point de contrôle aux mêmes coordonnées que le premier point d'ancrage de la courbe
  curveVertex(0, 0);
 
  for (PVector p : points) {
    p.x = p.x + random(-1,1)*2;  // On modifie légèrement les coordonnées de chaque points pour l'effet de vibration
    p.y = p.y + random(-1,1)*2;
    curveVertex(p.x, p.y);
  }
 
  curveVertex(width, height);
  curveVertex(width, height);  // Un dernier point de contrôle pour terminer la courbe
  endShape();
 
  for (PVector p : points) {
    circle(p.x, p.y, 10);
  }
}
 
void mousePressed() {
  points.add(new PVector(mouseX, mouseY));  // Chaque clique ajoute un nouveau points aux coordonnées du curseur de la souris
}

Dans le style de l'harmonographe.

ArrayList<PVector> list = new ArrayList();
 
void setup() {
  size(500, 500);
  background(255);
  strokeWeight(0.2);
}
 
void draw() {
  float x = 200 * cos(millis() * 0.005);
  float y = 200 * sin(millis() * 0.003);
  fill(0, 0);
  //background(255);
  beginShape();
  curveVertex(width*0.5, 50);
  curveVertex(width*0.5, 50);
  curveVertex(width*0.5 + x, height*0.5 + y);
  curveVertex(width*0.5, height-50);
  curveVertex(width*0.5, height-50);
  endShape();
}
 
void mouseClicked() {
  list.add(new PVector(mouseX, mouseY));
}

Je ne m'attendais pas à voir venir beaucoup de monde le 4 Janvier, pour le premier atelier Processing de cette nouvelle année. Puisqu'à l'heure prévue il n'y avait qu'Alex et moi, j'ai voulu proposer quelque chose d'un peu plus complexe que d'habitude. L'idée était de créer des spirales denses, à la façon des sillons de disques vinyle.

La méthode la plus “simple” (à condition de connaître un peu de trigonométrie) est de faire usage des coordonnées polaires. Tout à fait approprié dans les conditions arctiques que nous avons actuellement à la Baleine. Ne vous laissez pas intimider par ces mathématiques froides et souvenez-vous que l'essentiel est de dessiner des jolis trucs à l'écran.

Le mini cours de trigo sur les coordonnées polaires

Par Brews_ohare — Own wwork, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=3891159

Pour décrire la position d'un point dans un espace en deux dimensions, on a l'habite d'utiliser les coordonnées cartésiennes (x,y)x représente la distance depuis l'origine sur l'axe horizontal et y la distance sur l'axe vertical. Sur la figure précédente, l'origine est en bas à gauche du repère. Dans Processing l'origine du repère cartésien se trouve en haut à gauche. Normalement là je ne vous apprend rien.

Une autre façon pour décrire la position d'un point est par les coordonnées polaires (φ,θ), ou φ est la distance euclidienne à vol d'oiseau entre l'origine et notre point, et θ est l'angle entre la droite origine-point et l'axe horizontal.

Si on imagine un cercle centré sur l'origine du repère et traversant le point p, alors la distance φ est égal au rayon de ce cercle. D'ailleurs les lettres grecs sont un peu pénibles à taper au clavier alors on utilisera plutôt les lettres (r,a) pour nos coordonnées polaires.

On peut aussi imaginer le cadran d'un horloge avec les chiffres des heures situés sur le périmètre du cercle. Dans ce cas toutes les heures on la même coordonnée r (elles sont toutes à la même distance du centre, correspondante au rayon du cadran) mais elles ont toutes une coordonnée a (angle) différente. L'angle 0 (zéro) se situerait à 3 heures. “12h” aurait l'angle +90° et “9h” aurait l'angle -90°. Si on fait un tour complet (360°) on revient sur le même point, donc les coordonnées (r, 10°) et (r, 370°) décrivent exactement la même position.

Bon alors il y a une subtilité : en trigonométrie on ne compte pas les angles en degrés comme tout le monde, mais en radians, qui permettent de donner un angle en fraction de PI. Un tour de cercle complet (360°) fait 2×PI radians. Vous l'aurez deviné, un demi tour de cercle (180°) fait donc PI radians. Sur notre cadran d'horloge, “12h” est à l'angle PI/2 radians et “9h” est à l'angle -PI/2 radians (ou bien (3/4)×PI, si on tourne toujours dans le même sens). C'est le fameux cercle trigonométrique, où l'angle croît dans le sens anti-horaire.

J'espère que vous n'avez pas la tête qui tourne trop, car il y encore une autre subtilité. Vous vous souvenez peut-être que dans Processing (et beaucoup d'autres environnement de programmation), l'axe y est inversé (y grandit de haut en bas) ? Et bien c'est la même chose pour le sens de rotation. Sous processing, l'angle grandit dans le sens horaire, contrairement aux conventions mathématiques !

Enfin, connaissant les coordonnées polaires un point, on peut calculer ses coordonnées cartésiennes (essentiel pour se situer sur une grille de pixels, comme celle de notre fenêtre graphique) en appliquant les formules suivantes :

x = r × cos(a) et y = r × sin(a), où x et y sont nos coordonnées cartésiennes et r et a sont nos coordonnées polaires (avec a en radian bien entendu). Les fonctions cos() et sin() se trouvent comme telles dans Processing, et on a même les fonctions radians() pour convertir les angles en degrés vers radians, et degrees() pour convertir les angles en radians vers degrés.

Ouf ! On va enfin pouvoir programmer !

void setup() {
  size(500, 500);
}
 
void draw() {
  background(255);
 
  translate(width/2, height/2); // Pour déplacer l'origine au milieu de la fenêtre
 
  // On initialise les variables dont on aura besoin
  PVector p1 = new PVector();  // Un premier point
  PVector p2 = new PVector();  // Un deuxième point
  float angle = 0.0;           // l'angle actuel (en radians)
  float radius = 0.0;          // le rayon actuel
 
  while (radius < width*0.5) {
    p1.set(radius * cos(angle), radius * sin(angle));  // On définit un premier point aux coordonnées actuelles
    angle += 0.2f;                                     // On augmente légèrement l'angle (en radians)
    radius += 0.3f;                                    // et le rayon
    p2.set(radius * cos(angle), radius * sin(angle));  // On définit le deuxième point aux nouvelles coordonnées
    line(p1.x, p1.y, p2.x, p2.y);                      // On trace une ligne entre nos deux points
 
    // Et on recommence ! (tant que le rayon est inférieur à un certain seuil)
  }
}
 
 
void keyPressed() {
  // Pratique pour exporter des captures d'écran
  // (elles seront placés dans le sous-dossier "data" du sketch)
  // On peut ouvrir le dossier du sketch avec le raccourci Ctrl+K
  if (key == 'p') {
    saveFrame("####.png");
  }
}

Je suis resté longtemps à progra-dessiner en 2D avant d'oser franchir le pas de la 3D avec Processing. Et pourtant il suffit de pas grand chose pour rajouter une toute nouvelle dimension à vos créations. Avec la librairie PeasyCam vous pourrez naviguer très facilement à l'aide de la souris pour admirer vos œuvres sous tous les angles, même les dessins plats.

Pour installer la librairie PeasyCam, assurez-vous d'être relié à Internet puis cliquez sur le menu “Sketch” > “Importer une librairie…” > “Manage librairies” (chez moi c'est en anglais). Écrivez “peasycam” dans le champ de recherche et enfin cliquez sur le bouton “Install”.

import peasy.*;  // On importe la librairie peasyCam
 
PeasyCam cam;    // Cette variable va contenir les infos de notre caméra
 
void setup() {
  size(800, 800, P3D);            // On choisit le moteur de rendu "P3D" pour la 3D
  cam = new PeasyCam(this, 400);  // On crée la caméra
}
 
void draw() {
  background(255);
 
  PVector p1 = new PVector();
  PVector p2 = new PVector();
  float angle = 0.0;
  float radius = 0.0;
 
  while (radius < width*0.5) {
    p1.set(radius * cos(angle), radius * sin(angle));
    angle += 0.1f;
    radius += 0.02f;
    p2.set(radius * cos(angle), radius * sin(angle));
    float seg_angle = sin(2 * atan2(p2.y-p1.y, p2.x-p1.x));  // Un peu de magie trigonométrique
    stroke(seg_angle*200);                                   // Qui colore les segments en fonction de l'angle que forment les deux points
    line(p1.x, p1.y, p2.x, p2.y);
  }
}

Voilà ce que ça peut donner, avec une bonne dose d'obstination de persévérance et de caféine trigonométrie.

Le texte pour l'étiquette a été fait avec Inkscape (le fichier, nommé “label.png”, doit être placé dans le répertoire du sketch).

import peasy.*;
 
PeasyCam cam;
 
float LABEL_RADIUS = 130.0f;
float RECORD_RADIUS = 400f;
PVector p1 = new PVector();
PVector p2 = new PVector();
float rec_rot = 0.0f;
float rot_speed = 0.02f;
PImage label;
 
 
void setup() {
  size(500, 500, P3D);
  //fullScreen(P3D);
  hint(DISABLE_DEPTH_TEST);  // Pour éviter les problèmes de superposition lorsqu'on dessine plusieurs objets sur le même plan en 3D
  cam = new PeasyCam(this, 400);
  label = loadImage("label.png");
}
 
void draw() {
  background(255);
 
  // Dessiner les sillons du vinyle
  fill(0);
  noStroke();
  circle(0, 0, 2 * RECORD_RADIUS);
  strokeWeight(1.4);
  float angle = rec_rot;
  float radius = LABEL_RADIUS;
  while (radius < LABEL_RADIUS + 34) {
    p1.set(radius * cos(angle), radius * sin(angle));
    angle += 0.05f;
    radius += 0.14f;
    p2.set(radius * cos(angle), radius * sin(angle));
    float seg_angle = 1.02 * sin(2 * atan2(p2.y-p1.y, p2.x-p1.x));
    seg_angle *= seg_angle * seg_angle;
    seg_angle += 0.5 * sin(2 * atan2(p2.y-p1.y, p2.x-p1.x) + PI);
    seg_angle += random(0.2f);
    seg_angle *= 230;
    stroke(seg_angle * 0.8, seg_angle*0.8, seg_angle);
    line(p1.x, p1.y, p2.x, p2.y);
  }
  while (radius < RECORD_RADIUS - 14) {
    p1.set(radius * cos(angle), radius * sin(angle));
    angle += 0.03f;
    radius += 0.005f;
    p2.set(radius * cos(angle), radius * sin(angle));
    float seg_angle = 1.02 * sin(2 * atan2(p2.y-p1.y, p2.x-p1.x));
    seg_angle *= seg_angle * seg_angle;
    seg_angle *= seg_angle * seg_angle;
    seg_angle += 0.5 * sin(2 * atan2(p2.y-p1.y, p2.x-p1.x) + PI);
    seg_angle += random(0.1f);
    seg_angle *= 230;
    stroke(seg_angle * 0.8, seg_angle*0.8, seg_angle);
    line(p1.x, p1.y, p2.x, p2.y);
  }
 
  // L'étiquette centrale
  noStroke();
  fill(255, 255, 0);
  circle(0, 0, LABEL_RADIUS * 2);
  rotate(rec_rot);
  image(label, -label.width*0.5, -label.height*0.5);
  // Le trou central
  fill(255);
  circle(0, 0, 20);
 
  rec_rot += rot_speed;
  if (rec_rot > TWO_PI) {
    rec_rot -= TWO_PI;
  }
}

Encore un truc de saison, saupoudré de kitshitude, avec cette ode à l'hiver : nous allons faire tomber de flocons. Une façon d'aborder les particules et de s'émerveiller devant son écran.

On commence par trouver une belle image de flocon sur internet (je ne sais plus d'où je l'ai sorties donc excusez l'absence de source et de licence…)

La seconde image a été dérivée de la première en y appliquant un flou gaussien dans le logiciel Gimp.

PImage fl_flou;
PImage fl_moyen;
PImage fl_petit;
 
ArrayList<PVector> flocons_pos = new ArrayList();  // Liste qui contiendra la position de chaque flocon
ArrayList<PImage> flocons_img = new ArrayList();   // Liste qui contiendra l'image de chaque flocon
 
 
void setup() {
  size(800, 600);
 
  fl_flou = loadImage("snowflake_flou.png");
  fl_moyen = loadImage("snowflake_400.png");
  fl_moyen.resize(100, 0);     // On redimensionne l'image à 100 pixels de largeur,
  fl_petit = fl_moyen.copy();
  fl_petit.resize(50, 0);      // le second argument '0' permet de garder la même proportion pour la hauteur
 
  for (int i=0; i<30; i++) {   // On crée 30 petits flocons
    flocons_img.add(fl_petit);
    flocons_pos.add(new PVector(random(width), random(height)));
  }
  for (int i=0; i<10; i++) {   // 10 flocons moyens
    flocons_img.add(fl_moyen);
    flocons_pos.add(new PVector(random(width), random(height)));
  }
  for (int i=0; i<4; i++) {    // et 4 gros flocons
    flocons_img.add(fl_flou);
    flocons_pos.add(new PVector(random(width), random(height)));
  }
}
 
 
void draw() {
  background(255);
 
  for (int i=0; i<flocons_img.size(); i++) {
    PImage img = flocons_img.get(i);
    PVector pos = flocons_pos.get(i);
    pos.y += img.width * 0.01;      // Le floncon tombe à une vitesse proportionelle à sa taille
    pos.x += random(-1, 1);
    pos.y += random(-1, 1);
    pos.z += random(0, 1) * 0.1;    // Rotation du flocon
    if (pos.y > height + img.height/2) {
      pos.y = -img.height/2;        // On replace le flocon au dessus de la fenêtre
      pos.x = random(width) - img.width/2;  // Avec une position horizontale aléatoire
    }
    // Les instructions suivantes permettent de faire une rotation et une translation du flocon
    push();
    translate(pos.x, pos.y);
    rotate(pos.z);
    image(img, -img.width*0.5, -img.height*0.5);
    pop();
  }
}

Pour le moment l'animation des flocons est saccadée puisque qu'ils sautent d'une position à une autre (à une distance aléatoire). Ça donne un effet stop-motion assez sympa, mais si on veut avoir des mouvements plus naturels il va falloir procéder d'une autre façon : en utilisant un variable de vitesse.

PImage fl_flou;
PImage fl_moyen;
PImage fl_petit;
 
ArrayList<PVector> flocons_pos = new ArrayList();
ArrayList<PImage> flocons_img = new ArrayList();
ArrayList<PVector> flocons_vel = new ArrayList();  // Liste qui contiendra la vitesse (linéaire et angulaire) de chaque flocon
 
 
void setup() {
  size(500, 500);
 
  fl_flou = loadImage("snowflake_flou.png");
  fl_moyen = loadImage("snowflake_400.png");
  fl_moyen.resize(100, 0);
  fl_petit = fl_moyen.copy();
  fl_petit.resize(50, 0);
  for (int i=0; i<30; i++) {
    flocons_img.add(fl_petit);
    flocons_pos.add(new PVector(random(width), random(height)));
  }
  for (int i=0; i<10; i++) {
    flocons_img.add(fl_moyen);
    flocons_pos.add(new PVector(random(width), random(height)));
  }
  for (int i=0; i<4; i++) {
    flocons_img.add(fl_flou);
    flocons_pos.add(new PVector(random(width), random(height)));
  }
 
  // Donne une vitesse aléatoire (la troisième valeur étant la vitesse de rotation) à chaque flocon
  for (int i=0; i<flocons_img.size(); i++) {
    flocons_vel.add(new PVector( random(-1, 1) * 0.01, random(-1, 1) * 0.01, random(-1, 1) * 0.1 ));
  }
}
 
 
void draw() {
  background(255);
 
  for (int i=0; i<flocons_img.size(); i++) {
    PImage img = flocons_img.get(i);
    PVector pos = flocons_pos.get(i);
    PVector vel = flocons_vel.get(i);  // On récupère la vitesse de ce flocon
 
    // On ajoute une accélération aléatoire à la vitesse
    vel.x += random(-1, 1) * 0.004;
    vel.y += random(-1, 1) * 0.002;
    vel.z += random(-1, 1) * 0.001;
 
    pos.y += img.width * (vel.y + 0.04);  // La position augmente en fonction de la vitesse
    pos.x += vel.x;
    pos.z += vel.z;
    if (pos.y > height + img.height/2) {
      pos.y = -img.height/2;
      pos.x = random(width) - img.width/2;
      vel.y = 0;
    }
    // Les instructions suivantes permettent de faire une rotation et une translation du flocon
    push();
    translate(pos.x, pos.y);
    rotate(pos.z);
    image(img, -img.width*0.5, -img.height*0.5);
    pop();
  }
}

On continue de suivre le fil des saisons avec une idée proposée par Martin et inspirée par un sketch, Impluvium de Pol Guezennec.

La particularité de cet exercice (et sa complexité) tient dans le fait qu'il y a deux états à l'animation : le premier lorsque la goutte tombe verticalement, le second lorsque 3 ondes croissent jusqu'à atteindre chacun leur taille maximale.

Puisqu'il n'y a que ces deux états on pourra utiliser une variable etat de type boolean (valeur binaire true ou false) pour définir l'état actuel de notre euh… élément aqueux.

L' etat passera de false (goutte tombante) à true (onde croissante) lorsque la goutte sera tombée d'une hauteur supérieure à la composante y de la variable pg. pg est un vecteur à deux composantes (x et y) qui définit à la fois le point de départ (sur l'axe horizontal) et le point d'arrivée (sur l'axe vertical) de notre goutte. Les valeurs de pg seront réinitialisées au hasard à chaque nouveau cycle pour ajouter un peu de variété à l'animation.

L'illusion n'est pas parfaite car la goutte disparaît instantanément après impact pour laisser place aux ondes. Si on était soucieux du réalisme on tronquerait progressivement la partie inférieur de la goutte qui est au-delà du point d'impact mais bon… L'animation est suffisamment rapide pour qu'on y voit que du feu !

// Variables de l'état "goutte"
float vitesse_goutte = 30;  // Vitesse verticale (en pixel/frame)
float taille_goutte = 70;
float inclinaison = 20;     // Décallage horizontal (en pixels) entre le haut et le bas de la goutte
PVector pg = new PVector(random(500), 370);  // Contient la coordonnée horizontale de la goutte (x)
                                              // Et la coordonnée verticale du point d'impact final (y)
PVector p1 = new PVector(pg.x, -taille_goutte);  // Point supérieur de la goutte
PVector p2 = new PVector(pg.x + inclinaison, 0); // Point inférieur de la goutte
 
// Variables de l'état "onde"
float vitesse_onde = 3;     // Vitesse de croissance des ondes (en pixel/frame)
float decalage_onde = 40;   // Décallage entre chaque onde (en pixels)
float taille = 0.0;         // Taille de la première onde, à chaque instant
float taille2 = -1 * decalage_onde;  // Taille de la deuxième onde, à chaque instant
float taille3 = -2 * decalage_onde;  // Taille de la troisième onde, à chaque instant
float taille_max = 150;
float ratio = 2.5;          // Ratio entre la largeur et la hauteur de l'onde
 
boolean etat = false;      // État goutte si "true, état onde si "false"
 
 
void setup() {
  size(500, 500);
  stroke(#9D62FF);
  strokeWeight(2);
  noFill();
}
 
void draw() {
  background(255);
 
  if (etat == false) {
    // Goutte d'eau
    p1.y += vitesse_goutte;      // La goutte descend (verticalement)
    p2.y += vitesse_goutte;
    p1.x += vitesse_goutte * inclinaison / taille_goutte;  // La goutte se décalle en fct de son inclinaison
    p2.x += vitesse_goutte * inclinaison / taille_goutte;
    line(p1.x, p1.y, p2.x, p2.y);
 
    if (p2.y > pg.y) {
      etat = true;
      pg.x = p2.x;
    }
  } else {
    // Ondes
    taille = taille + vitesse_onde;
    taille2 = taille2 + vitesse_onde;
    taille3 = taille3 + vitesse_onde;
 
    if (taille > 0 && taille < taille_max)
      ellipse(pg.x, pg.y, ratio * taille, taille);
    if (taille2 > 0  && taille2 < taille_max)
      ellipse(pg.x, pg.y, ratio * taille2, taille2);
    if (taille3 > 0)
      ellipse(pg.x, pg.y, ratio * taille3, taille3);
 
    if (taille3 > taille_max) {
      taille = 0.0;
      taille2 = -1 * decalage_onde;
      taille3 = -2 * decalage_onde;
      etat = false;
      pg = new PVector(random(0, 500), random(200, 400));
      p1 = new PVector(pg.x, -taille_goutte);
      p2 = new PVector(pg.x + inclinaison, 0);
    }
  }
}
  • atelier/processing/start.txt
  • Dernière modification: 2023/03/02 23:39
  • par gweltaz