Archives de catégorie : Design Génératif [techniques]

Liste des cours Processing + P5JS 2016-2017 [Digital Lab]

Retrouvez la liste complètes des cours de digital lab — processing/P5js (3e. année, option) sur l’année 2016-2017

Semestre 1

Sujet + liste des cours Processing 2015-2016 [Digital Lab]

Retrouvez la liste complètes des cours et le sujet travaillés en cours de digital lab — processing (3e. année, option) sur l’année 2015-2016

Sujet 2015-2016

Cours 2015-2016

 

Documenter son travail de creative coding

La documentation d’un travail est une partie indispensable à tout projet. D’une part parce que la documentation d’un projet permet d’en retracer la genèse mais également parce qu’elle permet d’en retrouver les sources. Cela est d’autant plus utile dans le cadre d’un projet de creative coding.

Qui ne s’est jamais replongé dans un projet une semaine, un mois ou un an après en se posant l’une des questions suivantes :

  1. « Pourquoi avais-je fais ces choix graphiques ? »
  2. « Quelle était la problématique de départ? Quels étaient mes conclusions ? »
  3. « Que peut bien faire cette fonction ? Comment fonctionne-t-elle ? »

Autant de questions qui montrent l’importance d’un travail documenté.

Commenter son code

L’une des premières méthodes de documentation est sans aucun doute l’ajout de commentaires au code source du projet. Évidemment il est inutile de documenter la moindre ligne de code. Ainsi il est plus judicieux de documenter les algorithmes dont il sera important se de rappeler du fonctionnement.

L’exemple suivant montre une méthode commentée permettant de calculer la position d’un point sur un cercle.

PVector getPositionOnCircle(float radius, float eta)
{
   //calcul des coordonnées x,y d'un point sur un cercle de rayon radius à l'angle eta
   float x = cos(eta) * radius;
   float y = sin(eta) * radius;
   return new PVector(x, y);
}

Dans le cas de certaines fonctions nous utiliserons des commentaires plus détaillés permettant de retrouver le détail de l’algorithme. L’exemple suivant montre une fonction documentée permettant d’appliquer une rotation à un vecteur en 3 dimensions autour d’un vecteur servant d’axe.

PVector computeRodrigueRotation(PVector k, PVector v, float theta)
{
  /* Rotation du vecteur v autour du vecteur k selon l’angle theta; 
  Based on Onlinde-Rodrigue Formula : https://en.wikipedia.org/wiki/Rodrigues'_rotation_formula
  Olinde Rodrigues formula : Vrot = v* cos(theta) + (k x v) * sin(theta) + k * (k . v) * (1 - cos(theta));
  */
  PVector kcrossv = k.cross(v); //Soit kcrossv le Cross Product (produit vectoriel) des vecteur k et v
  float kdotv = k.dot(v); //Soit kdotv le Dot Product (produit scalaire) des vecteur k et v

  float x = v.x * cos(theta) + kcrossv.x * sin(theta) + k.x * kdotv * (1 - cos(theta)); 
//Position x du vecteur V après rotation
  float y = v.y * cos(theta) + kcrossv.y * sin(theta) + k.y * kdotv * (1 - cos(theta)); 
//Position y du vecteur V après rotation
  float z = v.z * cos(theta) + kcrossv.z * sin(theta) + k.z * kdotv * (1 - cos(theta)); 
//Position z du vecteur V après rotation
 
  PVector nv = new PVector(x, y, z); //Vecteur V après rotation
  nv.normalize(); //Normalization du vecteur V
  return  nv;
}

Codes et attribution

Réutiliser des codes (entier ou par portions) trouvés sur internet est un bon exercice pratique cependant la réutilisation de code sans en avoir citer la source peut être considérée comme de la copie et du plagiat.

Le developpement a toujours impliqué l’utilisation de code provenant d’autres sources et nous avons la chance de profiter d’une communauté open source. Sans le partage de savoir de cette communauté celle-ci ne grandirait pas et votre travail ne verrait sans doute pas le jour. C’est la raison pour laquelle il est éthique de citer ses sources.

Tout comme vous citez les sources original dans vos travaux graphiques et écrits, vous devez citer l’ensemble des sources techniques afférant à votre projet tel que les auteurs des algorithmes ou outils incorporés à vos projets ou les documents de recherches et cours utilisés dans la création de votre projet.

Texte original de Scott Murray, Assistant Professor, Design Department of Art + Architecture, University of San Francisco.

Lorsque que vous utilisez une fonction ou un code réalisé par un autre développeur il est donc important d’en citer la source. Tout comme le commentaire de fonction, citer la source vous permet à vous et aux autres personnes qui auront accès à votre code de retrouver l’origine de l’algorithme, du programme et de sa documentation. Lorsque nous citons la provenance d’une méthode nous utiliserons la méthode suivante :

/******************************************************************
*    Titre du programme d’origine : <Titre du programme d’origine>
*    Auteur : <auteur>
*    Date : <date si disponible>
*    Version du code : <version si disponible>
*    Lien vers la source : <Lien du site>
*
*********************************************************************/

edit (26/11/2015) : Jonathan, collègue enseignant et co-créateur du site disruptions.fr, m’a fait remonter un commentaire en ce qui concerne l’importance de la citation du code d’autrui :

En ce qui concerne les citations du code d’autrui, en plus de l’aspect morale, il y a un côté bassement pragmatique – parfois le code ne fonctionne pas dans telle ou telle condition. D’avoir le lien accessible permet de vérifier s’il y a une mise à jour [tenant compte de votre problème] ou de dialoguer avec l’auteur pour déceler le problème.

Jonathan Munn

Documenter l’ensemble de son travail

ixd-documentation
Extrait de la documentation d’un projet d’installation interactive — Bonjour, interactive lab 2015

Si l’utilisation des commentaires permet à la fois de documenter son code et d’en citer les sources et références, ils ne permettent pas d’incorporer des images ou vidéos. Lors de la réalisation d’un projet il est important de tenir à jour une documentation complète permettant à tout instant de revenir en arrière sur la phase d’un projet. Cette documentation est destinée avant tout à nous même et aux collaborateurs du projet. Elle ne requiert donc pas de mise en forme particulière et peut tout à fait prendre la forme d’un document texte. Pour ma part mes documentations prennent la forme d’un document texte reprenant les points suivants :

  1. Concept
  2. Références
  3. Détail de l’idée
  4. Parcours utilisateur
  5. Questionnement :
    remise en cause ou points à explorer durant le projet
  6. Pour aller plus loin :
    Déclinaisons et évolutions possibles du projet
  7. Scénographie / format d’exploitation
  8. Cahier des charges techniques :
    Technologies envisagées, langages, librairies…
  9. Notes de développement :
    Note au jour le jour du développement effectué indiquant les problèmes rencontrés, les solutions envisagées, les sources et les références.

Une fois le projet terminé, le document pourra alors être remise en forme afin de réaliser le making of ou servir aux évolutions futur du projet.

Introduction à Processing [3.0]

Processing est un langage de programmation open source basé sur JAVA et un IDE (integrated development environment) créé par Ben Fry et Casey Reas en 2001. Leur projet était de créer un langage simple, destiné aux graphistes et artistes, afin de répondre à certains de leur besoins tel que la visualisation de données ou la production de visuels génératifs. Processing est le prolongement « multimédia » de Design by Numbers, l’environnement de programmation graphique développé par John Maeda au MIT Media Lab.

Processing a bien évolué depuis sa création en 2001. S’il est toujours un véritable outils de production pour les artistes et designers interactifs il est également une plateforme d’apprentissage de la programmation à destination de l’éducation. Lors de sa conférence à Eyeo 2015, Casey Reas présenta notamment l’évolution de Processing mais également le developpement de la Fondation Processing autour de nouveaux outils tel que P5.js et Processing.py.

Processing est basé sur JAVA, rendant ainsi le langage plus facile d’accès par le biais d’une syntaxe simple sans pour autant simplifier JAVA, cela permet notamment d’accéder à l’ensemble de possibilités offertes par JAVA. Il intègre également OpenGL permettant ainsi d’accéder à de plus grandes capacités graphiques dans le traitement d’éléments 2D et 3D.

Si nous devions faire un rapide état des lieux des principaux langages utilisés dans le design génératif ou l’art numérique en règle générale nous aurions :

  • C/C++ avec des librairies tel que OpenFramework, Cinder…
  • Java en natif ou via Processing
  • Javascript en natif ou via des frameworks tel que Angular.js, Tree.js, Backbone.js…
  • Les langages de type nodal avec VVVV, Max MSP/Jitter ou Pure Data

Il existe évidement de nombreux autres langages utilisés dans l’expérimentation interactive et visuelle tel que Flash (même si son utilisation se fait rare de nos jours) ou encore Touch Designer.

Les cours de l’option «Digital Lab» à e-art sup Paris porteront sur les expérimentations visuelles et interactives, qu’elles soient sur écran ou hors écran et ce à travers l’utilisation de Processing. Cette option a pour but d’expérimenter de nouvelles techniques de design apportées par le design par le code et de sortir des techniques habituelles de conception. Il sera une initiation à l’expérimentation interactive et aux cours de développement interactif apportés par la filière Design Interactif d’e-art sup.

Cette année (2015-2016) l’enseignement portera essentiellement sur Processing 3.0, la dernière version stable de Processing.

Introduction et nouveautés

Septembre 2015 a vu la sortie de la version 3.0 de Processing. Cette nouvelle version offre sans doute l’un des plus grands changements de Processing, son IDE devient plus développé et sa base a été repensé. Cet article s’adresse aussi bien aux néophytes qu’aux personnes utilisant déjà Processing. Nous couvrirons aussi bien les bases du langage que ses nouveautés. Afin d’être plus accessible à tout le monde, les paragraphes traitant des nouveautés et d’informations nécessitant une plus grande connaissance du langage seront marqués du signe § orange.

§ Parmi les grands changements de Processing 3.0 nous noterons :

  • La reconstruction du moteur de rendu OpenGL (2D et 3D) aujourd’hui plus rapide et sans saccade. On note également l’apparition d’un nouveau moteur de rendu, le Java FX2D.
  • La création d’un nouvel éditeur comportant :
    • Une numérotation des lignes
    • La possibilité d’utiliser de l’autocomplétion
    • L’ajout d’un debugger simple (temps réel) et avancé
    • L’ajout du TweakMode en natif (souffre encore de quelques bugs sous windows 7, 8, 8.1 et 10)
  • Le support des résolutions retina via les méthodes pixelDensity() et displayDensity()
  • Le retrait de l’utilisation de variables avec la méthode size(). Il est toujours possible de changer la taille du programme via la méthode surface.setSize(w, h)
  • L’objet frame devient l’objet surface
  • La réécriture de l’objet PApplet. De part ce changement de nombreux sketch et librairies risquent de ne pas être compatible de processing 2.0 à processing 3.0. Beaucoup ont déjà été mis à jour pour Processing 3.0
  • L’ajout d’une méthode fullScreen() simplifiant la création d’un programme plein écran et offrant la possibilité d’explorer le multi-screen.
  • L’objet PVector supporte désormais la désignation chaînée
  • L’ajout de l’export SVG

Pour plus d’informations sur les changements de Processing 3.0 je vous invite à explorer la liste présente sur le GitHub du projet ou à regarder la vidéo de présentation de Daniel Shiffman.

L’environnement

En tant que Graphiste, l’interface de Processing peut paraitre un peu “aride” lors de son premier lancement. En effet nous sommes tous des habitués de Photoshop et ce qui se rapproche le plus du code pour nous reste Flash avec son interface proche du tableau de bord d’une formule 1. Et comme pour Photoshop ou la formule 1 il va nous falloir comprendre l’interface avant de se lancer dans la production.

L’interface de Processing peut se découper en trois zones :

  • La zone d’outils permettant d’exécuter ou stopper le programme et donnant accès au menu.
  • L’éditeur de texte où sera écrit le code.
  • La zone de console affichant les messages et les erreurs.

processing_interface_00

Trois autre fenêtres vont également nous être utile régulièrement à savoir :

  • La fenêtre Exemple regroupant les exemples fournis avec Processing ou importés avec les librairies
  • La fenêtre Sketchbook regroupant l’ensemble de nos créations et pouvant être paramétrée dans la fenêtre Préférences
  • La fenêtre Préférences regroupant l’ensemble des préférences du logiciel à savoir
    • L’emplacement du dossier sketchbook
    • La langue
    • Police utilisée dans l’éditeur et la console ainsi que leurs tailles respectives
    • La couleur d’arrière plan du mode présentation
    • Le lissage des polices
    • La saisie des caractères non latin
    • La détection des erreurs et affichage des avertissements
    • Utilisation de l’auto-complétion
    • Suggestion des imports
    • L’augmentation de la mémoire vive allouée (par défaut 256Mb)
    • Effacer le dossier précédent lors de l’export (évitant ainsi les nombreux fichiers issus de multiples exports)
    • Vérification des mises à jour au démarrage
    • Taille par défaut du fullscreen
    • Ouvrir tout les fichiers .pde avec processing

processing_interface_01

Dans la fenêtre préférences nous vérifierons que les éléments suivant sont actif :

  • Détecter les erreurs en continu
  • Afficher les avertissements
  • Autocomplétion du code ctrl+espace
  • Augmenter la mémoire vive disponible (augmenter la mémoire en fonction de la quantité de RAM dont vous disposez)

L’espace cartésien

Une fois l’interface appréhendée il est important de connaître la manière dont un programme gère son espace afin de pouvoir y dessiner. Si nous devions faire un rapprochement avec des logiciels que nous connaissons en graphisme cela serait les logiciels de la suite Adobe. Tout comme illustrator, par exemple, Processing possède un système de cordonnées cartésiennes qui nous permet de situer un point dans l’espace (x, y, z). Enfin, tout comme les logiciels que nous connaissons en design, l’origine de ce repère cartésien se trouvera en haut à gauche de notre fenêtre d’exécution.

processing_interface_02

Ainsi pour un programme d’une taille de 800 × 600 pixels, si je dessine un point rouge à la position 750 × 550, un point vert à la position 50 × 50 et un point bleu à la position 400 × 300; ces derniers seront respectivement en bas à droite, en haut à gauche et au centre du programme.

processing_interface_03

La structure du code

Tous programme se base sur un code écrit qui sera ensuite lu/exécuté ligne par ligne. Créer un programme est donc un acte d’écriture, il est donc important de connaître la manière dont le programme lit le code ainsi que la structure de celui-ci.

Les commentaires

Les commentaires sont des lignes de textes ignorées par le programme lors de son exécution. Ils sont à destination du ou des créateurs du programme et permettent de le commenter ou de laisser des notes souvent utiles afin de s’y retrouver. Ils sont notamment utiles lorsque l’on reprend un programme datant de quelques mois ou créé par un autre créateur et dont certaines fonctionnalités sont relativement complexes. Ils sont la syntaxe suivante :

// les double slash sont utilisés pour les commentaires sur une seule ligne
/*
L'utilisation de slash+astérisque permet d'ouvrir un paragraphe de commentaire 
et ainsi être utilisé sur plusieurs lignes. Afin d'être fermé, le paragraphe devra 
être suivi d'un astérisque+slash
*/

Les déclarations

Les déclarations sont les lignes lues par le programme. Elles composent le programme et ses fonctionnalités. Elles peuvent être assimilées à une phrase dans le langage. Comme pour une phrase dans la langue française, il est important d’indiquer à une déclaration qu’elle se termine pour laisser place à une nouvelle déclaration. Si nous utilisons des points pour clôturer nos phrases, dans Processing — comme dans de nombreux autres langages — une déclaration devra se terminer par un point virgule ;. Cela s’explique par l’utilisation du point qui en mathématique permet d’indiquer la présence d’une décimale. Ainsi pour écrire la phrase suivant x est égale à un plus un cela se traduira par :

x = 1+1;

Les fonctions

Les fonctions — ou méthodes— sont des regroupements de déclarations. Elle permettent notamment d’exécuter une suite de déclarations et sont donc des raccourcis extrêmement pratique. Elles sont reconnaissable par leur forme utilisant un nom — en bas de casse généralement — suivi de parenthèses de la manière suivante :

function();

Les parenthèses d’une fonctions permettent de passer à celle-ci différents paramètres qui seront utilisés par la suite de déclarations présente dans la fonction. Processing possède un grand nombre de fonctions. L’exemple suivant montre la fonction permettant de définir la taille d’un programme de 250 × 500 pixels :

size(250, 500);

§ Il existe également des fonctions dites de retour. Il s’agit de fonctions dont les déclarations contenues ont pour objectif de renvoyer un résultat sous la forme d’une variable ou d’un objet. Par défaut toute fonction est une fonction de retour, cependant les fonctions de donnant pas résultat mais effectuant une action sont des fonctions dite void, ou ne renvoyant aucun élément. Dans l’exemple suivant, la fonction calcul() permet d’obtenir le résultat du calcul suivant : 1+1. Elle renverra donc une variable et s’utilisera de la manière suivante :

int x = calcul();
/*
ici la fonction calcul renvoi le résultat de l'opération 1+1
*/

§ Comme vu précédent il est également possible de passer à une fonction de retour des paramètres afin que ceux-ci soit utilisés dans la suite de déclarations contenue dans la fonction. Dans l’exemple suivant, la fonction calcul(int x, int y) permet d’obtenir le résultat du calcul suivant : x+y ou x et y sont des entiers.

int x = calcul(10, 20);
/*
ici la fonction calcul renvoi le résultat de l'opération 10+20
*/

§ La création d’une fonction se fait de la manière suivante :

type nomDeLaFonction(type parametre1, type parametre2)
{
//Déclaration
return resultat;
}

§ Le type présent en début de phrase correspond au type de résultat renvoyé par la fonction. Il peut être de type :

  • void
  • boolean
  • int
  • float
  • char
  • String
  • color
  • Objet

§ Le type void est un type de fonction spécial ne renvoyant rien et effectuant uniquement la suite d’instructions. Sa création se passera donc de la déclaration return.

void dessineUnLigne()
{
//Je dessine une ligne
}

§ À la suite du type de la fonction se trouve le nom de celle-ci suivi de parenthèses permettant d’accueillir des paramètres si nécessaire. Le nombre de paramètres n’est pas limité, il est également possible de ne pas en mettre si la fonction n’en a pas l’utilité. Afin de marquer le début et la fin de la fonction, sa suite de déclarations devra être précédée par une  accolade ouverte { et suivi d’une accolade fermée }. Enfin, toute méthode renvoyant un résultat devra se terminer par la déclaration suivante return leResultat avant l’accolade fermée marquant la fin de la fonction. Ainsi la fonction permettant de calculer le résultat de x*y, où x et y sont des nombre décimaux, s’écrira de la manière suivante :

float getProduct(float x, float y)
{
float product = x * y;
return product;
}

Utilisation de la casse

Lorsque nous écrivons une phrase nous utilisons des bas de casse, des capitales et des espaces. Cela nous permet d’identifier certains mots ou nom mais également de se repérer à l’intérieur de la phrase. Dans de nombreux langages de programmation nous retrouvons également l’utilisation de la capitale et de la bas de casse.

Ainsi dans Processing les noms de variables, leurs types et les fonctions utilisent des bas de casse. Nous aurons donc :

int var = 1; //variable
function(); //fonction

L’utilisation des capitales se fait dans la déclaration d’un objet (ou classe). Par exemple, lorsque je déclare un objet de type String, le type de l’objet prendra une capitale à sa première lettre, son nom de variable quant à lui restera en bas de casse. Nous noterons ici que l’objet String est un objet réunissant des variables de type char une chaîne de caractères étant composée de caractères.

String mot = "Une chaîne de caractères"; //objet de type String

La capitale peut également être utilisée dans les noms des variables ou fonctions afin d’indiquer un espace à la lecture du nom. Cela est notamment utile lorsque nous créons des variables aux noms très descriptif. En effet il sera plus facile de lire angleDeRotationSurAxeX que anglederotationsuraxex. Nous noterons cependant que la première lettre de la variable ou de la fonction restera en bas de casse, l’utilisation de la capitale pour la première lettre étant réservée à la déclaration d’un objet comme vu plus haut.

float angleDeRotationX = 3.14; //variable
effectueUneRotation(); //fonction

La structure d’un programme

Un programme se compose généralement de deux fonctions globales nécessaire à son fonctionnement. La première est la fonction permettant d’initialiser le programme. Il s’agit de la première fonction que lira le programme et qui ne sera exécutée qu’une seule fois. Celle-ci permet notamment de définir la taille du programme, son moteur de rendu — 2D ou 3D — ou de définir des variables.
La seconde fonction est la boucle. Il s’agit d’une fonction qui s’exécute en boucle, permettant ainsi de rafraichir l’affichage du programme et d’exécuter les opérations successivement.

Dans Processing la fonction permettant d’initialiser le programme est la fonction setup(). La fonction permettant d’exécuter le programme en boucle est la fonction draw(). Par défaut, cette dernière s’exécute 60 fois par seconde. Ainsi si je souhaite réaliser un programme affichant le nombre de frames écoulées et de taille 250 × 250 j’écrirai :

void setup()//initialisation du programme
{
   size(250, 250);
}

void draw()
{
   background(255, 255, 255);
   text(frameCount, 25, 25);
}

La grammaire du code

Les variables

La variable est un concept de base de la programmation, il s’agit d’un mot-clef associé à une valeur. Les variables permettent ainsi de nommer des valeurs. Nous utilisons également des variables dans la vie de tout les jours, par exemple chaque personne possède un nom et prénom qui permet de l’identifier. Il en est de même en programmation où nous pouvons nommer la coordonnée x d’une ellipse ellipseCoordX. Il sera alors plus facile d’appeler cette variable par son nom.

Processing possède plusieurs type de variables permettant ainsi de reconnaitre rapidement s’il s’agit d’un nombre entier, d’un nombre décimal ou d’un caractère. Nous pouvons compter parmi ces variables :

  • boolean etat = true; Variable de type boolean (ou binaire), il s’agit d’une variable ne pouvant renvoyer que deux valeurs : Vrai/Faux. Elle permet de faire des comparaisons. Par exemple la réponse à la question “Cette robe est-elle rouge?” est une booléenne car elle ne peut être que vrai ou faux : “Oui elle est rouge” “non elle ne l’est pas”
  • int x = 10; est une variable de type nombre entier (0, 1, 2,  3…)
  • float x = 3.141592; est une variable de type nombre décimal (0.001, 0.002, 0.003…)
  • String nom = “moi”; est un objet renvoyant une chaine de caractères, elle permet de stocker des mots. Il ne s’agit pas à proprement parler d’une variable cependant nous la considérerons comme telle.
  • char nom = “a”; est une variable de type caractère
  • color couleur = color(255, 255, 255); est une variable de type couleur

Les opérateurs (opérations, assignations, relationnels et logiques)

La seconde notion importante en programmation est la notion d’opérateurs. Ils permettent d’effectuer divers calculs ou comparaisons. Nous sommes ici dans des notions de bases des mathématiques. Nous avons différents types d’opérateurs.

Les opérateurs arithmétiques  :

  • +, , *, / qui permettent d’addition, soustraire, multiplier ou diviser des valeurs.
  • % le modulo qui permet de connaitre la valeur d’un reste d’une division (valeur résiduelle) par exemple 17%3 = 2 car 2 est le reste de la division de 17/3

Les opérateurs d’assignation :

  • = permet d’attribuer une valeur à une variable
  • +=, -=, *=, /= permettent d’incrémenter, soustraire, multiplier ou diviser des valeurs par exemple x = x+1 est la même chose que x +=1;

Les opérateurs relationnels (s’utilisant dans le cadre des structures itératives et conditionnelles)

  • >, <, <=, =>, == , != permettent de comparer deux valeurs afin de savoir si celles-ci sont supérieur, inférieur, supérieur ou égale, inférieur ou égale, égale ou différentes. Ces opérateurs ne renvoient que des valeurs booléennes, c’est à dire vrai ou faux. Par exemple 1 == 2 renverra la valeur “false” car 1 n’est pas égale à 2;

Les opérateurs logiques (s’utilisant dans le cadre des structures itératives et conditionnelles)

  • &&, || permettent d’effectuer des opérations booléennes de type ET et OU.

Des variables spécifiques à Processing

Processing possède également différentes variables pré-écrites permettant d’accéder à différentes valeurs numérales ou booléennes tel que :

  • width; Variable renvoyant la largeur du programme déclarée dans la fonction size() lors de l’initialisation du programme.
  • height; Variable renvoyant la hauteur du programme déclarée dans la fonction size() lors de l’initialisation du programme.
  • mouseX; Variable renvoyant la position actuelle sur l’axe X de la souris.
  • mouseY; Variable renvoyant la position actuelle sur l’axe Y de la souris.
  • pmouseX; Variable renvoyant la position précédente — à la frame précédente — sur l’axe X de la souris.
  • pmouseY; Variable renvoyant la position précédente — à la frame précédente — sur l’axe Y de la souris.
  • mousePressed; Variable de type booléenne indiquant si la souris est cliquée (true) ou non cliquée (false).
  • mouseReleased; Variable de type booléenne indiquant si le clique de la souris a été relâché (true) ou non (false).
  • keyPressed; Variable de type booléenne indiquant si une touche du clavier est appuyée (true) ou non (false).
  • keyReleased; Variable de type booléenne indiquant si une touche du clavier appuyée a été relâché (true) ou non (false).
  • frameCount; Variable renvoyant le nombre de frames écoulées.
  • frameRate; Variable renvoyant le nombre de frames par seconde à laquelles s’exécute le programme.

Des méthodes spécifiques à Processing (création, dessin, définition des couleur…)

Nous l’avons vu précédemment, Processing est un langage simplifié de JAVA permettant notamment d’accéder à de nombreuses fonctions de dessins pré-écrites plus rapidement. C’est cette approche qui en fait un langage d’apprentissage et de prototypage idéal pour les artistes, créateurs et graphistes. Parmi ces fonctions nous pouvons compter :

Les fonctions de création, celles-ci sont appelées dans la fonction setup() lors de l’initialisation du programme :

  • size(int largeur, int hauteur); Fonction permettant de définir la largeur et hauteur du programme.
  • size(int largeur, int hauteur, moteur); Fonction permettant de définir la largeur, hauteur et moteur de rendu du programme (P2D, P3D, PDF, FX2D).
  • smooth(int niveau); Fonction permettant de définir le niveau antialiasing. Par défaut l’antialiasing est activé. Le niveau de l’antialiasing peut être 2, 3, 4 ou 8 en fonction du moteur de rendu.
  • fullScreen(); Fonction permettant de définir la taille du programme en fullscreen.
  • fullScreen(int ecran); Fonction permettant de définir la taille du programme en fullscreen et de choisir l’écran sur lequel l’exécuter lorsqu’on possède plusieurs écrans (1, 2, 3 ou plus en fonction de son nombre d’écran). Nous noterons que l’utilisation de SPAN à la place du numéro de l’écran permet d’exécuter le programme réparti sur l’ensemble des écrans disponibles.
  • fullScreen(moteur); Fonction permettant de définir la taille du programme en fullscreen et de choisir le moteur de rendu (P2D, P3D, PDF, FX2D).
  • fullScreen(int ecran, moteur); Fonction permettant de définir la taille du programme en fullscreen, de définir son moteur de rendu (P2D, P3D, PDF, FX2D) et de choisir l’écran sur lequel l’exécuter lorsqu’on possède plusieurs écrans (1, 2, 3 ou plus en fonction de son nombre d’écran). Nous noterons que l’utilisation de SPAN à la place du numéro de l’écran permet d’exécuter le programme réparti sur l’ensemble des écrans disponibles.

Les fonctions de dessin permettant de dessiner des formes géométriques prédéfinies en 2D ou 3D :

  • point(float x, float y); Fonction permettant de dessiner un point dans un espace 2D.
  • point(float x, float y, float z); Fonction permettant de dessiner un point dans un espace 3D.
  • line(float x1, float y1, float x2, float y2); Fonction permettant de dessiner une ligne dans un espace 2D.
  • line(float x1, float y1, float z1, float x2, float y2, float y3); Fonction permettant de dessiner une ligne dans un espace 3D.
  • triange(float x1, float y1, float x2, float y2, float x3, float y3); Fonction permettant de dessiner un triangle dans un espace 2D.
  • quad(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4); Fonction permettant de dessiner un quadrilatère dans un espace 2D.
  • rect(float x, float y, float largeur, float hauteur); Fonction permettant de dessiner un rectangle dans un espace 2D.
  • ellipse(float x1, float y1, float largeur, float hauteur); Fonction permettant de dessiner une ellipse dans un espace 2D.
  • box(float taille); Fonction permettant de dessiner un cube dans un espace 3D.
  • box(float largeur, float hauteur, float profondeur); Fonction permettant de dessiner un parallélépipède dans un espace 3D.
  • sphere(float rayon); Fonction permettant de dessiner une sphère dans un espace 3D.
  • text(char caractere, float x, float y); Fonction permettant de dessiner un caractère dans un espace 2D.
  • text(char caractere, float x, float y, float z); Fonction permettant de dessiner un caractère dans un espace 3D.
  • text(String mot, float x, float y); Fonction permettant de dessiner une chaîne de caractères dans un espace 2D.
  • text(String mot, float x, float y, flaot z); Fonction permettant de dessiner une chaîne de caractères dans un espace 3D.

Les fonctions de remplissage permettant de définir la couleur de remplissage et de contour.

  • colorMode(mode, int max1, int max2, int max3); Fonction permettant de définir le mode colorimétrique utilisé. Le mode peut être RVB (rouge, verte bleu) ou HSB (teinte, saturation, luminosité). Par défaut Processing est en mode RVB .
    Les valeurs max correspondent aux valeurs maximum des trois composants. Lorsque nous travaillons en teinte, saturation et luminosité nous travaillons la teinte de 0 à 360°, la saturation et luminosité sont quant à elles travaillées de 0 à 100%. Nous écrirons alors :

    colorMode(HSB, 360, 100, 100);

    Dans le cas du mode colorimétrique RVB  nous savons que nos valeurs rouges, vertes et bleues seront comprises entre 0 et 255, nous écrirons alors :

    colorMode(RVB, 255, 255, 255);

    Il est également possible de définir la manière dont nous souhaitons traiter l’opacité; de 0 à 100% ou de 0 à 255. Pour cela nous rajouterons un quatrième paramètre au mode colorimétrique afin de définir la façon dont l’opacité sera traitée :

    /*
    Mode colorimétrique en Teinte, Saturation et Luminosité où :
    La teinte est traité de 0 à 360°
    La saturation, luminosité et l'opacité sont traités de 0 à 100%
    */
    colorMode(HSB, 360, 100, 100, 100);
    
    /*
    Mode colorimétrique en Rouge, Vert et Bleu où l'ensemble des
    valeurs sont traité de 0 à 255
    */
    colorMode(HSB, 255, 255, 255, 255);

     

  • background(float rouge, float vert, float bleu); Fonction permettant de définir la couleur de fond du programme en RVB (ou HSB). On notera que les valeurs RVB sont comprises entre 0 et 255.
    Il est important de noter que sans couleur de fond, le programme dessinera en sur-impression, c’est à dire que les dessins effectués aux frames précédentes seront toujours visible en dessous du nouveau dessin. Cela reviendrait, dans Photoshop, à dessiner sur plusieurs claques superposés. La fonction background() a donc pour rôle «d’effacer» les dessins effectués aux frames précédentes.

    ixd_processingIntro_00
    Lorsque je dessine, à chaque frame, un cercle à la position de ma souris sans utiliser la fonction background() je remarque que les dessins se superposent au fil du temps.

    ixd_processingIntro_01
    Si pour ce même code j’utilise la fonction background(), alors je dessinerai un fond entre chaque frame. Les dessins des frames précédentes seront effacés et je verrai alors mon cercle se déplacer.
  • fill(float rouge, float vert, float bleu); Fonction permettant de définir la couleur de remplissage d’une forme en RVB (ou HSB). On notera que les valeurs RVB sont comprises entre 0 et 255.
  • fill(float rouge, float vert, float bleu, float opacité); Fonction permettant de définir la couleur de remplissage d’une forme en RVB (ou HSB) + Alpha. On notera que les valeurs RVB et Alpha sont comprises entre 0 et 255.
  • stroke(float rouge, float vert, float bleu); Fonction permettant de définir la couleur de contour d’une forme en RVB (ou HSB). On notera que les valeurs RVB sont comprises entre 0 et 255.
  • stroke(float rouge, float vert, float bleu, float opacité); Fonction permettant de définir la couleur de contour d’une forme en RVB (ou HSB) + Alpha. On notera que les valeurs RVB et Alpha sont comprises entre 0 et 255.
  • strokeWeight(float epaisseur); Fonction permettant de définir l’épaisseur d’un contour.

Pour rappel, nous savons que notre programme s’exécute de la première à la dernière ligne, de haut en bas. Ainsi je devrai définir la couleur d’une forme avant que celle-ci soit dessiné de la manière suivante :

/*
Je dessine un carré rouge à bordure bleu, 
de taille 20 et à la position 10, 10
*/
stroke(0, 0, 255);
fill(255, 0, 0);
rect(10, 10, 20, 20);

/*
Je dessine un carré verte à bordure violet, 
de taille 20 et à la position 10, 10
*/
stroke(255, 0, 255);
fill(0, 255, 0);
rect(10, 10, 20, 20);

Il existe évidement de nombreuses autres fonctions de dessin permettant de dessiner des courbes de Bézier, des courbes de Catmull-Rom ou encore des réaliser ses formes personnalisées. Je vous invite à les découvrir parmi les références présentes sur le site processing.org.

Les moteurs de rendu

Comme de nombreux autres langages de programmation, Processing possède différents moteurs de rendu. Un moteur de rendu permet de définir la manière dont le programme dessinera. Il peut être en 2D ou 3D. Comme vu plus haut, le moteur de rendu se défini dans la fonction size() . Dans Processing nous avons différent moteurs de rendu tel que :

  • JAVA2D : moteur de rendu 2D JAVA. Il s’agit du moteur de base de Processing.
  • P2D : moteur de rendu 2D utilisant OpenGL.
  • P3D : moteur de rendu 3D utilisant OpenGL.
  • PDF : moteur de rendu PDF.
  • FX2D : moteur de rendu 2D JAVA accéléré.

Aides

Autocomplétion

L’autocomplétion est la capacité pour un élément donné de se compléter de manière automatique. Il s’agit d’une des nouveautés de Processing 3.0. Une fois activée — via la fenêtre Préférences — elle permet à Processing de vous proposer la suite des éléments (ou paramètres) à écrire dans une fonction. Cela est très utile lorsqu’on ne se rappel plus des paramètres à passer dans une fonction. Pour l’utiliser, il nous suffit d’utiliser la commande CTRL+ESPACE lorsque nous écrivons notre fonction.

Ici, via la commande CTRL+ESPACE, Processing me rappel les paramètres possibles que peut prendre la fonction line().
Via la commande CTRL+ESPACE, Processing me rappel les paramètres possibles que peut prendre la fonction line().

Détection et correction des erreurs

La détection des erreurs en programmation est un élément extrêmement important. Qui n’a jamais été frustré par ce code de 4000 lignes qui refuse de compiler suite à un oublie de point virgule ou à une parenthèse manquante parmi ces lignes. La version 3.0 de Processing propose désormais des outils simple permettant de détecter ces erreurs mais également de nous indiquer où elles se trouvent.

Le premier outils pratique est la détection en continu des erreurs. Un fois activé dans la fenêtre Paramètres, il nous permet de détecter des erreurs durant l’écriture et de les indiquer dans la zone de message. Ainsi lorsque j’écris : background(255) la zone de message m’indiquera le message suivant : «Missing a semicolon « ; »» ou «Point virgule manquant». Par ailleurs la ligne où se trouve mon erreur sera indiquée à droite par un trait rouge et l’erreur en elle même sera souligné en rouge dans le code. Je pourrai alors aisément corriger mon erreur.

continousErrors

La zone de message n’indiquant que la dernière erreur je peux également accéder à la liste complète des erreurs via l’onglet Erreurs situé en dessus de la console. Dans cet onglet je découvre alors la liste complète des erreurs détectées ainsi que leurs lignes et les onglets dans lesquelles elles se trouvent. Je peux par un simple clique sur une ligne de l’onglet d’erreur, surligner la ligne comportant l’erreur en question et ainsi la corriger.

consoleError

§ L’onglet Erreurs me permet également de mettre en évidence des avertissements si cette option a été activé via la fenêtre de Préférences. Un avertissement n’est pas une erreur, il n’empêchera pas le code de compiler. Il s’agit d’avertissement concernant l’optimisation du code, notamment lorsqu’on déclare des variables qui ne sont pas utilisées dans la suite du code. En effet chaque variable déclarée prend une certaine place dans la mémoire allouée au programme, il est donc recommandé de ne pas déclarer des variables qui ne seront pas utilisée.

Les avertissement apparaissent dans l’onglet Erreurs sous la forme de pastilles oranges. Au clique sur la ligne le message d’avertissement apparait indiquant ainsi de quel type d’avertissement il s’agit.

warning

Pour aller plus loin

Si vous souhaitez approfondir la découverte de Processing et de ses nouveautés je vous invite à consulter les références suivante :

Liste des cours Processing 2012-2015 [Digital Lab]

Retrouvez la liste complètes des cours et sujets travaillés en cours de digital lab — processing (3e. année, option)

Cours et sujets 2014-2015

  1. Design génératif et expériences interactives — introduction
  2. Processing 2.0 — langages, grammaire et variables
  3. Grilles et répétitions de motif — itérations et conditions
  4. Aléatoire Brownien et Perlin
  5. Tableaux — introduction aux classes
  6. Programmation orientée objet : les classes
  7. Sujet semestre 1 : Carte de voeux 2015
  8. Analyse audio et réactivité au son
  9. World of sound : Plongez au coeur d’un environnement reactif au son.

Cours et sujets 2013-2014

  1. Design génératif et expériences interactives — introduction
  2. Processing 2.0 — langages, grammaire et variables
  3. Grilles et répétitions de motif — itérations et conditions
  4. Aléatoire Brownien et Perlin
  5. Tableaux — introduction aux classes
  6. Programmation orientée objet : les classes
  7. Temps : frames, horaires et décomptes
  8. Rappel sur les classes et le processus itératif
  9. Sujet : clip interactif (en fin de document)
  10. Analyse audio et réactivité au son
  11. Transformation 2D
  12. Espace 3D et formes primitives
  13. Expérimentation typographique
  14. Sujet : expérimentation typographique

Cours et sujets 2012-2013

  1. Design génératif et expériences interactives — introduction
  2. Processing 2.0 — langages, grammaire et variables
  3. Grilles et répétitions de motif — itérations et conditions
  4. Aléatoire Brownien et Perlin
  5. Tableaux — introduction aux classes
  6. Programmation orientée objet : les classes
  7. Analyse audio et réactivité au son
  8. Temps : frames, horaires et décomptes
  9. Sujet : Lightpainting procedural
  10. Sujet : Installation interactive

Expérimentations typographiques interactives

L’option Digital Lab a pour objectif de sensibiliser les étudiants à la conception graphique générative et interactive par l’étude de la forme depuis une approche géométrique et mathématique ainsi que par l’étude de la programmation par l’intermédiaire de Processing. Durant ces cours les étudiants sont amenés à appréhender les méthodes itératives, la création de tableaux et d’objets, la notion de comportement, la gestion d’un espace 2D/3D, les différents type d’aléatoire…

Après plusieurs cours portant sur la création d’objets aux comportements propres — programmation orientée objet — l’aléatoire et la gestion de chaîne de caractère, il leur a été demandé à chacun de réaliser une expérimentation typographique interactive.

Le Brief & les contraintes :

« Réaliser une expérimentation typographique interactive à partir d’une citation qui vous sera donnée. »

À travers cet exercice les étudiants se sont interrogés sur le sens des citations choisies, sur la composition graphique et typographique au sein d’un espace génératif ainsi que sur le comportement dont peut disposer un caractère typographique.

Temps de production : 4 cours

Étudiants :
Cindy Texeira
Savinien Fruhauf
Cyrus Bruns
Vincent Roure
Alice Zhu
Laurine Munsch
Justine Gaudin
Nadège Calegari
Anis Chouk
Romy Kombet
Stefania Giraldo-Garcia

Expérimentations Typographiques

Les expérimentations graphiques et typographiques sont depuis longtemps un des terrains de jeux préférés des graphistes. Entre respect et transgression des règles typographiques et de lectures, de nombreux graphistes tels que Roger Excoffon, Paul Rand, Milton Glaser ou encore Stefan Sagmeister, se sont prêtés à ces exercices. Il en est de même dans le domaine du design interactif et on compte de nombreuses expérimentations typographiques comme Alaphabet interactif de Murielle Lefèvre, Frédéric Durieu et Jean-Jacques Birgé en 1999 ou encore LetterScape en 2002 de Peter Cho, étudiant de John Maeda. À travers ce cours nous allons apprendre à utiliser et mettre un forme un texte à l’aide de processing mais aussi à manipuler les variables de types textes afin de les utiliser par la suite.

Variables textes et méthodes text();

Processing différencie deux types de variables pouvant contenir du texte, la variable de type String et la variable de type char

Comme son nom l’indique la variable de type String est une suite (chaîne) de caractères, elle permet de stocker des mots. Elle se déclare de la manière suivante :

String monTexte = "Ceci est mon premier texte";

La variable de type char, permet de ne stocker qu’un seul caractère. Elle se déclare de la manière suivante :

char maLettre = 'a';

Il est important de noter que les suites de caractères sont encadrés par des doubles guillemets «  » là ou le caractère simple n’est encadré que par des simples guillemets  »

Lorsque nous souhaitons afficher un texte dans notre programme nous utilisons la méthode text(), celle-ci nous permettra d’afficher un texte à un endroit précis de notre programme. Cette méthode peut s’utiliser de différentes manières

//ici monTexte peut être une variable de type String, char, int ou float
text(monTexte, x, y);
text(monTexte, x, y, z);
text(monTexte, x1, y1, x2, y2); // où x1 et y1 sont les coordonnées de position du texte et x2, y2 la largeur et hauteur de la "boite" contenant notre texte.

Enfin la méthode textSize() nous permet de modifier le corps du texte. Comme toutes méthodes de mise en forme, celle-ci doit être placée avant la méthode d’affichage du texte.

textSize(23);
text("hello world", 20, 20);

Nous verrons un peu plus bas les méthodes de mise en forme de texte avancées.

String monTexte = "Hello World!";

void setup()
{
  size(700, 250, P2D);
  smooth();
}

void draw()
{
  background(255);
  
  fill(0);
  textSize(23);
  text(monTexte, 20, height/2);
}

Manipulations des variables String et char

Lors d’une expérimentation typographique il est souvent utile de pouvoir manipuler le texte.
Nous verrons, dans cette partie, différentes méthodes nous permettant de manipuler nos variables String et char

charAt()

La méthode charAt() permet de connaître un caractère à une position donnée dans une chaîne de caractères.

String monTexte = "Hello World!";
char caractere = monTexte.charAt(4);

Ici la variable caractere sera égale à o car il s’agit du 4ème caractère de la variable monTexte

NB : il est important de noter que notre programme commence son calcul à 0. Il compte donc de la sorte :

  • charAt(0) = H
  • charAt(1) = e
  • charAt(2) = l
  • charAt(3) = l
  • charAt(4) = o
  • charAt(5) = (espace)
  • charAt(6) = W

 

substring()

substring() permet d’extraire une chaîne de caractères au sein d’une autre. Nous obtenons ainsi une partie ou un extrait de cette chaîne de caractères.

String monTexte = "Hello World, welwome on board";
String subst  = monTexte .substring(6, 11);

Ici la variable subst sera égale à World qui correspond à la chaîne de caractères comprise entre le caractère 6 et 11 de la variable monTexte.

length()

Comme pour les tableaux statiques, length() permet de connaitre une taille. Appliquée à une variable de type String, cette méthode nous permet de connaître la taille, en caractère, d’une chaîne de caractères.

String monTexte = "Hello World, welcome on board";
int len = monTexte.length();

Ici la variable len sera égale à 29 car la variable monTexte possède 29 caractères.

indexOf()

La méthode indexOf() permet de connaitre la position d’un caractère dans une chaîne de caractères. Elle correspond à l’inverse de la méthode charAt()

String monTexte = "Hello World, welcome on board";
int indexMonTexte = monTexte.indexOf("b");

Ici la variable indexMonTexte sera égale à 24 car ‘b’ est en 24ème position de la variable monTexte.

toLowerCase() & toUpperCase()

Ces deux méthodes, plus apparentées à des méthodes de mise en forme, permettent de réécrire une chaîne de caractère en capitale ou bas de casse.

String monTexte = "Hello World, Welcome on Board";
String   bdc = monTexte.toLowerCase();
String   CAP = monTexte.toUpperCase();

Ici la variable bdc sera égale à « hello world, welcome on board » et CAP à « HELLO WORLD, WELCOME EN BOARD », soit, respectivement, la variable mon monTexte en bas de casse et capitale.

str()

La méthode str() permet de convertir des variables de type int ouf float (nombre entier ou décimal) en chaîne de caractères.

int number = 2014;
String numberToString = str(number);

Ici la variable numberToString sera égale à la chaîne de caractères « 2014 ».

char() & int()

Les méthodes char() & int() permettent respectivement de convertir un caractère en sa valeur numérique et vice versa (notamment utilisé en ASCII).

char caractere = char(65);
int numberChar = int('E');

Ici la variable caractere sera égale à ‘A’ qui est la lettre correspondant à la valeur ’65’ et la variable numberChar sera égale à ’69’ qui est la valeur correspondante à ‘E’.

NB : Vous pouvez retrouver les correspondances ASCII ici : Tables de correspondance ASCII

join(), split(), splitTokens()

La méthode join() permet d’assembler les éléments d’un tableau de String en une seule chaîne de caractères. Elle s’emploie de la manière suivante : join(nomDuTableau, « séparateur »);. Séparateur correspond au caractère ou chaîne de caractères que nous souhaitons utiliser pour séparer nos mots.

String[] etudiants = {"prénom1", "prénom2", "prénom3", "prénom4", "prénom5"};
String ensemble = "les étudiants sont : "+join(etudiants, ", ");

Ici la variable ensemble sera égale à « les étudiants sont : prénom1, prénom2, prénom3, prénom4, prénom5 » où « ,  » sera le séparateur de chacun de nos mots.

La méthode split() effectue l’inverse de la méthode join(). Elle permet de convertir en tableau de type String une chaîne de caractères. Elle s’emploie de la manière suivante : split(variableString, « séparateur »);. Séparateur correspond au caractère ou chaîne de caractères que nous souhaitons utiliser pour séparer nos mots dans notre variable String.

String ensemble = "prénom1, prénom2, prénom3, prénom4, prénom5");
String[] prenom = split(ensemble, ", ");

Ici notre tableau prénom contiendra alors les éléments suivant : prénom1, prénom2, prénom3, prénom4, prénom5.

splitTokens effectue la même action que la méthode split() à cela près qu’elle permet d’utiliser divers séparateurs.

String monTexte = "Hello World! Welcome on Board.";
String[] phrase = splitTokens(monTexte, "!.");

Ici notre tableau phrase contiendra les éléments : « Hello World » et « Welcome on Board » soit notre chaîne de caractères monTexte séparée à chaque ponctuation ‘!’ et ‘.’.

toCharArray()

La méthode toCharArray() permet de convertir en tableau de type char une chaîne de caractères. Elle permet ainsi d’isoler l’ensemble des caractères d’une phrase ou d’un mot.

String phrase = "Hello World!";
char[] car = phrase.toCharArray();

Ici la tableau car contiendra les éléments suivant : H, e, l, l, o, ,W, o, r, l, d, !.

Ces méthodes constituent les principales méthodes nécessaire à des expérimentations typographique. Il existe évidement encore d’autres méthodes permettant de traiter des variables textes dont vous pourrez retrouver les références sur le site processing

Mise en forme (avancée) de texte

Utiliser une typographie

Comme pour beaucoup de programmes, si nous souhaitons utiliser une typographie spécifique il nous faudra la charger dans notre programme. Processing utilisant une format spécifique pour ses fichiers typographiques, il nous faudra dans un premier temps créer puis charger notre typographie. Cela va se faire très facilement à l’aide de l’outil Create Font disponible via le menu Tools de notre fenêtre processing. Cet outil nous permettra, via la pop-up suivante, de choisir puis créer notre typographie au corps souhaité.

createFont

Une fois notre typographie créée nous pouvons la retrouver dans le dossier /data présent à la racine de notre sketch. Celui portera le nom que nous lui aurons donné précédemment (filename).

Afin de pouvoir utiliser notre typographie dans notre programme il est nécessaire de la charger dans une variable de type PFont. PFont pour processing font est un variable de type typographie. Elle permet de stocker une typographie chargée. Elle s’utilise de la manière suivante :

PFont myFont;

void setup()
{
  size(200, 100, P2D);
  
  myFont = loadFont("nomDeMaTypoCrée.vlw");
}

Nous pouvons alors utiliser notre typographie au sein de notre méthode draw() afin de mettre en forme notre texte. Pour cela nous utiliserons la méthode suivante textFont(nomDeMaVariableFont);

textFont(myFont);
text(monTexte, 20, height/2);

Nous remarquons que notre texte est automatiquement mise en forme au corps auquel nous avons créer notre font (Size dans l’outil de création font). Il nous est cependant possible de changer celle-ci à l’aide de la méthode textSize();

textAlign()

Cette méthode nous permet de mettre en forme l’alignement d’un texte.

  • CENTER pour un alignement centré
  • LEFT pour un alignement à gauche
  • RIGHT pour un alignement à droite

Elle s’utilise de la manière suivante :

textAlign(CENTER); //CENTER, LEFT ou RIGHT

Il est également possible de gérer l’alignement vertical d’un texte. Pour se faire nous utiliserons toujours la méthode textAlign() auquel nous ajouterons un argument. Nous aurons donc textAlign(alignementHorizontal, alignementVertical). Les alignements verticaux possible étant:

  • TOP pour un alignement haut
  • BOTTOM pour un alignement bas
  • CENTER pour un alignement centré
  • BASELINE pour un alignement sur la ligne de base

Nous l’utilisons donc la manière suivante :

textAlign(CENTER, TOP);
/* 
alignement horizontaux :
CENTER, LEFT, RIGHT

alignement verticaux :
TOP, BOTTOM, CENTER, BASELINE
*/

textLeading()

La méthode textLeading() permet de manipuler, en pixel, l’interlignage d’un texte. Elle s’utilise de la manière suivante :

String monTexte = "Hello World!\nHello You.\nHello Me";

textLeading(50);
text(monTexte, width/2, height/2);

NB : Il est possible dans une variable String d’indiquer un retour à la ligne. Pour se faire il suffit d’inclure la chaîne de caractères suivante « \n » avant chaque retour à la ligne dans notre variable String. Dans l’exemple ci-dessus nous avons donc écrit : Hello World!\nHello You.\nHello Me afin d’indiquer un retour à la ligne avant chaque nouvelle phrase.

textWidth()

textWidth() permet de connaitre, en pixel, la largeur d’un texte affiché par notre programme.

float tWidth = textWidth(monTexte);

textAscent() & textDescent()

La méthode textAscent() permet de connaitre la hauteur de jambage supérieur (hampe ou longue ascendante) de la typographie utilisée dans notre programme. Cette hauteur est calculée depuis la ligne de pied de notre typographie.
Elle s’utilise de manière suivante :

float aText = textAscent();

La méthode textDescent() permet de connaitre la hauteur de jambage inférieur (hampe ou longue descendante) de la typographie utilisée dans notre programme. Cette hauteur est calculée depuis la ligne de pied de notre typographie.
Elle s’utilise de manière suivante :

float aText = textDescent();

En combinant ces deux méthode nous pouvons alors calculer la hauteur d’une ligne en fonction de la typographie utilisée.

float totalHeight = textAscent()+textDescent();

Part_05. Exemple d’expérimentation typographique

experimentation_0764

Dans l’exemple suivant nous allons voir comment exploiter les méthodes précédentes afin de réaliser une expérimentation typographique interactive.
Pour cet exemple nous allons utiliser une citation de Charles Eames : « Design is a plan for arranging elements in such a way as best to accomplish a particular purpose. »

Nous souhaitons disposer l’ensemble des caractères formant cette citation sur une grille 3D. Celle-ci effectuera une rotation sur différents axes.
Enfin nous allons voir comment mettre en exergue certaines lettres de cette citation lorsque l’utilisateur appuie sur la touche de son clavier correspondant. Celles-ci avanceront sur l’axe z puis changeront de couleurs.

Nous allons commencer par créer un classe Lettre. Celle-ci permettra de stocker et gérer nos objets caractères. Nous voulons positionner nos lettres dans un espace x, y, z et que nous souhaitons en changer la couleur. Nous aurons donc besoins des variables suivantes :

  • La variable char correspondant à la lettre de la citation
  • La position x,y,z (float)
  • La couleur HSB de la lettre (float)
  • Une variable boolean afin de savoir si notre lettre et appuyée ou non

Cela nous donnera le début de classe suivante :

class Lettre
{
  //variable
  float x;
  float y;
  float z;

  char lettre;

  float hue;
  float sat;
  float bright;
  
  boolean clicked;
//constructeur

//methode
}

Nous aurons par ailleurs besoins de diverses méthodes dont :

  • Le constructeur de notre objet
  • Une méthode permettant de définir la lettre et la couleur de notre objet defineLetter(char lettre_)
  • Une méthode permettant de changer le comportement de la lettre si celle-ci est appuyée changeBehavior()
  • Une méthode permettant d’afficher notre lettre display()
  • Une méthode globale permettant d’appeler les méthodes nécessaires à notre programme run()

Ajoutons notre première méthode, à savoir, notre constructeur. Celui-ci va nous permettre de définir la position de notre objet. Nous définirons la lettre puis la couleur dans une seconde méthode afin de s’éviter un calcul de correspondance entre une grille et un suite de caractères.

class Lettre
{
  //variable
  float x;
  float y;
  float z;

  char lettre;

  float hue;
  float sat;
  float bright;
  
  boolean clicked;

  //constructeur
  Lettre(float x_, float y_, float z_)
  {
    x = x_;
    y = y_;
    z = z_;
  }
//methodes
}

Nous allons ensuite ajouter notre méthode permettant de définir la lettre et la couleur de notre objet. Nous n’allons pas choisir la couleur de nos lettre au hasard mais allons mapper leur valeurs en ASCII sur des valeurs de bleu du cercle chromatique. Pour ce faire nous allons utiliser la méthode int() vu précédemment puis nous mapperons les valeurs à l’aide d’une méthode map(). Nous définirons le caractère de notre objet en découpant la citation, donc en dehors de l’objet en lui même, nous devrons donc passer la variable caractère à notre méthode. Pour ce faire nous allons utiliser la syntaxe suivante fonction(typeDeVariable nomDeVariable)

 
void defineLetter(char lettre_)
{
    lettre = lettre_;

    float corresspondanceASCII = int(lettre);
    hue = map(corresspondanceASCII, 65, 122, 130, 230);
    sat = 0;
    bright = 50;
}

Nous ajoutons ensuite nos méthodes changeBehavior() et display(). La première nous permettra de changer la position z et la saturation de notre objet si sa lettre correspondante est appuyée par l’utilisateur. Nous utilisons une simple condition ainsi que notre variable booléenne.

void changeBehavior()
{
    if(clicked == true)
    {
      
      z = 100;
      sat = 100;
      bright = 100;
    }
    else
    {
      z = 0;
      sat = 0;
      bright = 50;
    }
}

Notre méthode display() dessinera notre lettre au centre de sa position. Nous dessinerons également une tige à notre lettre afin d’accentuer graphiquement notre mise en valeur des lettre appuyées.

 void display()
{
    noStroke();
    fill(hue, sat, bright);
    textAlign(CENTER, CENTER);
    textFont(type);
    text(lettre, x, y, z);
    
    stroke(0, 0, 80);
    line(x, y, 0, x, y, z);
}

Enfin nous regroupons nos deux méthodes changeBehavior() et display dans une méthode run()

void run()
  {
    display();
    changeBehavior();
  }

Nous obtenons alors la classe suivante

class Lettre
{
  //variable
  float x;
  float y;
  float z;

  char lettre;

  float hue;
  float sat;
  float bright;
  
  boolean clicked;

  //constructeur
  Lettre(float x_, float y_, float z_)
  {
    x = x_;
    y = y_;
    z = z_;
  }

  //methodes
  void run()
  {
    display();
    changeBehavior();
  }

  void display()
  {
    noStroke();
    fill(hue, sat, bright);
    textAlign(CENTER, CENTER);
    textFont(type);
    text(lettre, x, y, z);
    
    stroke(0, 0, 80);
    line(x, y, 0, x, y, z);
  }

  void defineLetter(char lettre_)
  {
    lettre = lettre_;

    float corresspondanceASCII = int(lettre);
    hue = map(corresspondanceASCII, 65, 122, 130, 230);
    sat = 0;
    bright = 50;
  }
  
  void changeBehavior()
  {
    if(clicked == true)
    {
      
      z = 100;
      sat = 100;
      bright = 100;
    }
    else
    {
      z = 0;
      sat = 0;
      bright = 50;
    }
  }
}

Nous allons à présent nous occuper de découper notre citation puis l’afficher sur scène. Si nous analysons le programme voulu et son design, nous remarquons que nous aurons besoins de

  • Découper notre citation en tableau de caractères
  • Positionner nos caractères sur un grille d’une certaines taille, avec un espace précis entre chacun d’eux
  • Positionner notre grille sur la scène
  • Effectuer une rotation de notre scène

Nous aurons besoins des variables suivantes :

  • Taille de la scène
  • Citation et Auteur
  • Tableau pour stocker l’ensemble des caractères formant la citation
  • Tableau pour stocker l’ensemble de nos objets Lettre
  • La typographie pour notre mise en forme du texte
  • La taille de notre grille
  • les marge haute et basse pour positionner notre grille
  • la résolution en largeur et hauteur de notre grille
  • Les angles x, y, z de notre scène

Soit :

//définition de la taille de la scène
int sWidth = 800;
int sHeight = sWidth;

//Variable String de notre citation et tableau pour stocker nos caractères
String citation = "Design is a plan for arranging elements in such a way as best to accomplish a particular purpose.";
String auteur = "Charles Eames";
char[] caracteres;

//Objet caractères et typographie de mise en forme
ArrayList<Lettre> lettres;
PFont type;

//Variable de la grille et position
float boxWidth;
float boxHeight;
float margeBoxHeight;
float margeBoxWidth;
float resX;
float resY;

//scene
float incAngleX, incAngleY, incAngleZ;

Nous pouvons à présent définir notre setup(). C’est dans ce dernier que nous effectuerons l’analyse de notre texte, la création des objets Lettre et leur position.
Nous commençons par les paramètres de base de notre programme ainsi que le chargement de la typographie créée au préalable :

  size(sWidth, sHeight, P3D);
  smooth(8);
  colorMode(HSB, 360, 100, 100, 100);
  
  type = loadFont("Dekar-20.vlw");

Puis, à l’aide de la méthode toCharArray() nous allons créer un tableau contenant tout les caractères de la variable citation.

 caracteres = citation.toCharArray();

Nous définissons ensuite les paramètres de notre grille, puis à l’aide d’une double boucle for, nous créons et positionnons nos objets de type Lettre.

boxWidth = sWidth/2;
  boxHeight = sHeight/2;
  resX = 40;
  resY = 40;
  margeBoxWidth = boxWidth/2;
  margeBoxHeight = boxHeight/2;

  lettres = new ArrayList<Lettre>();  

  for (int i=0; i<boxHeight/resY; i++)
  {
    for (int j=0; j<boxWidth/resX; j++)
    {
      float margeHorizontale = -width/2+margeBoxWidth+resX/2;
      float margeVerticale = -height/2+margeBoxHeight+resY/2;
      float y = margeVerticale+i*resX;
      float x = margeHorizontale+j*resY;
      float z = 0;

      lettres.add(new Lettre(x, y, z));
    }
  }

Enfin nous n’avons plus qu’à définir les lettres de chacun de nos objets. Si nous observons notre grille, nous remarquons qu’il s’agit d’une grille de 100 emplacements. Or notre citation fait 97 caractères. Nous devrons donc, à l’aide d’une condition, définir un caractère vide ‘ ‘ pour tout nous objets n’ayant pas de caractères correspondant (de l’emplacement 98 à 100).

 for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    char lettre = ' ';
    if (i<caracteres.length)
    {
      lettre = caracteres[i];
    }

    le.defineLetter(lettre);
  }

Nous obtenons donc le setup() suivant

void setup()
{
  size(sWidth, sHeight, P3D);
  smooth(8);
  colorMode(HSB, 360, 100, 100, 100);
  
  type = loadFont("Dekar-20.vlw"); 

  caracteres = citation.toCharArray();

  boxWidth = sWidth/2;
  boxHeight = sHeight/2;
  resX = 40;
  resY = 40;
  margeBoxWidth = boxWidth/2;
  margeBoxHeight = boxHeight/2;

  lettres = new ArrayList<Lettre>();  

  for (int i=0; i<boxHeight/resY; i++)
  {
    for (int j=0; j<boxWidth/resX; j++)
    {
      float margeHorizontale = -width/2+margeBoxWidth+resX/2;
      float margeVerticale = -height/2+margeBoxHeight+resY/2;
      float y = margeVerticale+i*resX;
      float x = margeHorizontale+j*resY;
      float z = 0;

      lettres.add(new Lettre(x, y, z));
    }
  }

  for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    char lettre = ' ';
    if (i<caracteres.length)
    {
      lettre = caracteres[i];
    }

    le.defineLetter(lettre);
  }
  
}

Nous allons désormais définir notre draw(). Nous souhaitons pouvoir effectuer une rotation de notre grille. Cela implique donc l’utilisation d’une déplacement de matrice et d’une rotation sur des axes x, y et z. Pour afficher nos objets nous utilisons un simple boucle for() afin d’appeler leur méthodes run().

void draw()
{
  background(0, 0, 100);
  pushMatrix();
  translate(width/2, height/2);
  rotateX(incAngleX);
  rotateY(incAngleY);
  rotateZ(incAngleZ);  

  for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    le.run();
  }
  popMatrix();
  
   incAngleX += 0.0005;
  incAngleY += 0.0003;
  incAngleZ += 0.0002;
  
  fill(0, 0, 50);
  textAlign(RIGHT, BOTTOM);
  text(auteur, width-10, height-10);
  
}

Une fois notre affichage créé, il ne nous reste plus qu’à réaliser la partie interactive de notre expérimentation. Nous allons utiliser une méthode keyPressed() dans laquelle nous comparons la touche du clavier appuyée par l’utilisateur avec le lettres de nos objets. Si celles-ci correspondent, nous passerons alors les variables booléenne clicked de nos objets en vrai, sinon en faux.

void keyPressed()
{
  for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    if(key == le.lettre)
    {
      le.clicked = true;
    }
    else
    {
      le.clicked = false;
    }
  }
}

Nous obtenons alors l’expérimentation suivante pour le code suivant :

int sWidth = 800;
int sHeight = sWidth;

String citation = "Design is a plan for arranging elements in such a way as best to accomplish a particular purpose.";
String auteur = "Charles Eames";
char[] caracteres;
PFont type;

ArrayList<Lettre> lettres;
float boxWidth;
float boxHeight;
float margeBoxHeight;
float margeBoxWidth;
float resX;
float resY;

//scene
float incAngleX, incAngleY, incAngleZ;

void setup()
{
  size(sWidth, sHeight, P3D);
  smooth(8);
  colorMode(HSB, 360, 100, 100, 100);
  
  type = loadFont("Dekar-20.vlw"); 

  caracteres = citation.toCharArray();

  boxWidth = sWidth/2;
  boxHeight = sHeight/2;
  resX = 40;
  resY = 40;
  margeBoxWidth = boxWidth/2;
  margeBoxHeight = boxHeight/2;

  lettres = new ArrayList<Lettre>();  

  for (int i=0; i<boxHeight/resY; i++)
  {
    for (int j=0; j<boxWidth/resX; j++)
    {
      float margeHorizontale = -width/2+margeBoxWidth+resX/2;
      float margeVerticale = -height/2+margeBoxHeight+resY/2;
      float y = margeVerticale+i*resX;
      float x = margeHorizontale+j*resY;
      float z = 0;

      lettres.add(new Lettre(x, y, z));
    }
  }

  for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    char lettre = ' ';
    if (i<caracteres.length)
    {
      lettre = caracteres[i];
    }

    le.defineLetter(lettre);
  }
  
}

void draw()
{
  background(0, 0, 100);
  pushMatrix();
  translate(width/2, height/2);
  rotateX(incAngleX);
  rotateY(incAngleY);
  rotateZ(incAngleZ);  

  for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    le.run();
  }
  popMatrix();
  
   incAngleX += 0.0005;
  incAngleY += 0.0003;
  incAngleZ += 0.0002;
  
  fill(0, 0, 50);
  textAlign(RIGHT, BOTTOM);
  text(auteur, width-10, height-10);
  
}


void keyPressed()
{
  for (int i=0; i<lettres.size(); i++)
  {
    Lettre le = lettres.get(i);

    if(key == le.lettre)
    {
      le.clicked = true;
    }
    else
    {
      le.clicked = false;
    }
  }
}

class Lettre
{
  //variable
  float x;
  float y;
  float z;

  char lettre;

  float hue;
  float sat;
  float bright;
  
  boolean clicked;

  //constructeur
  Lettre(float x_, float y_, float z_)
  {
    x = x_;
    y = y_;
    z = z_;
  }

  //methodes
  void run()
  {
    display();
    changeBehavior();
  }

  void display()
  {
    noStroke();
    fill(hue, sat, bright);
    textAlign(CENTER, CENTER);
    textFont(type);
    text(lettre, x, y, z);
    
    stroke(0, 0, 80);
    line(x, y, 0, x, y, z);
  }

  void defineLetter(char lettre_)
  {
    lettre = lettre_;

    float corresspondanceASCII = int(lettre);
    hue = map(corresspondanceASCII, 65, 122, 130, 230);
    sat = 0;
    bright = 50;
  }
  
  void changeBehavior()
  {
    if(clicked == true)
    {
      
      z = 100;
      sat = 100;
      bright = 100;
    }
    else
    {
      z = 0;
      sat = 0;
      bright = 50;
    }
  }
}

Espace 3D et formes primitive – [initiation]

L’espace tridimensionnelle cartésien est un espace assez simple à aborder. Il possède l’ensemble des propriétés de l’espace cartésien bidimensionnel à cela prêt que l’ajout d’une dimension offre une nouvelle perspective : la profondeur.

Cet espace est habituellement représenté par les axe X, Y, Z suivant :

espace3D-01

Afin de comprendre cet espace il est important de connaitre le sens des différents axes.
Nous savons que par convention, l’espace bidimensionnel de notre programme est inversé, notre point d’origine 0,0 étant en haut à gauche, les x s’incrémentant de gauche à droite et les y s’incrémentant de haut en bas. De la cas de l’axe Z, celui-ci va en se décrémentant. Ainsi un objet loin de notre point de vue aura une position z inférieur de celle d’un objet proche de notre point de vu.
Nous obtenons alors le schéma suivant :

espace3D-02

Enfin il est important de noter que la position z de notre « oeil » ou point de vue sera de 0 par défaut. Celui-ci regardant dans le sens inverse nous observons alors que notre axe z visible sera un axe inférieur à 0 et se décrémentant. Il sera donc négatif.

L’espace tridimensionnel rajoute donc un concept de point de vu qu’il sera important de maîtriser afin de déplacer son point de vue dans son espace.

Transformations de matrice afin de déplacer son point de vue

Afin de maîtriser son point de vue et donc la position dans un espace nous allons utiliser une méthode vue précédemment pour changer un système de coordonnées, les transformations de matrice.
Nous savons qu’il nous est possible de déplacer notre point d’origine où l’on souhaite dans notre programme et ce sur un axe XY. Il en est de même pour un espace 3D. Dans l’exemple suivant nous allons dessiner un rectangle de taille 20, positionner en 0,0 puis nous verrons comment en changer son point de vue à l’aide des transformations de matrices.

Lorsque nous dessinons notre rectangle en position 0,0,0 nous obtenons l’image suivante.

void setup()
{
  size(700, 350, P3D);
  smooth();
}

void draw()
{
  background(255);
  rectMode(CENTER);
  fill(127);
  stroke(0);
  rect(0,0, 100, 100);
}

Notre rectangle, de position 0,0,0 n’est visible que de moitié. Si nous souhaitons le placer au centre de notre programme tout en conservant sa position nous utiliserons une transformation de matrice. Il est est de même si nous souhaitons changer sa position sur l’axe Z. Ainsi nous pouvons obtenir les transformations suivante :

pushMatrix();
translate(x, y, z);
popMatrix();

Dans notre cas nous allons positionner notre rectangle en z -100 et au centre de notre programme.

void setup()
{
  size(700, 350, P3D);
  smooth();
}

void draw()
{
  background(255);
  rectMode(CENTER);
  fill(127);
  stroke(0);
  
  pushMatrix();
  translate(width/2, height/2, -100);
  rect(0,0, 100, 100);
  popMatrix();
}

Rotations de matrice

Nous savons, dans un espace bidimensionnel, qu’il nous est possible d’effectuer un rotation de notre objet par le biais d’une rotation de matrice. Cette transformation nous est toujours possible dans un espace 3D en devenant plus précise puisque nous pouvons désormais effectuer des rotations sur un axe choisi.

espace3D-03

Pour ce faire nous allons utiliser les méthodes suivantes

rotateX(); //angle en radians
rotateY(); //angle en radians
rotateZ(); //angle en radians

Si nous reprenons notre exemple ci-dessus et que nous appliquons à chacun des axes X,Y,Z un angle s’incrémentant respectivement de 0.01, 0.02, 0.03 radians, nous obtenons alors de notre objet rectangle, une rotation différente sur chacun de ses axes.

float angleX, angleY, angleZ;

void setup()
{
  size(700, 350, P3D);
  smooth();
}

void draw()
{
  background(255);
  rectMode(CENTER);
  fill(127);
  stroke(0);
  
  pushMatrix();
  translate(width/2, height/2, -100);
  rotateX(angleX);
  rotateY(angleY);
  rotateZ(angleZ);
  
  rect(0,0, 100, 100);
  popMatrix();
  
  angleX += 0.01;
  angleY += 0.02;
  angleZ += 0.03;
}

Formes primitives

Comme pour l’espace 2D, processing possède des formes primitives en 3D, celle-ci nous permettent d’obtenir rapidement une forme 3D sans avoir à dessiner l’ensemble de ses sommets.
Les deux principales formes primitives sont la box et la sphère.

//Box de type Cube
box(taille) // ou la taille sera une valeur identique pour la largeur, hauteur et profondeur
//ou box de type parallélépipédique
box(largeur, hauteur, profondeur)

//Sphere
sphereDetail(res); // résolution de la sphere, c'est à dire le nombre de sous division en logitude et latitude. Par defaut cette résolution et des 30
sphereDetail(resLogitude, resLatitude); //résolution par longitude ou latitude
sphere(rayon);

Nous remarquons qu’aucune de ses formes primitives ne possèdent de position x,y,z, ces dernières devant être définies par des transformations de matrice.
Certaines formes primitives 2D sont elle aussi disponible dans un espace 3D par le simple ajout d’une position z.

line(x1, y1, z1, x2, y2, z2);
point(x, y, z);
vertex(x, y, z);
text(string, x, y, z);

Enfin, comme nous l’avons vu précédemment, il nous est toujours possible d’utiliser des formes primitives 2D (rect, quad, triangle…) dans un espace 3D lorsque nous utilisons des transformations de matrice.

Transformations 2D

Lorsque nous dessinons sur nos écrans, nous travaillons sur une grille, une matrice. Dans le cas d’une réalisation de maquette pour un site web, nous travaillons, par exemple, sur des .psd de 1280*1024 soit une grille de pixels de 1280*1024, c’est notre matrice. Nous plaçons ensuite nos éléments sur cette grille. Il en est de même avec processing où notre grille commence au point d’origine 0, 0 pour finir au point width, height.
Nous allons voir comment manipuler cette matrice.

matrice-01-01

Déplacements de matrice

Par défaut le point d’origine (0, 0) de notre matrice se trouve au coin supérieur gauche de notre programme mais il peut être facilement déplaçable.

Déplacer une matrice signifie que l’on décale son point d’origine à un autre endroit. Cela nous donne un autre point de vue et change notre système de géométrie.
Il peut être pratique de déplacer son point d’origine au centre du programme, notamment lorsque nous souhaitons réaliser un programme en symétrie. Ainsi dans le cas d’un rectangle de position 10, 10 dans une symétrie axiale, dont le point d’origine sera le centre de notre programme, le second rectangle aura une position de -10, 10 soit l’inverse de la position x du premier rectangle.

matrice-02-02

Afin d’effectuer un déplacement de matrice dans processing, nous utilisons la méthode suivante :

translate(x, y);

Cela aura pour effet de déplacer la matrice de x pixels en latéral et y pixels en vertical. Le point d’origine 0,0 sera alors en x, y.
Il est important de savoir que cette méthode est cumulative, c’est à dire que si nous effectuons, tout au long de notre code, les méthodes suivante :

translate(10, 10);

//code

translate(10, 10);

//code

translate(10, 10)

//code

Cela aura pour effet de déplacer une première fois notre matrice en 10,10 puis en 20, 20 puis en 30, 30. Nous devrons donc effectuer un déplacement inverse si nous souhaitons repositionner la matrice à son premier point d’origine.

matrice-03-03

Sauvegarde du point d’origine avant transformation

Afin d’éviter de nombreuses transformations retour durant notre code, il est possible d’isoler notre déplacement de matrice à l’aide des méthodes suivante :

pushMatrix();

//transformation de matrice
translate(10, 10);

popMatrix();

pushMatrix() permet à un instant T d’enregistrer les coordonnées du point d’origine afin de pouvoir les restituer à l’aide de la commande popMatrix()

À l’exécution de ce code, la matrice se déplacera en position x, y lors de la méthode translate(), puis reviendra à son point d’origine lors de la méthode popMatrix(). Cela nous permet donc de changer notre géométrie à un instant T et de dessiner l’ensemble de nos formes voulues dans ce nouveau système de géométrie encadrée par les méthodes pushMatrix(); popMatrix();

matrice-04-04

Rotations de matrice

La rotation d’un élément est un cas particulier. Pour bien comprendre son fonctionnement revenons au point de vue microscopique de notre espace de travail, le pixel.
Nous souhaitons dessiner un rectangle de position 10, 10 et de taille 10, 10.

rect(10, 10, 10, 10);

puis nous souhaitons effectuer un rotation de 45° de ce même rectangle. Le pixel est un élément dessiné, 0 ou 1, plein ou vide, il ne peut être rempli de manière partielle.
Lorsque nous effectuons ce genre de manipulation dans photoshop, notre logiciel dessine notre diagonale sous forme de pixels pleins ou vides.

matrice-05-05

Cette transformation utilisée par photoshop n’est pas la plus simple à réaliser et processing utilise un autre méthode.
Au lieu de redessiner les pixels de notre rectangle, nous allons effectuer une rotation de notre espace de coordonnées à l’aide la méthode suivante :

rotate(angle);

Cette méthode permet donc faire faire une rotation à notre matrice et ce depuis son point d’origine.

matrice-06-06

Pour obtenir notre rectangle à une véritable position 10, 10 et ayant effectué une rotation à 45° nous devront donc déplacer la matrice un premier temps, puis effectuer la rotation.

matrice-07-07

Cela se traduira en code par :

pushMatrix()

translate(10, 10);
rotate(degrees(45));

fill(0);
noStroke();
rectMode(CENTER);

rect(0,0, 10, 10);

popMatrix();

Exemple

Dans l’exemple suivant, nous allons réaliser un grille de 20*20 pixels contenant des rectangles de 18 pixels. Nous souhaitons que ces rectangles effectuent des rotations lorsque la souris passera en roll over dessus.

Pour cela nous allons partir d’une classe de rectangle relativement simple possédant les variables suivante :

  • position x
  • position y
  • largeur
  • hauteur
  • couleur aléatoire
  • angle
  • vitesse de l’angle
class Rect
{
  //variable
  float x, y, w, h;
  color c;

  float angle, vAngle;

  //constructeur
  Rect(float x_, float y_, float w_, float h_)
  {
    x = x_;
    y = y_;
    w = w_;
    h = h_;
    c = color(int(random(100, 200)), int(random(50, 100)), int(random(50, 100)));

    vAngle = 0.4;
  }
  //méthode
  void run()
  {
  }
}

Nous allons ajouter une méthode display() qui affichera nos rectangles et sera appelée par la méthode run().
C’est dans cette méthode que nous effectuerons nos transformations de matrice, c’est à dire untranslate() à la position du rectangle ainsi qu’un rotate() de la valeur de l’angle.

void run()
  {
    display();
  }

void display()
  {
    pushMatrix();
    translate(x, y);
    rotate(angle);

    fill(c);
    noStroke();
    rectMode(CENTER);
    rect(0, 0, w, h);

    popMatrix();
  }

Puis nous réalisons une méthode updateAngle() dans laquelle nous effectuerons une rotation de l’angle et qui sera appelée si notre souris est en roll over sur notre rectangle

 void updateAngle(boolean rollOver)
  {
    if (rollOver)
    {
      angle += vAngle;
    }
    else
    {
    }
  }

Nous pouvons maintenant revenir dans notre feuille principale afin de réaliser notre grille. Pour cela nous allons utiliser un ArrayList<>() pour stocker nos objets et une double boucle for pour réaliser notre grille.

int sWidth = 700;
int sHeight = sWidth/2;

ArrayList<Rect> rectList;
float res;

void setup()
{
  size(sWidth, sHeight, P2D);
  smooth(8);
  colorMode(HSB, 360, 100, 100, 100);
  
  rectList = new ArrayList<Rect>();
  res = 20;
  for(int i=0; i<width/res; i++)
  {
    for(int j=0; j<height/res; j++)
    {
      float taille = res-2;
      float marge = taille/2;
      rectList.add(new Rect(marge+i*res, marge+j*res, taille, taille));
    }
  }
}

Dans notre draw() nous réalisons ensuite une boucle for sur l’ensemble de notre tableau d’objets afin de les afficher. Puis nous comparons la position de notre souris avec chacun des rectangles. Si un rectangle se trouve sous la souris, celui-ci effectuera une rotation à l’aide de sa méthode updateAngle()

void draw()
{
  background(0, 0, 100);
  
  for(int i=0; i<rectList.size(); i++)
  {
    Rect rect = rectList.get(i);
    
    rect.run();
    
   if(mouseX >= rect.x-rect.w/2 && mouseX <= rect.x+rect.w/2 && mouseY >= rect.y-rect.h/2 && mouseY <= rect.y+rect.h/2)
   {
     rect.updateAngle(true);
   }
  }
}

Nous obtenons alors le résultat escompté.

int sWidth = 700;
int sHeight = sWidth/2;

ArrayList<Rect> rectList;
float res;

void setup()
{
  size(sWidth, sHeight, P2D);
  smooth(8);
  colorMode(HSB, 360, 100, 100, 100);
  
  rectList = new ArrayList<Rect>();
  res = 20;
  for(int i=0; i<width/res; i++)
  {
    for(int j=0; j<height/res; j++)
    {
      float taille = res-2;
      float marge = taille/2;
      rectList.add(new Rect(marge+i*res, marge+j*res, taille, taille));
    }
  }
}

void draw()
{
  background(0, 0, 100);
  println(mouseX);
  for(int i=0; i<rectList.size(); i++)
  {
    Rect rect = rectList.get(i);
    
    rect.run();
    
   if(mouseX >= rect.x-rect.w/2 && mouseX <= rect.x+rect.w/2 && mouseY >= rect.y-rect.h/2 && mouseY <= rect.y+rect.h/2)
   {
     rect.updateAngle(true);
   }
  }
}

class Rect
{
  //variable
  float x, y, w, h;
  color c;

  float angle, vAngle;

  //constructeur
  Rect(float x_, float y_, float w_, float h_)
  {
    x = x_;
    y = y_;
    w = w_;
    h = h_;
    c = color(int(random(100, 200)), int(random(50, 100)), int(random(50, 100)));

    vAngle = 0.4;
  }
  //méthode
  void run()
  {
    display();
  }

  void updateAngle(boolean rollOver)
  {
    if (rollOver)
    {
      angle += vAngle;
    }
    else
    {
    }
  }

  void display()
  {
    pushMatrix();
    translate(x, y);
    rotate(angle);

    fill(c);
    noStroke();
    rectMode(CENTER);
    rect(0, 0, w, h);

    popMatrix();
  }
}

Analyse audio et l’utilisation de données FFT

L’animation d’images synchronisées sur une musique est un procédé reconnu et utilisé depuis longtemps tant dans le VJing que dans les divers domaines de l’animation. De nombreux outils comme after effect nous permettent de synchroniser, de manière automatique ou plus maîtrisée, une animation sur un son, il en est de même avec les outils de creative coding.
Processing permet, par l’utilisation de la librairie Minim, de jouer et analyser un son. Les valeurs récupérées peuvent ensuite être utilisées de manière différente comme dans l’exemple ci-dessous où l’analyse audio permet de gérer la déformation d’un mesh 3D ou le comportement d’un flocon de neige.

Nous verrons ici comment utiliser la librairie Minim et créer un objet nous permettant de récupérer l’amplitude des bandes FFT (issues de la transformation de Fourier) afin d’animer des objets.

Analyse audio et les FFT

Lorsque nous écoutons un son nous entendons les diverses notes jouées. Cependant lorsque nous analysons un son, ce dernier n’est pas représenté en terme de notes mais en amplitudes/temps.
D’une manière simple, l’amplitude à un instant T d’un son correspond à son volume à ce même instant T. Si nous souhaitons représenter un son nous obtenons alors le graphique suivant :

son-amplitude-01-01

Nous pouvons facilement utiliser l’amplitude d’un son comme valeur pour une animation. Dans l’exemple suivant nous avons mappé la valeur y d’une ellipse sur l’amplitude de notre musique.
Pour ce faire, nous avons utilisé after effect afin de convertir les données audio en keyFrame afin de pouvoir lire cette amplitude. Nous avons ensuite mappé la valeur y de notre objet à l’aide de l’expression suivante.

X = effect("Both Channels")("Slider");
value+[0, -X*20]

Nous obtenons alors l’animation suivante :

Dans le cas d’une animation simple cela pourrait nous suffir, mais dans le cadre d’un clip génératif par exemple nous souhaitons obtenir plus de variations et pouvoir différencier des notes, des graves ou des aïgues. Pour ce faire nous allons utiliser la transformée de Fourier. La transformée de Fourier est une fonction inventée par Jean Baptiste Joseph Fourier permettant de transformer un spectre en fréquences. Dans notre cas nous allons nous en servir pour transposer l’amplitude de notre son en fréquences Hertz.

Un son est composé d’un ensemble de fréquences Hertz, chacun de ces hertz aura une amplitude différente. Si nous traçons cela nous obtenons le graphique suivant (pour l’instant T, l’amplitude des fréquence changeant à chaque instant)

son-hertz-01-01

Cependant on pourrait se demander en quoi cela nous intéresse de transformer l’amplitude d’un son en fréquence.
Il faut savoir que chaque son possède une fréquence majeure, il s’agit de la fréquence dont l’amplitude sera la plus haute. Chaque son étant différent il est alors possible d’isoler pour chacun d’entre un leur fréquence majeure, cela devient leur empreinte. On peut donc facilement reconnaître une note par sa fréquence majeure, ainsi la fréquence d’un La (octave 2) sera de 220hz.
Evidemment dans le cadre d’une musique, il faut prendre en compte le fait que plusieurs instruments joueront en même temps, il sera donc très difficile d’extraire une note exacte par sa fréquence puisque nous obtenons l’amplitude des fréquences pour l’ensemble des instruments enregistrés.

Un autre élément intéressant des fréquences est leur ordre croissant. Les fréquences audible par l’homme vont en moyenne de 3 hz à 22 000 hz, et il est intéressant de savoir que les sons associés aux fréquences les plus basses sont les sons graves contre les sons aux fréquences hautes qui seront les plus aiguës. Les fréquences peuvent donc nous aider à obtenir des variations tant sur les graves, les médiums que les aiguës. Ils nous offrent alors beaucoup de possibilités en terme d’animation.

Nous avons vu, dans les grandes lignes, ce que nous pouvons analyser d’un son. Passons à la pratique en essayant d’extraire les données de notre musique à l’aide de processing.

L’utilisation d’une librairie intégrée dans processing

Processing ne gère pas de manière « native » la lecture/analyse d’un son. Pour se faire nous allons utiliser une librairie intégrée : Minim. Il s’agit d’une librairie dédiée au traitement de son et permettant à la fois la gestion, lecture, écriture, analyse d’un son. Cette librairie fait depuis longtemps partie du package processing étant donné ses grandes capacités. Minim étant une librairie nous aurons besoins de l’importer dans notre skecth afin de pouvoir accéder à ses méthodes. Pour se faire nous utilisons le code suivant :

import ddf.minim.analysis.*;
import ddf.minim.*;

Création d’une classe FFTObject – variables et constructeur

Afin de faciliter l’analyse des fréquences tout au long de notre code nous allons créer un objet que nous appellerons fftObject. Cet objet nous permettra à la fois de charger et jouer un son mais aussi d’accéder à l’analyse des bandes FFT de ce même son.

Nous aurons besoins de divers objets liés à la librairie Minim, à savoir :

  • Un objet minim de type Minim permettant de gérer l’ensemble des objets de chargement, lecture et analyse d’un son
  • Un objet music de type AudioPlayer afin de lire notre musique
  • Un objet fft de type FFT afin de réaliser et utiliser la transformée de fourier

Nous aurons ensuite besoins de la variable suivant :

  • Une variable timeSize définissant la taille du buffer de notre son à analyser

Nous pouvons donc commencer à créer notre classe fftObject de la manière suivante :

class FFTObject
{
  //variables
  Minim minim;
  AudioPlayer music;
  int timeSize;

  FFT fft;

  //constructeur
  FFTObject(PApplet parent)
  {
  }
  //methodes
}

Passons maintenant au constructeur de notre objet.
Nous allons définir notre variable timeSize à 1024. Il s’agit de la taille du buffer dédié au son. Le buffer est une mémoire tampon, il permet de stocker à l’avance les données du son devant être lues. Ce buffer nous servira lors de l’initialisation de l’objet music.

Nous devons ensuite initialiser notre objet minim de la manière suivante :

minim = new Minim(parent);

Nous remarquons l’ajout d’une variable parent, celle-ci correspond au contexte. En effet lors de son initialisation l’objet minim a besoins de connaitre le contexte dans lequel il va être appelé. Dans notre cas il s’agit de notre application processing. Si nous avions initialisé notre objet dans le setup, nous aurions utilisé la variable this permettant de cibler notre application comme étant le contexte.
Puisque nous somme dans un objet, la variable this ne cible plus l’application comme contexte mais l’objet en lui même. Il nous faudra alors la passer dans notre objet lors de son initialisation. Pour ce faire nous avons ajouté au constructeur une variable parent de type PApplet.

Nous pouvons ensuite initialisé l’objet music. Ce dernier aura besoins de l’url de notre son à jouer ainsi que de la taille du buffer que l’application devra lui allouer.

music = minim.loadFile("casseNoisetteTchaikovski.mp3", timeSize);

L’objet fft aura aussi besoins de la taille du buffer ainsi que la fréquence à laquelle la musique a été échantillonné. Cette dernière valeur nous est donnée par l’objet music à l’aide de la méthodes suivante :

music.sampleRate()

Nous obtenons donc pour l’objet fft

fft = new FFT(timeSize, music.sampleRate() );

Enfin, une fois l’ensemble de nos objets construit nous lançons la lecture de notre son à l’aide de la méthode suivante

music.play();

Nous obtenons alors, pour notre classe fftObject le constructeur suivant :

class FFTObject
{
  //variables
  Minim minim;
  AudioPlayer music;
  int timeSize;

  FFT fft;

  //constructeur
  FFTObject(PApplet parent)
  {
    
    timeSize = 1024;
    minim = new Minim(parent);
    music = minim.loadFile("casseNoisetteTchaikovski.mp3", timeSize);

    fft = new FFT(timeSize, music.sampleRate() );

    music.play();
  }
//methodes
}

Création d’une classe FFTObject – les méthodes

Une fois notre constructeur défini nous avons besoins de créer les diverses méthodes dont notre objet aura besoins. Dans notre cas nous avons besoins des méthodes suivantes :

  • Une méthode getFFTLevel nous renvoyant l’amplitude d’une fréquence souhaitée
  • Une méthode displayFFT permettant de dessiner les amplitudes de chacune de nos fréquences sous forme d’égaliseur

Commençons par la méthode getFFTLevel, celle-ci sera relativement simple.
Nous allons utiliser une méthode de type float. Les méthodes typées (float, int, boolean…) sont des méthodes renvoyant des variables une fois exécutée.

Si on écrit :

float tarteAuCitron()
{
  float valeur = 0.5;
  return valeur;
}

float x = tarteAuCitron();

Alors x sera égale à valeur
Dans notre cas nous souhaitons pouvoir connaitre l’amplitude d’une bande i et ceux à n’importe quel endroit de notre code. Nous allons donc créer une méthode nous permettant par la suite de dire :

float x = monObjetFFT.getFFTLevel(i);

où x sera égale à la valeur de l’amplitude de la fréquence i.

Cette méthode getFFTLevel se composera alors de la manière suivante :

float getFFTLevel(int index)
{
    fft.forward( music.mix ); //ici nous mixons les donnée des deux chaine d'écoute droite et gauche
    float fftLevel = fft.getBand(index);

    return fftLevel;
}

Nous allons désormais définir la méthode displayFFT. Celle-ci nous permettra d’afficher sous forme d’égaliseur les amplitudes de nos fréquences.
Nous aurons besoins de passer diverses variables dans cette méthode, à savoir :

  • la position x de notre égaliseur
  • la position y de notre égaliseur
  • la hauteur maximum heightBande de nos fréquences
  • le nombre nbBandes de fréquences voulues (de nombreuses fréquences n’étant pas couvertes ou non présentes dans notre échantillons nous n’aurons pas besoins de dessiner les 22000 hertz)

enfin à l’aide d’une boucle for nous dessinerons l’ensemble de nos fréquences. Cela nous donnera :

void displayFFT(float x, float y, float heightBande, float nbBande)
{
   fft.forward( music.mix );
   int w = int( width/nbBande);
   for (int i = 0; i < nbBande; i++)
   {
     fill(100);
     noStroke();
     float newX = w;//i*(w*2);
  
     rect(x+i*newX, y, w, -fft.getBand(i)*heightBande );
     text(i, x+i*newX, y+20);
   }
}

Utilisation de notre class FFTObject

Maintenant que nous avons créé notre classe FFTObject il ne nous reste plus qu’à l’utiliser. Dans notre feuille principale nous allons déclarer un objet fftObj de type FFTObject ainsi qu’une variable parent de type PApplet.

Nous initialisons l’ensemble de nos variables et objets dans notre setup

PApplet parent;
FFTObject fftObj;

int sWidth = 1280;//round(1280/2.5);//round(sWidth/2.5);
int  sHeight= 400;//round(sWidth*2);//round(1920/2.5);//

void setup()
{
  parent = this;
  size(sWidth, sHeight, P2D);
  smooth();
  
  fftObj = new FFTObject(parent);
}

Nous pouvons désormais dessiner notre égaliseur dans notre boucle draw() à l’aide de la méthode suivante

fftObj.displayFFT(20, height-30, 5, 50); //où l'égaliseur commence à la position 20, height-30, à une taille maximale de 5 et un nombre de bandes de 50

De la même manière nous pouvons dessiner un lot de 20 ellipses dont la taille pourra varier en fonction de bandes FFT comprises en 0 et 20.

for(int i=0; i<20; i++)
{
    float x = 10+i*20;
    float y = height/2;
    float taille = 1+fftObj.getFFTLevel(i)*2;
    fill(255);
    noStroke();
    ellipse(x, y, taille, taille);
}

Ils ne vous reste plus qu’à expérimenter à votre tour à créer votre propre application réactive au son.

taille

Bibliographie

Ce cours a été réalisé à l’aide des informations suivantes :

Amplitude et Décibels, Topos générals préliminaires sur l’audio numérique.

Transformation de Fourier

Définition des Hertz

Notes de musique et correspondance en fréquences hertz

échantillonage d’un son

Minim, références

Code : stratégies de débogguage

Parfois, lorsqu’un sketch tourne, c’est utile d’afficher des informations complémentaires afin de montrer si des parties de notre code tourne comme prévu. Le cas c’est produit récemment avec un étudiant de 4e année qui faisait du tracking avec un Kinect, la visualisation de son sketch ne donnait pas le résultat escompté lors des interactions. Mais où se trouvait le problème ?

//	pseudo code
void draw() {
	//	obtenir la forme de la main
	hand = kinect.getHand();
	//	detecte collision entre la main et mes objets
	for( i = 0; i < myObjects.length; i++ ) {
		if( rectInRect( hand.rect, myObjects[i].rect ) {
			//	gérer les collisions
			//	faites quelque chose…
		}
	}
}

Pour les besoins de ce code, on va supposer que kinet.getHand() invoque une librairie externe qui récupère les informations formattées venant du Kinect 60 fois par seconde. Que la classe kinect peut détecter une main et retourner la forme détecter, ainsi que le rectangle qui enferme cette forme de main. Et que la fonction rectInRect() retourne true s’il y a recouvrement entre les deux rectangles, et false sinon. Mais en fin de compte, ces details ne sont pas importantes.

Si les collisions ne semblent pas fonctionner, nous avons besoin, tout d’abord de vérifier que les informations arrivent de la part du Kinect.

//	pseudo code
void draw() {
	//	obtenir la forme de la main
	hand = kinect.getHand();

	//	dessine rectangle de la main
	pushStyle(); //		préserve l’état du dessin
	fill( #ff0000, 0.5 );	// rouge à 50% opacity
	rect( hand.rect.x,hand.rect.y,hand.rect.w, hand.rect.h );
	popStyle(); //		récupère l’état antérieur de dessin

	//	detecte collision entre la main et mes objets
	for( i = 0; i < myObjects.length; i++ ) {
		if( rectInRect( hand.rect, myObjects[i].rect ) {
			//	gérer les collisions
			//	faites quelque chose…
		}
	}
}

Dans ce cas, on devrait voir un restangle rouge translucide à l’écran et qui devrait suivre la position de notre main. Si nous ne voyons pas ceci, soit le Kinect, soit la librairie ne fonctionne pas prévu, et nous rechercher ailleurs.

Pendant que nous travaillons sur notre sketch, nous allons rajouter et enlever ce bout de code à plusieurs reprises, probablement en utilisant des commentaires pour le rendre inactif.

Or, il existe une manière plus simple.

//	drapeau débogguage global
final boolean debug = true;

//	pseudo code
void draw() {
	//	obtenir la forme de la main
	hand = kinect.getHand();

if( debug ) {
	//	dessine rectangle de la main
	pushStyle(); //		préserve l’état du dessin
	fill( #ff0000, 0.5 );	// rouge à 50% opacity
	rect( hand.rect.x,hand.rect.y,hand.rect.w, hand.rect.h );
	popStyle(); //		récupère l’état antérieur de dessin
}
	//	detecte collision entre la main et mes objets
	for( i = 0; i < myObjects.length; i++ ) {
		if( rectInRect( hand.rect, myObjects[i].rect ) {
			//	gérer les collisions
			//	faites quelque chose…
		}
	}
}

Il y a plusieurs choses à voir dans ce code. Le premier est l’emploi d’un constant debug. Différents langages de programmation gèrent les constants de la même manière, mais le principe reste pareil — une fois que ce genre particulier de variable a été défini, il ne peut plus être changé par la suite, le variable reste constant… Ceci possède au moins deux avantages. Primo, on ne peut changer sa valeur par accident ailleurs dans notre code, ce qui peut se produre quand le code devient plus complexe. Deuxio, il y a généralement un petit avantage de vitesse, car lors de l’exécution du programme [ou sketch] l’environnement n’a pas besoin de vérifier si le variable a changé de valeur.

Dans Processing, le mot clé final permet d’indiquer que ceci est la valeur finale, et donc ne peut pas changer. Tout ce qu’on peut demander d’un constant.

Lors de l’exécution de notre code, on utilise une condition — si debug est vrai, alors exécute le code suivant, sinon on le saute. Ceci permet de garder des bouts de code de débogguage en place dans notre code, puis de les activer/désactiver en changeant la valeur de notre constant — soit true, soit false.

À noter aussi, comme ceci est du code temporaire, je n’applique pas l’indentation habituelle du code, je reviens sur la marge gauche. Ceci me permet, quand je regarde le code dans sa version finale, de savoir ce qui relève du code fonctionnel — ça suit l’indentation habituelle — ou du code temporaire, typiquement du débogguage — l’indentation revient sur la marge gauche. Et je peux l’éliminer sans souci du code final.

Allant plus loin avec des drapeaux…

J’ai commencé la programmation il y a très longtemps, où les ordinateurs avaient beaucoup moins de mémoire — mon premier avait 1K — 1024 octets! Mon préféré, un Apple //c, avait 128K soit 128 x 1024 octets. Autant dire rien. Non seulement je codais en Assembleur — une réprésentation symbolique des instructions du microprocesseur — mais je devais apprendre des astuces pour gagner de la place en mémoire. En, comme en C, nous devons gérer et, plus important, libérer l’espace mémoire manuellement.

Une des techniques est d’utiliser des drapeaux — aujourd’hui on dirait un boolean — un simple variable qui peut avoir deux états, et dans le fil du programme, comme pour la gestion du debug ci-haut, on sélectionne tel ou tel chemin dans le code. Mais chaque drapeau devait normalement prendre un octet en mémoire. Ce qui est très gaspilleur quand on pense qu’un octet peut encoder 8 bits, et que, fondamentalement, un boolean n’est qu’un bit. Ainsi, la logique nous disait que sur un octet on pouvait encoder 8 bits, donc 8 drapeaux.

Si on regarde un octet en mémoire, on peut le répresenter comme un tiroir à 8 cases. Chaque case peut contenir soit zéro, soit un. SI c’est un, le drapeau est mis.

Mais comment les mettre en place, et comment les lire par la suite ?

La réponse se passe par ce qu’on appèle des opérateurs bitwise, c’est-à-dire qu’au lieu de regarder la logique boolean [vrai ou faux], ces opérateurs regardent l’état des bits…

Exemple :

int un = 1;
int deux = 2;
int trois = 3;

if(( un & deux ) == un ) {
  println( "correspondence" );
} else {
  println( "pas de correspondance" );
}

Résultat : "pas de correspondance"

Mais si nous changeons la comparaison…

int un = 1;
int deux = 2;
int trois = 3;

if(( un & trois ) == un ) {
  println( "correspondence" );
} else {
  println( "pas de correspondance" );
}

Résultat : "correspondance"

La valeur 1 se lit en bits comme 00000001

La valeur 2 se lit en bits comme 00000010

La valeur 3 se lit en bits comme 00000011

Ainsi on peut voir que dans la réprésentation binaire de 1 et 2 il n’y a pas de bit à ’1’ qui soit placé en même endroit. D’où le résultat "pas de corresdonce". Mais pour la réprésentation binaire de 1 et de 3 l’emplacement le plus à droite est égale à 1 pour les deux. On peut donc affirmer que
un & trois égale un
dans la logique des bits.

Et ce que nous venons de voir ici l’emploi d’un drapeau…

Et comment ça nous aide ?

Supposons que nous travaillons sur un sketch avec trois composants principaux — audio, une librairie externe et le rendu du kinect sur l’écran — et que, par moments, nous devons activer ou désactiver le code de débogguage correspondant à chaque.

final int debug_son = 1; 
final int debug_ecran = 2;
final int debug_kinect = 4;
final int debug = 0;	//	debug desactivé

//	premier cas…
if(( debug & debug_son ) == debug_son ) {
	//	ici code de débogguage son
}

//	deuxième cas…
if(( debug & debug_ecran ) == debug_ecran ) {
	//	ici code de débogguage écran
}

//	troisième cas…
if(( debug & debug_kinect ) == debug_kinect ) {
	//	ici code de débogguage kinect
}

ensuite, il suffit de décider à chaque fois ce que avons besoin de débogguer

final int debug = debug_ecran;

ou

final int debug = debug_son;

ou

final int debug = debug_kinect;

et, si on veut afficher le code de débogguage de plus d’une partie à la fois, on peut cumuler les drapeau au moment de l’initialisation de notre constant en utilisant l’opérateur OR.

final int debug = debug_ecran | debug_son;

dans ce cas, les codes de débogguage pour debug_ecran et debug_son seront activés.

Et ensuite ?

Ce qui pourrait être intéressant serait de lire le clavier pendant le déroulement d’un sketch, et selon la touche enfoncée — S pour son, K pour Kinect, et E pour écran — d’activer ou désactiver les parties correspondants du code.

Allez-y, proposer du code pour le faire et poster-le ici..

Code : l’itération des ensembles d’envergure

C’est un problème qu’on rencontre régulièrement — nous avons un grand nombre d’éléments [particules, points, traits…] qui doivent être traités dans une boucle, par exemple, dans la fonction draw() de Processing. Je vais utiliser Processing comme exemple, mais les mêmes principes s’appliquent à PHP, C, JavaScript, ActionScript…

Par défaut, Processing cherche à rafraîchir l’écran de votre ordinateur 60 fois par seconde. S’il y a trop de traitements à effectuer dans votre boucle principale vous allez faire face à soit des ralentissements, soit, carrément, à un plantage de votre sketch.

Explorons des stratégies pour fluidifier le traitement.

Dans Processing notre boucle de traitement ressemblerait à ceci. [MyObjects est simplement un élément d’exemple — tableau, objet… – qui contient les éléments à itérer.]

void draw() {

	for( int i = 0; i

Tandis que Processing peut facilement itérer à travers un tableau de 10000 éléments, les problèmes commencent quand, par exemple, vous voulez traiter des collisions avec un autre tableau [array] important.

Supposons un vol de 8000 oiseaux et un essaim de 8000 mouches. Les oiseaux devant attraper et manger les mouches. Dans ce cas, la boucle doit traiter 64 millions de collisions possibles (8000 × 8000) à chaque fois. Cette simple multiplication arithmétique est pourquoi, dans ce cas, votre sketch aura des problèmes.

Vient donc l’idée de ne pas traiter chaque cas lors de chaque itération, de procéder par lots. Dans la meilleure des cas, les humains réagissent en environ 1/30e de seconde à ce qui se produit autour d’eux. En traitant nos éléments par lot, et en rafraichissant l'écran 60 fois par seconde, mais, par exemple, ne traitant que 10  % de notre ensemble lors de chaque boucle, nous pouvons continuer de donner l’illusion d’un mouvement continu. Bien sûr, la taille des lots et le temps de traitement sera dépendant de chaque cas spécifique, mais les exemples ici sont destinés à vous mettre sur des pistes de travail intéressantes.

Alors, comment créer des lots ?

La première stratégie consiste à traiter les éléments aléatoirement.

for( int i = 0; i

Dans ce cas, nous ne traitons que la moitié des possibilités pour chaque itération. D'autres valeurs peuvent changer la taille du lot…

for( int i = 0; i

La loi des grands nombres impliquent que, considéré sur une période longue, chaque élément sera tout de même traité de manière égale. Toutefois, sur des périodes plus courtes, les éléments de notre tableau seront traités selon une courbe en cloche, qui se lissera plus le temps passe. Ceci implique, pendant les périodes courtes, qu'environ 10 % des éléments sera moins traités que les autres. Toutefois ceci peut être un effet intéressant, par exemple, si vous cherchez à simuler les effets d'essaimage ou de murmuration — oiseaux, insectes, poissons… — où des ‘retards’ au sein du groupe, des retardataires qui suivent le groupe de manière plus paresseusement, peut de fait augmenter la vraisemblance de la simulation.

Une manière plus traditionnelle sera de construire des lots, puis de les traiter en séquence.
Isolons, un échantillon d’un huitième de l’ensemble…

for( int i = 0; i

Mais dans ce cas, les sept autres lots sont ignorés. Comment les traiter en séquence ?

// ce variable a besoin d'être global
int offset = 0;

for( int i = offset ; i

Et voilà. Il y a d'autres techniques que nous allons explorer dans l’avenir. Mais ces informations devraient vous aider à démarrer.

Le processus génératif, retour sur les classes et le temps

Il est parfois utile de revoir ses bases et quoi de mieux qu’une fin d’année pour cela.
Durant cet exercice nous verrons comment réaliser un grand nombres de variations d’une même idée à l’aide du processus génératif.

Cet exercice nous permettra également de revoir les bases suivantes :

  • Création et utilisation des classes
  • Gestion du temps via un timer
  • Utilisation de coordonnées polaires

Définition du sketch 0

Notre skecth 0 sera la base de nos nombreuses variations. Il se traduit par un idée simple autour de laquelle nous pourrons facilement travailler.

Pour cela nous allons nous inspirer du travail de Joshua Nimoy et GMunk réalisé pour le film Tron Legacy et plus précisément l’arrière plan de la scène des « jeux ». Durant cette scène nous pouvons observer des feux d’artifices en arrière plan, ceux-ci présentent toutes les similitudes d’un feu d’artifice normal (étincelles, explosions, physique…) mais ont aussi la particularité d’être géométrique et ceux afin d’accentuer le fait que l’ensemble se passeau cœur d’un programme informatique.
Pour ce faire Joshua Nimoy a travaillé de nombreuses formes tel que les solides de planton comme l’icosaèdre ou le dodécaèdre.

Sur cette même idée nous allons réaliser un « feu d’artifice » géométrique. Notre skecth présentera un élément au comportement simple. Ce dernier effectuera en un temps déterminé un trajet. Lorsque son temps sera écoulé, ce dernier se stoppera et « explosera », laissant apparaître de nouveaux éléments ayant le même comportement et répartis sur un cercle.

expo

Pour réaliser cela nous aurons besoins d’un objet « Point » ayant un comportement défini et un certains nombre de variables.
Nous souhaitons que notre objet effectue un trajet vertical durant un temps donné avant que ce dernier ne s’arrête et « explose » en créant de nouveaux objets identiques à lui même. Enfin, afin que ces nouveaux points soit placés sur un cercle de centre « point explosé » nous aurons besoins de travailler en coordonnées polaires (cf cours trigonométrie).
Notre objet peut se décomposer de la manière suivante :

  • Variables
    • Positions x, y
    • Angle « theta »
    • Rayon « radius »
    • Vitesse du Rayon « vRadius » (cette variable nous permettra de déplacer notre Objet)
    • Valeur booléenne « vie » (Tant que le temps de vie de l’objet sera supérieur à 0 alors son état vie sera vrai)
    • Valeur booléenne « exploded » (Tant que notre objet n’aura pas explosé et engendré de nouveaux objets, alors son état exploded sera faux)
    • Taille « taille » de notre dessin
    • Valeur de margeX et margeY afin de positionner notre dessin sur la scène
    • Un objet Timer permettant de décompter la vie de l’objet
  • Méthodes
    • Methode « run » contrôlant l’ensemble des méthodes de l’objet
    • Methode « UpdateRadius » faisant parcourir ou non un chemin à l’objet en fonction de son état de vie
    • Méthode « displayPoint » affichant une ellipse à la position de l’objet
    • Méthode « displayLine » affichant une ligne depuis l’objet et son origine
  • Constructeur
    • Définitions du rayon, de l’angle et des marges
    • Initialisation des variables

Une fois l’ensemble de éléments composant notre objet défini, nous pouvons concevoir sa classe.
NB : Ici nous utilisons un timer afin de calculer le temps de vie de notre objet. Il ne faudra donc pas oublier d’inclure ce dernier dans notre sketch. Celui-ci est disponible ici

class Point
{
  //variables
  float x;
  float y;
  float theta;
  float radius;
  float vRadius;
  boolean vie;
  boolean exploded;
  float taille;
  float margeX;
  float margeY;
  Timer monTimer;


  //constructeur
  Point(float r_, float a_, float margeX_, float margeY_)
  {
    radius = r_;
    theta = a_;
    margeX = margeX_;
    margeY = margeY_;
    
    x = cos(theta)*radius+margeX;
    y = sin(theta)*radius+margeY;

    vRadius = 3;
    taille = 5;
    vie = true;
    exploded = false;


    monTimer = new Timer(500);
    monTimer.start();
  }
}

Nous allons désormais définir nos méthodes en commençant par la méthode permettant à notre objet de se mouvoir.
Notre objet possède un temps de vie durant lequel nous souhaitons qu’il se déplace. Lorsque ce temps de vie est épuisé notre objet devra s’arrêter. Pour cela nous allons créer une méthode « updateRadius » interrogeant le timer de notre objet. Il en résultera deux états :

  • Le timer est fini : Alors notre état de vie passe en faux
  • Le timer n’est pas fini : Alors nous incrémentons notre rayons de sa vitesse et mettons à jour les coordonnées de notre objet
void updateRadius()
  {
    if (monTimer.isFinished() == true)
    {
      vie = false;
    }
    else
    {
      radius += vRadius;
      x = cos(theta)*radius+margeX;
      y = sin(theta)*radius+margeY;
    }
  }

Nous réaliserons ensuite deux méthodes permettant de dessiner notre objet ainsi que son parcours

void displayPoint()
  {
    noStroke();
    fill(0);
    ellipse(x, y, taille, taille);
  }
  
  void displayLine()
  {
    noFill();
    stroke(127);
    line(x, y, margeX, margeY);
  }

Enfin nous assemblons l’ensemble des ces méthodes dans une méthode contrôleur

void run()
  {
    updateRadius();
    displayPoint();
    displayLine();
  }

Nous venons alors de créer notre classe Point

class Point
{
  //variables
  float x;
  float y;
  float theta;
  float radius;
  float vRadius;
  boolean vie;
  boolean exploded;
  float taille;
  float margeX;
  float margeY;
  Timer monTimer;


  //constructeur
  Point(float r_, float a_, float margeX_, float margeY_)
  {
    radius = r_;
    theta = a_;
    margeX = margeX_;
    margeY = margeY_;
    
    x = cos(theta)*radius+margeX;
    y = sin(theta)*radius+margeY;

    vRadius = 3;
    taille = 5;
    vie = true;
    exploded = false;


    monTimer = new Timer(500);
    monTimer.start();
  }

  //methodes
  void run()
  {
    updateRadius();
    displayPoint();
    displayLine();
  }

  void updateRadius()
  {
    if (monTimer.isFinished() == true)
    {
      vie = false;
    }
    else
    {
      radius += vRadius;
      x = cos(theta)*radius+margeX;
      y = sin(theta)*radius+margeY;
    }
  }

  void displayPoint()
  {
    noStroke();
    fill(0);
    ellipse(x, y, taille, taille);
  }
  
  void displayLine()
  {
    noFill();
    stroke(127);
    line(x, y, margeX, margeY);
  }
}

Nous pouvons donc attaquer la seconde partie de notre sketch permettant d’appeler et afficher nos objets.
Nous savons que nous aurons besoins de dessiner un nombre encore non-défini d’objets Point.
Nous souhaitons aussi que nos objets explosent en créant un certains nombre de point. Nous définirons tout cela dans notre « setup ».

int sWidth = 1920;
int sHeight = sWidth/2;

ArrayList<Point> mesPoints;
int nbNewPts;


void setup()
{
  size(sWidth, sHeight, P2D);
  smooth(8);
  mesPoints = new ArrayList<Point>();
  nbNewPts = int(random(3, 10));
  
  mesPoints.add(new Point(0, (3*PI)/2, width/2, height-200));
}

Afin de pouvoir recréer un nouveau « feu d’artifice » quand bon nous semble, nous allons créer un méthode « mousePressed » permettant de vider notre tableau et de re-initialiser nos variables.

void mousePressed()
{
  mesPoints.clear();
  nbNewPts = int(random(3, 10));
  
  mesPoints.add(new Point(0, (3*PI)/2, width/2, height-200));
}

Enfin dans notre méthode « draw » nous allons appeler, créer et afficher nos objets. Pour cela nous allons décomposer notre méthode en plusieurs parties

  • Une boucle for() nous permettant de parcourir notre tableau de points et à l’intérieur de laquelle nous effectuerons plusieurs taches
    • Appeler la méthode run() contrôlant nos objets
    • Interroger l’état de vie et d’explosion de notre objet
      • Si notre objet a fini son état de vie (vie == false) et n’a pas explosé (exploded == false) : alors nous créons de nouveaux objets dont l’origine sera la position de l’objet explosant. Enfin nous passerons l’état « exploded » de l’objet explosant en vrai

Si nous effectuons un « run » de notre skecth nous remarquons que nous obtenons l’effet souhaité, cependant notre sketch se met rapidement à ralentir puis se bloque. Cela s’explique par le fait que ce dernier effectue un calcul infini que la mémoire de l’ordinateur ne pourra supporter.

expo-bug

Si nous reprenons notre concept, nous remarquons que notre tableau d’objets augmente de manière exponentielle.
En effet chaque point, une fois sa vie terminée, explose en un nombre de points défini, par exemple 4.
Ainsi à chaque explosion (ou cycle) nous obtenons le calcul suivant : 1^4^4^4^4… soit : 4^nbDeCycle ou : mesPoints.size() = nbNewPts^nbDeCycle

Afin d’éviter à notre sketch de s’étendre à l’infini nous allons donc définir un nombre maximal de cycles auxquel notre tableau de points aura droit. Pour ce faire nous allons devoir compter les cycles de notre tableau, or nous disposons de 2 valeurs dans l’équation suivante : mesPoints.size() = nbNewPts^nbDeCycle soit mesPoints.size() et nbNewPts.

Pour calculer le nombre de cycles de notre tableau nous allons faire appel à une fonction bien pratique en Maths, la fonction logarithme népérien. En effet, en Maths, nous savons que log(a^x) = N ou a^N = x. Cela se note aussi de la manière suivante :
logNeperien

Nous pourrons alors traduire cela dans processing à l’aide des méthodes round() permettant de calculer un nombre arrondi et de la méthode log() permettant de calculer une valeur logarithmique. Ainsi nous aurons :

cycle = round(log(mesPoints.size()-1) / log(nbNewPts));

Il ne nous reste donc plus qu’à reporter cela dans notre sketch et de définir un nombre maximum de cycle pour notre tableau. Enfin dans notre méthode draw nous autorisons nos objets à se créer que si le nombre de cycle actuel n’est pas supérieur au nombre de cycle maximal. Nous rajoutons donc deux nouvelle variables :
cycle et limiteCycle. Notre « draw » se composera alors de la manière suivante :

void draw()
{
  background(255);

  for (int i=0; i<mesPoints.size(); i++)
  {
    Point pts = mesPoints.get(i);
    pts.run();
   if (cycle < limiteCycle)
    {
      if (pts.vie == false && pts.exploded == false)
      {
        for (int j=0; j<nbNewPts; j++)
        {
          float angle = map(j, 0, nbNewPts, 0, TWO_PI);
          mesPoints.add(new Point(0, angle, pts.x, pts.y));
        }

        pts.exploded = true;
        println(cycle);
      }
    }
  }
  
  cycle = round(log(mesPoints.size()-1) / log(nbNewPts));

  
}

Nous venons donc de terminer notre sketch 0, base de notre travail génératif.

int sWidth = 1920;
int sHeight = sWidth/2;

ArrayList<Point> mesPoints;
float cycle;
int nbNewPts;
int limiteCycle;

void setup()
{
  size(sWidth, sHeight, P2D);
  smooth(8);
  mesPoints = new ArrayList<Point>();
  nbNewPts = int(random(3, 10));
   limiteCycle = int(random(2, 4));

  mesPoints.add(new Point(0, (3*PI)/2, width/2, height-400));
}

void draw()
{
  background(255);

  for (int i=0; i<mesPoints.size(); i++)
  {
    Point pts = mesPoints.get(i);
    pts.run();
   if (cycle < limiteCycle)
    {
      if (pts.vie == false && pts.exploded == false)
      {
        for (int j=0; j<nbNewPts; j++)
        {
          float angle = map(j, 0, nbNewPts, 0, TWO_PI);
          mesPoints.add(new Point(0, angle, pts.x, pts.y));
        }

        pts.exploded = true;
        println(cycle);
      }
    }
  }
  
  cycle = round(log(mesPoints.size()-1) / log(nbNewPts));

  
}

void mousePressed()
{
  mesPoints.clear();
  cycle = 0;
  nbNewPts = int(random(3, 10));
  limiteCycle = int(random(2, 4));
  
  mesPoints.add(new Point(0, (3*PI)/2, width/2, height-200));
}

//TIMER******************************

class Timer {

  int savedTime;
  int totalTime;
  int passedTime;
  int remainingTime;
  boolean timerStarted, timerStopped;

  Timer(int tempTotalTime) {
    totalTime = tempTotalTime;
    timerStarted = false;
    timerStopped = true;
  }

  void start() {
    if (timerStopped) {
      savedTime = millis();
      timerStarted = true;
      timerStopped = false;
    }
  }

  void stop() {
    timerStopped = true;
    timerStarted = false;
  }

  void reset() {
    savedTime = millis();
  }

  boolean isFinished() {
    if (timerStarted) {
      passedTime = millis()- savedTime;
      if (passedTime > totalTime) {
        timerStarted = false;
        timerStopped = true;
        return true;
      } 
      else {
        return false;
      }
    } 
    else {
      return true;
    }
  }

  int getRemainingTime() {
    if (timerStarted) {
      remainingTime = totalTime-passedTime;    
      return remainingTime;
    } 
    else {
      return -1;
    }
  }
}

//Objet Point***********************
class Point
{
  //variables
  float x;
  float y;
  float theta;
  float radius;
  float vRadius;
  boolean vie;
  boolean exploded;
  float taille;
  float margeX;
  float margeY;
  Timer monTimer;


  //constructeur
  Point(float r_, float a_, float margeX_, float margeY_)
  {
    radius = r_;
    theta = a_;
    margeX = margeX_;
    margeY = margeY_;
    
    x = cos(theta)*radius+margeX;
    y = sin(theta)*radius+margeY;

    vRadius = 3;
    taille = 5;
    vie = true;
    exploded = false;


    monTimer = new Timer(500);
    monTimer.start();
  }

  //methodes
  void run()
  {
    updateRadius();
    displayLine();
    displayPoint();
  }

  void updateRadius()
  {
    if (monTimer.isFinished() == true)
    {
      vie = false;
    }
    else
    {
      radius += vRadius;
      x = cos(theta)*radius+margeX;
      y = sin(theta)*radius+margeY;
    }
  }

  void displayPoint()
  {
    noStroke();
    fill(0);
    ellipse(x, y, taille, taille);
  }
  
  void displayLine()
  {
    noFill();
    stroke(127);
    line(x, y, margeX, margeY);
  }
}

ixd_processusgeneratif_00

Processus génératif

Nous venons de réaliser notre sketch 0, base de notre travail. Nous allons maintenant nous interroger sur comment réaliser des variations de cette idée. Pour cela rien de plus simple, ils nous suffit de nous interroger sur les variables, méthodes ou dessins que nous pouvons modifier afin de créer de nouveau sketch. C’est la base du processus génératif, prendre une idée et en réaliser de nombreuses variations en cherchant de nouvelles voies. Nous pouvons procéder de différentes manières :

  • En changeant des variables existantes
  • En changeant/rajoutant des comportements
  • en rajouter des objet dans la composition
  • En travaillant le design (composition, couleur, formes…)

Voici quelques pistes permettant des modifications possible afin de réaliser de nouveaux éléments

  • Afficher uniquement les objets points
  • Afficher uniquement les lignes des objets points
  • Dessiner les liaisons entre les objets point lorsque leur distance est inférieur à une valeur définie
  • Changer la forme des objets point
  • Définir une durée de vie aléatoire pour chaque objet
  • Définir une durée de vie aléatoire pour chaque cycle
  • Définir une durée de vie se décrémentant à chaque cycle
  • Définir une durée de vie se s’incrémentant à chaque cycle
  • Définir la vitesse des objet en aléatoire
  • Effectuer une rotation selon un angle défini ou en bruit perlin durant la course de l’objet
  • Positionner les nouveau objet sur un arc de cercle de 180°, 30°….
  • Dessiner en 3D
  • Ne dessiner les objet ou les ligne un cycle sur deux
  • Dessiner uniquement les objet pair et les ligne impaire
  • Incrémenter l’axe Z à chaque cycle
  • Si la durée de vie de l’objet est fini, il explose puis repart dans une autre direction aléatoire ou défini
  • Varier la taille des objets en fonction de leur distance les séparant du centre
  • … à vous de trouver la suite

Il est donc aisé, à partir d’une idée simple, de générer un grand nombre de variation de cette même idée.

L’ensemble des sources des visuels ci-dessus sont à retrouver dans le dossier DigitalLab – Revision sur gitHub

Le temps [Frames, horloges et décomptes]

Le temps est une notion essentielle dans un programme. Cette notion permet de connaitre le temps d’exécution d’un programme, à quelle vitesse ce dernier s’exécute, de découper des animations selon un certains timing mais aussi de prévoir des animations ou actions à des instants précis.

Nous verrons ici trois méthodes permettant d’appréhender le temps dans un programme : les frames, les horloges et le décompte.

Les frames

Les frames (ou images) sont directement inspiré du cinéma et de l’animation, ce sont les images affichées par secondes. En programmation une frame est une exécution complète de la boucle draw(), ainsi le nombre total de frames correspond au nombre total de fois que boucle draw() s’est effectuée. Les frames par seconde (ou FPS) permettent de connaitre la vitesse d’exécution du programme.

Processing permet d’obtenir rapidement le nombre total de frames effectuées ainsi que le nombre de frames par seconde à l’aide des méthodes suivantes :

float nbTotalDeFrame = frameCount;
float nbDeFrameParSeconde = frameRate;

Sachant que notre programme s’exécute à une vitesse moyenne de 60 frames par seconde nous pouvons donc obtenir le nombre de secondes durant lequel le programme s’est exécuté par un calcul simple :

Temps = frameCount/frameRate

Cependant cette méthode n’est pas conseillée compte tenu du fait qu’elle ne nous renverra jamais un temps réel.
En effet, au cours de son exécution le nombre de frames par seconde d’un programme peut grandement varier en fonction du nombre de calculs à effectuer. Nous pouvons d’ailleurs remarquer que ce dernier oscille entre des valeurs décimales comprises entre 58 et 60. Cette méthode n’est donc pas la plus juste lorsque nous souhaitons calculer un temps précis.

Dans l’exemple ci-dessous nous dessinons une ligne d’un bout à l’autre de la scène en 600 frames. Lorsque le programme atteint la frame 600, nous arrêtons la double draw() à l’aide de la méthode noLoop().

int maxFrame = 600; //Limit de frame
float maxWidth; //Taille Max de la ligne
void setup()
{
  size(800, 600, P2D);
}
void draw()
{
  background(0);
  if (frameCount <= maxFrame)
  {
    maxWidth = map(frameCount, 0, maxFrame, 0, width);
  }
  else
  {
    noLoop();
  }
  stroke(255);
  line(0, height/2, maxWidth, height/2);

  println(«nombre total de frame : «+frameCount);
  println(«FrameRate : «+frameRate);
}

ixd_letemps_00

Les horloges

Une seconde méthode permettant de gérer du temps durant un programme consiste à se référer à l’horloge de la machine sur laquelle se dernier s’exécute. Cela permet, quelques soit la vitesse d’exécution du programme, de toujours obtenir une valeur de temps universelle et juste.

Processing nous permet de questionner l’horloge de manière très simple à l’aide des méthodes suivantes :

hour() // renvoie des valeurs entre 0 - 23.
minute() // renvoie des valeurs entre 0 - 59.
second() // renvoie des valeurs entre 0 - 59.
millis() 
day() // renvoie des valeurs entre 1 - 31.
month() // renvoie des valeurs entre 1 - 12.
year()

Nous noterons que les méthodes hour(), minute() et second() renvoi les valeur actuelles de l’horloge alors que la méthode millis() renvoi le temps d’exécution du programme en millisecondes.

Il est donc très facile, à partir de ces méthodes, de réaliser une horloge digitale.

int sWidth = 800;
int sHeight = 400;

void setup()
{
  size(sWidth, sHeight, P2D);
}

void draw()
{
  background(0);
  textSize(30);
  
  textAlign(RIGHT);
  text(hour()+» : «+minute()+» : «+second()+» : «+millis(), width/2-20, height/2);
  
  textAlign(LEFT);
  text(day()+» / «+month()+» / «+year(), width/2+20, height/2);
}

ixd_letemps_01

Le décompte ou timer

Le décompte ou timer est une des méthodes les plus rependues permettant de gérer du temps lors de l’exécution d’un programme. Cette méthode permet de lancer un décompte en millisecondes. Pour cela nous utiliserons une class Timer. Cette classe permet de calculer une temps passé ainsi qu’un temps restant. Elle se construit de la manière suivante :

class Timer {

  int savedTime;
  int totalTime;
  int passedTime;
  int remainingTime;
  boolean timerStarted, timerStopped;

  Timer(int tempTotalTime) {
    totalTime = tempTotalTime;
    timerStarted = false;
    timerStopped = true;
  }

  void start() {
    if (timerStopped) {
      savedTime = millis();
      timerStarted = true;
      timerStopped = false;
    }
  }

  void stop() {
    timerStopped = true;
    timerStarted = false;
  }

  void reset() {
    savedTime = millis();
  }

  boolean isFinished() {
    if (timerStarted) {
      passedTime = millis()- savedTime;
      if (passedTime > totalTime) {
        timerStarted = false;
        timerStopped = true;
        return true;
      } 
      else {
        return false;
      }
    } 
    else {
      return true;
    }
  }

  int getRemainingTime() {
    if (timerStarted) {
      remainingTime = totalTime-passedTime;    
      return remainingTime;
    } 
    else {
      return -1;
    }
  }
}

Les méthodes start(), stop() et reset() permettent respectivement de lancer arrêter ou réinitialiser le timer.
Les méthodes isFinished() et getRemainingTime() permettent quant à elles de savoir si le timer est fini et quelle est son temps passé en millisecondes.

Cette classe utilise un algorythme simpme permettant de décompter le temps.
Lorsque ce dernier est lancé, le timer sauvegarde le temps d’exécution actuel du programme à l’aide de la méthode millis() dans une variable correspondant au point de départ de notre Timer.
Enfin nous effectuons le calcul suivant :

TempsPassé = TempsactuelleEnMillisecondes – TempsDeDepart

Enfin si cette valeur est supérieur au temps total que nous souhaitons décompter alors notre timer est terminé.

Ainsi, pour créer un timer à l’aide de cet objet il nous suffira alors de créer un objet de type Timer, de le déclarer avec le temps que nous souhaitons décompter puis de le lancer à l’aide de la méthode start(). Nous pourrons ensuite, lors de l’exécution de la boucle draw() interroger l’état de notre timer (en cours ou fini) ainsi que son temps restant.

int sWidth = 800;
int sHeight = 400;

Timer monTimer;
float taille;

void setup()
{
  size(sWidth, sHeight, P2D);
  monTimer = new Timer(5000);
  monTimer.start();
}

void draw()
{
  background(0);

  if (monTimer.isFinished())
  {
    monTimer.start();
  }

  taille = map(monTimer.getRemainingTime(), 0, monTimer.totalTime, 100, 300);
  
  noFill();
  stroke(255);
  ellipse(width/2, height/2, taille, taille);
  
  fill(255);
  textAlign(CENTER);
  text(monTimer.getRemainingTime(), width/2, height/2);

  println(monTimer.getRemainingTime());
}

class Timer {

  int savedTime;
  int totalTime;
  int passedTime;
  int remainingTime;
  boolean timerStarted, timerStopped;

  Timer(int tempTotalTime) {
    totalTime = tempTotalTime;
    timerStarted = false;
    timerStopped = true;
  }

  void start() {
    if (timerStopped) {
      savedTime = millis();
      timerStarted = true;
      timerStopped = false;
    }
  }

  void stop() {
    timerStopped = true;
    timerStarted = false;
  }

  void reset() {
    savedTime = millis();
  }

  boolean isFinished() {
    if (timerStarted) {
      passedTime = millis()- savedTime;
      if (passedTime > totalTime) {
        timerStarted = false;
        timerStopped = true;
        return true;
      } 
      else {
        return false;
      }
    } 
    else {
      return true;
    }
  }

  int getRemainingTime() {
    if (timerStarted) {
      remainingTime = totalTime-passedTime;    
      return remainingTime;
    } 
    else {
      return -1;
    }
  }
}

ixd_letemps_02

Un exemple d’horloge décomptant les secondes

Dans cet exemple nous allons réaliser une horloge décomptant le temps d’exécution du programme en minutes et secondes. Pour cela nous allons utiliser le concept simple du cadran de montre en secondes. À chaque minute passée nous dessinerons un nouveau cadran plus grand que le cadran précédent, chaque cadran se composant de cercles représentant les 60 secondes passées.

horloge

Pour cela nous aurons besoins de créer différentes classes : une classe cadran, que nous appellerons secondsDial, et une classe point qui permettra de dessiner un cercle par seconde sur notre cadran.

La classe point sera relativement simple et possédera peu de méthodes. Nous aurons besoins de connaitre une position x et y qui seront définies dans une méthode de la classe secondsDial. Nous définirons un taille ainsi qu’une couleur, enfin nous définirons une méthode display() qui dessinera nos points.

class Point
{
  //variables
  float x;
  float y;
  float taille;
  float rgb;
  
  Point(float x_, float y_)
  {
    x = x_;
    y = y_;
    taille = 10;
    rgb = 0;
  }
  
  void run()
  {
    display();
  }
  
  void display()
  {
    fill(127);
    noStroke();
    ellipse(x, y, taille, taille);
  }
}

Notre classe cadran sera quant à elle un peu plus difficile à mettre en place. Nous aurons besoins des variables suivantes :

  • La position x,y du cadran
  • Le rayon du cadran
  • Un ArrayList d’objet Point (nos cercles)
  • Un compteur de cycle afin de savoir si notre cadran à atteint 60 secondes
  • Un décompte des secondes
  • Une limite de secondes

Afin de réaliser une horloge décomptant le temps d’exécution du programme nous utiliserons la méthode millis(). Celle-ci nous permettra de connaitre en millisecondes le temps d’exécution du programme alors que la méthode seconde() nous renvoi la valeur seconde de notre horloge machine (cf partie 03). Les milliscondes s’incrémentant nous aurons donc besoins de fixer une limite afin de savoir quand nous aurons atteint les 60 secondes pour chaque cadran. C’est la raison pour laquelle nous avons besoins ici d’une variable secondesLimites qui sera équale au temps actuel d’exécution du programme en millisecondes + 60000 millisecondes (soit 1 minute).

NB : Nous pourrions utiliser un timer afin de réaliser ces calculs mais nous verrons ici comment les réaliser « from scratch » afin de se familliariser avec la méthode millis() et le décompte d’un temps

Notre constructeur sera alors :

class secondsDial
{
  //variables
  float posX, posY;
  float radius;
  ArrayList<Point> points;
  int countCycle;
  int secondes;
  int secondesLimite;

  //constructeur
  secondsDial(float radius_, float posX_, float posY_)
  {
    posX = posX_;
    posY = posY_;
    radius = radius_;
    countCycle = 0;
    secondes = 0;
    secondesLimite = millis()+60000;

    points = new ArrayList<Point>();
    
  }
}

Afin de décompter les secondes de notre cadran nous réaliserons une méthodes updateSecondes nous permettant de mapper la valeur millis() en secondes pour notre cadran.

void updateSeconde()
  {
    if(millis()<=secondesLimite)
    {
      secondes = int(map(millis(), secondesLimite-60000, secondesLimite, 0, 60));
    }
    else
    {
      secondes = 60;
    }
  }

Nous réaliserons ensuite une méthodes booléenne nous permettant de savoir si notre cadran a atteint sa limite de décompte (60 secondes)

 boolean cycle()
  {
    if (countCycle == 0)
    {
      if (secondes >= 60)
      {
        countCycle = 1;
        return true;
      }
      else
      {
        return false;
      }
    }
    else
    {
      return true;
    }
  }

Une utiliserons ensuite une méthode update() afin d’ajouter un objet point à notre tableau dynamique à chaque seconde passée. Afin de définir la position de nos point sur notre cadran nous utiliserons les base de trigonométrie nous permettant de définir la position d’un point sur un cercle (cf cours trigonométrie)

  void update()
  {
    if (!cycle())
    {

      float theta = map(secondes, 0, 60, 0, 360);
      float x = radius*cos(radians(theta))+posX;
      float y = radius*sin(radians(theta))+posY;

      if (points.size()-1 != secondes)
      {
        points.add(new Point(x, y));
      }
    }
  }

Enfin nous réaliserons une méthode run regroupant nos mises à jour et notre dessin.

 void run()
  {
    updateSeconde();
    update();
    display();
  }

Une fois notre objet cadran réalisé il ne nous restera plus qu’à l’appeler dans notre boucle draw(). Sachant que nous souhaitons ajouter un cadran de plus en plus grand à chaque minutes nous aurons besoins de créer une tableau dynamique d’objet secondsDials. Enfin à l’aide d’une condition nous interrogerons le dernier cadran afin de savoir si son cycle est terminé, et ce à l’aide de sa méthode booléenne cycle(). Si ce dernier est fini nous ajouterons alors un nouveau cadran dont le rayon sera défini par le rayon du cadran inférieur + 10 pixels.
Enfin il ne faudra pas oublier d’afficher nos cadrans à l’aide de leur méthodes run()

int sWidth = 700;
int sHeight = sWidth/2;

ArrayList<secondsDial> mySD;
int lastElement;

void setup()
{
  size(sWidth, sHeight, P2D);
  smooth(8);

  mySD = new ArrayList<secondsDial>();
  mySD.add(new secondsDial(50, width/2, height/2));
}

void draw()
{
  background(255);
  
 
  for (int i =0; i < mySD.size(); i++)
  {
    secondsDial sd = mySD.get(i);
    sd.run();
    
  }
  
  
  if(mySD.get(lastElement).cycle())
  {
    //println("newElements");
    float newRadius = mySD.get(lastElement).radius+10;
    //println(mySD.get(lastElement).points.size());
    mySD.add(new secondsDial(newRadius, width/2, height/2));
    lastElement = mySD.size()-1;
  }
}

ixd_letemps_03

Grilles et répétitions de motifs (Structures itératives & conditionnelles)

On appelle structure iterative une structure qui permet de répéter un certain nombre de fois une série d’instructions simples ou composées. Celle-ci permet notamment d’effectuer une grand nombre d’actions ou de calculs simultanés. On appelle structure conditonnelle une structure permettent de tester si une condition est vraie ou non. Cette structure est souvent utilisée afin d’attibuer une valeur à une variable booléenne.

Les structures itératives et conditionnelles sont deux grande bases de la programmation. Ici nous verrons comment les utiliser afin de créer une série de motifs génératifs.

Structure itérative : construction

La structure itérative permet d’executer plusieurs fois une ou des instructions. Elle peut s’écrire de plusieurs manières différentes. Ici nous nous concenterons sur la plus connue : La boucle for(). Celle-ci s’écrit de la manière suivante :

for(int i=0; i<Maximum; i++)
{
instruction
}

Où i est le compteur; i est inferieur au nombre maximum de répétitions et i s’incrémente de 1. On peut alors traduire cette phrase de la manière suivante : Pour i = 0; i étant toujours inférieur à une valeur maximum
et i s’incrémentant de 1 à chaque boucle alors…
Appliquons cela à un exemple concret. Ici je souhaite déssiner un motif composé de cercles de diamètre 30 pixels et se répétant tout les 40 pixels.

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);

  for (int i=0; i<width; i+=40)
  {

      ellipse(i+15, height/2, 30, 30);

  }
}

ixd_grille_00

On peut alors traduire cette phrase de la manière suivante : Pour i=0; i étant toujours inférieur à la largeur de mon sketch; et i incrémentant de 40 pixels (0, 40, 80…) je dessine des cercles dont x = i, y = moitié de la hauteur de mon skecth et de diamètre 30 pixels.

Dans notre premier exemple nous avons utilisé une structure itérative pour répéter notre visuel sur l’axe de x. Dans le cadre d’un motif nous aurons besoins de la répéter à la fois sur les x et les y. Pour cela nous allons utiliser une double boucle for.

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);
  
  noFill();
  stroke(0);
  for (int i=0; i<width; i+=40)
  {
    for (int j=0; j<height; j+=40)
    {
      ellipse(i+15, j+15, 30, 30);
    }
  }
}

ixd_grille_01

Structure conditionnelle : construction

La structure conditionnelle est la structure la plus basique en programmation. Elle permet d’exécuter une instruction
si un condition est validée. Il existe plusieurs structures conditionnelles :

  • If (si…)
  • If else (si… sinon…)
  • If else if (si… sinon si…)
  • switch (Dans le cas où …)

Dans un premier temps nous nous concentrerons sur les 3 premières structures. Reprenons notre première itération et
ajoutons la condition suivante : Si i est supérieur à la moitié de la largeur de la scène alors
mes cercles sont bleu. Il s’agit ici d’une condition simple if(). Elle se traduira par :

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);
  fill(255);

  for (int i=0; i<width; i+=40)
  {
    if (i > width/2)
    {
      fill(0, 0, 255);
    }
    ellipse(i+15, height/2, 30, 30);
  }
}

ixd_grille_02

Ajoutons maintenant une second condition à notre phrase : Si i est supérieur à la moitié de la largeur de la scène alors mes cercles sont bleu, sinon, il sont rouge. Il s’agit ici d’une condition if()…else{}. Elle se traduira de la manière suivante :

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);
  fill(255);

  for (int i=0; i<width; i+=40)
  {
    if (i > width/2)
    {
      fill(0, 0, 255);
    }
    else
    {
      fill(255, 0, 0);
    }
    ellipse(i+15, height/2, 30, 30);
  }
}

ixd_grille_03

Enfin complexifions un peu plus notre phrase de la manière suivante : Si i est supérieur à 1/3 de la largeur de la scène alors mes cercles sont vert, sinon si i est supérieur a 2/3 de la largeur de la scène alors mes cercles sont bleu, sinon il sont rouge. Il s’agit ici d’une condition multiple if()…else if(). Elle se traduira
de la manière suivante :

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);
  fill(255);

  for (int i=0; i<width; i+=40)
  {
    if (i > width/3*2)
    {
      fill(0, 0, 255);
    }
    else if (i > width/3)
    {
      fill(0, 255, 0);
    }
    else
    {
      fill(255, 0, 0);
    }
    ellipse(i+15, height/2, 30, 30);
  }
}

ixd_grille_04

Il est est possible de définir plusieurs conditionnelles de type OU ou ET à l’aide des opérateurs logique || et &&.
Ainsi nous pourrions dire : Si la position Y de la souris est supérieur à la moitié de la hateur de la scène ET que i est paire alors nos cercles sont bleu, sinon il sont rouge. Cela se traduirai par l’utilisation de l’opérateur logique && dans notre condition.

NB : Pour savoir si i est paire ou impaire nous utiliserons le modulo %. Cette opérateur
permet de connaitre la valeur résiduelle d’une division, c’est à dire son reste. Or si on
divise un nombre paire par 2 sont reste nulle. Cela se traduira en code par if(i%2 == 0).

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);
  fill(255);
  
  for(int i=0; i<width; i++)
  {
    float modulo = i%2;
    if(modulo > 0 && i<(width/2))
    {
      fill(0,0,255);
    }
    else
    {
      fill(255,0,0);
    }
    ellipse(i*40, height/2, 30, 30);
  }
}

ixd_grille_05

De la même manière que la double condition (&&) il est possible de réaliser une condition OU à l’aide de ||.
Ainsi nous pourrions dire : Si la position Y de la souris est supérieur à la moitié de la hateur de la scène OU que i est paire alors nos cercles sont bleu, sinon il sont rouge. Cela se traduirai par l’utilisation de l’opérateur logique || dans notre condition.

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);
  
  
   for(int i=0; i<width; i++)
  {
    if(mouseY > height/2 || i%2 ==0)
    {
      fill(0,0,255);
    }
    else
    {
      fill(255, 0, 0);
    }
    ellipse(i*40, height/2, 30, 30);
  }
}

ixd_grille_06

La dernière structure conditionnelle est la structure switch. Son fonctionnement est très proche de la structure if()…else if() mais celle-ci est plus pratique dans le cas où nous avons deux ou trois cas/condition à vérifier. Celle-ci fonctionne de la manière suivante :

int num = 1;
switch(num) {
case 0:
println(«Zero»); // Does not execute
break;
case 1:
println(«One»); // Prints «One»
break;
}

Elle se traduit de la manière suivante : Pour la variable entière i. Si celle ci est égale à 0 alors on effectue l’action println(«Zero»), si elle est égale à 1 alors on effectue l’action println(«Un»).

Prenons la en main de la manière suivante : Pour une variable entière «pattern», si celle-ci est égale à 0 alors mes cercle seront rouge, si celle-ci est égale à 1 alors mes cercles seront vert, enfin, si celle-ci est égale à 2 alors mes cercles seront bleu.

NB : Ici nous utiliserons de nouveau le modulo. En effet dans le cas d’une boucle de nombreux calculs à l’aide de modulo permettent de créer des suites.Ici nous définirons pattern à l’aide de i%3 = 0,1,2,0,1,2,0,1,2…

void setup()
{
  size(500, 200, P2D);
  smooth(8);
}

void draw()
{
  background(255);


  for (int i=0; i<width; i++)
  {
    int pattern = i%3;
    switch (pattern)
    {
    case 0:
      fill(255, 0, 0);
      break;
    case 1:
      fill(0, 255, 0);
      break;
    case 2:
      fill(0, 0, 255);
      break;
    }
    ellipse(i*40, height/2, 30, 30);
  }
}

ixd_grille_07

Un exemple de motif utilisant les structures conditionnelles et itératives

Dans cette exemple nous verrons comment utiliser les itérations afin de réaliser une motif composé de triangles.

motif_1_00111_2

Nous allons dans une premier temps définir la composition de notre motif. Ici nous souhaitons obtenir un motif à l’aide de superposition de triangles de teinte proche et ayant suffisamment de transparence pour laisser apparaitre leurs superpositions. Nous aurons aussi besoins de créer une grille à l’aide d’une double boucle for et d’en définir la résolution. Enfin nous utiliserons la méthode colorMode() pour travailler en teinte saturation et luminosité.

info

Dans un premier temps nous allons devoir définir les variables dont nous aurons besoins :

  • – Position x y des sommest A, B, C, D
  • – Teinte et incrément de teinte pour ne pas uniformiser les couleurs
  • – Résolution de la grille
  • – Random Seed (permettant de définir une constante à une méthode aléatoire)
int sWidth = 400;
int sHeight = sWidth*2;
int xA, yA, xB, yB, xC, yC, xD, yD;
float hue;
int Spacing=20;
int colorInc = 20;
int actualRandomSeed = 1000;

Nous définissons ensuite le constructeur de notre skecth

void setup()
{
size(sWidth, sHeight, P2D);
colorMode(HSB, 360, 100, 100, 100);
}

Enfin nous définissons notre boucle draw(). Nous utiliserons la méthode randomSeed() afin de définir une constante pour tout les nombre aléatoire que nous générerons. Nous créons ensuite notre double boucle for afin d’établir notre grille de motif de résolution «Spacing».

void draw()
{
background(0, 0, 100);
randomSeed(actualRandomSeed);

for (int gridX = 0; gridX<width/Spacing; gridX ++)
{
for (int gridY = 0; gridY<height/Spacing; gridY ++)
{
}
}
}

Enfin dans notre double boucle for() nous définissons les position de nos sommets où :

  • xA = gridX*Spacing;
  • yA = gridY*Spacing;
  • xB = xA+Spacing;
  • yB = yA;
  • xC = xA;
  • yC = yA+Spacing;
  • xD = xA+Spacing;
  • yD = yA+Spacing;

Afin de créer un dégradé de couleur nous utiliserons la méthode map() afin de définir la teinte gloable de nos triangles en fonction de leur position sur l’axe Y. La méthode map() permet de réaliser une règle de trois afin de mapper une valeur d’un rang de valeurs à un autre rang de valeurs.

xA = gridX*Spacing;
yA = gridY*Spacing;
xB = xA+Spacing;
yB = yA;
xC = xA;
yC = yA+Spacing;
xD = xA+Spacing;
yD = yA+Spacing;
hue = map(yA, 0, height, 180, 230);

Enfin, notre dernière étape consistera à dessiner nos triangles. Afin d’obtenir des triangles de teinte et d’opacité différentes nous utiliserons la méthode random() afin de celle-ci soit défini de manière aléatoire.

noStroke();
//ABC
fill(random(hue-colorInc, hue+colorInc), random(50, 100),
random(50, 100), random(20, 100));
triangle(xA, yA, xB, yB, xC, yC);
//BCD
fill(random(hue-colorInc, hue+colorInc), random(50, 100),
random(50, 100), random(20, 100));
triangle(xB, yB, xC, yC, xD, yD);
//ABD
fill(random(hue-colorInc, hue+colorInc), random(50, 100),
random(50, 100), random(20, 100));
triangle(xA, yA, xB, yB, xD, yD);
//ADC
fill(random(hue-colorInc, hue+colorInc), random(50, 100),
random(50, 100), random(20, 100));
triangle(xA, yA, xD, yD, xC, yC);

Ce qui nous donnera au final

int sWidth = 400;
int sHeight = 400;

int xA, yA, xB, yB, xC, yC, xD, yD;
float hue;
int Spacing=20;
int colorInc = 20;
int actualRandomSeed = 1000;

void setup()
{
  size(sWidth, sHeight, P2D);

  colorMode(HSB, 360, 100, 100, 100);
}



void draw()
{
  background(0, 0, 100);
  randomSeed(actualRandomSeed);

  for (int gridX = 0; gridX<width/Spacing; gridX ++)
  {
    for (int gridY = 0; gridY<height/Spacing; gridY ++)
    {
      xA = gridX*Spacing;
      yA = gridY*Spacing;

      xB = xA+Spacing;
      yB = yA;

      xC = xA;
      yC = yA+Spacing;

      xD = xA+Spacing;
      yD = yA+Spacing;


      hue = map(yA, 0, height, 180, 230);

      noStroke();

      //ABC
      fill(random(hue-colorInc, hue+colorInc), random(50, 100), random(50, 100), random(20, 100));
      triangle(xA, yA, xB, yB, xC, yC);

      //BCD
      fill(random(hue-colorInc, hue+colorInc), random(50, 100), random(50, 100), random(20, 100));
      triangle(xB, yB, xC, yC, xD, yD);

      //ABD
      fill(random(hue-colorInc, hue+colorInc), random(50, 100), random(50, 100), random(20, 100));
      triangle(xA, yA, xB, yB, xD, yD);

      //ADC
      fill(random(hue-colorInc, hue+colorInc), random(50, 100), random(50, 100), random(20, 100));
      triangle(xA, yA, xD, yD, xC, yC);
    }
  }
}

void mousePressed()
{
  actualRandomSeed = int(random(10000));
}

ixd_grille_08

Introduction à Processing [2.03]

Nota Bene : Cette article est un mise à jour de l’article du 10 Mars 2013 sur l’introduction à Processing. Il reprend les grandes lignes de l’article d’origine mais adapté aux modifications apportées dans Processing 2.0

Processing est un framework de developpement et un langage simplifié de JAVA créé par Benjamin Fry et Casey Reas au MIT Media Lab en 2001. Leur projet était de créer un langage simple, destiné aux graphistes et artistes, afin de répondre à certains de leur besoins tel que la visualisation de données ou la production de visuels génératifs. Processing est le prolongement « multimédia » de Design by numbers, l’environnement de programmation graphique développé par John Maeda au MIT Media Lab.

Processing a bien évolué depuis sa création en 2001 et est devenu un véritable outils de production pour les designer interactif et artistes.

Actuellement à sa version 2.03, processing est devenu une véritable communauté dont chaque membre apporte sa pierre à l’édifice que ce soit dans le framework directement, à travers des librairies ou par des extensions tel que processing.js ou ruby-processing.

Si nous devions faire un rapide état des lieux des principaux langages et/ou frameworks utilisés dans le design génératif ou l’art numérique nous aurions :

  • C/C++ avec des librairies tel que OpenFramework, Cinder…
  • Java en natif ou simplifié avec Processing ou Eclipse
  • Javascript en natif ou via des frameworks tel que Angular.js, Tree.js, Backbone.js…
  • Les langages de type nodal avec VVVV, Max MSP/Jitter ou Pure Data

Il existe évidement de nombreux autres langages utilisés dans l’expérimentation interactive et visuel tel que Flash (même si son utilisation se fait rare de nos jours) ou encore Touch Designer.

Les cours d’option « Digital Lab » à e-art sup Paris porteront sur les expérimentations visuelles et interactives, qu’elles soient sur écran ou hors écran et ce à travers l’utilisation de Processing. Cette option à pour but d’expérimenter de nouvelles techniques de design apporter par le design par le code et de sortir des techniques habituelles de conception. Il sera une initiation à l’expérimentation interactive et aux cours de développement interactif apportés par la filière Design Interactif d’e-art sup.

Cette année (2013-2014) l’enseignement portera essentiellement sur Processing 2.03, la dernière version stable de processing.

Processing et son environnement

En tant que Graphiste, l’interface de processing peut paraitre un peu « aride » lors de son premier lancement. En effet nous sommes tous des habitués de photoshop et ce qui se rapproche le plus du code pour nous reste flash avec son interface proche du tableau de bord d’une formule 1. Et comme pour photoshop ou la formule 1 il va nous falloir comprendre l’interface avant de se lancer dans la production.

L’interface de processing peut se découper en trois zones :
1_environnement

La première, que nous appellerons « header » contient les principaux éléments d’interface à savoir les boutons permettant :

  • 1 : l’execution du code
  • 2 : Stopper l’execution du code
  • 3 : Créer un nouveau sketch
  • 4 : Ouvrir un sketch
  • 5 : Sauvegarder un sketch
  • 6 : Exporter un Sketch (en applet web ou en stand alone)
  • 7 : Sélection du mode (Java, Android, javascript… de nombreux modes sont developpé par la communauté)
  • 8 : Créer une nouvelle tab, ou nouvelle feuille de code pour ce même sketch.

La seconde zone est notre zone de code. C’est ici que nous écrivons notre sketch.

Enfin la troisième zone est notre console de debug. C’est dans cette zone que s’affichera toute les données que nous demanderons d’afficher afin de debuguer notre sketch. Cette zone permet d’obtenir des informations textuelles sur un sketch sans que celles-ci soient affichées sur notre création.

Lorsque nous faisons « Executer le Code » ou ctrl+R (cmd+R pour OSX) une nouvelle fenêtre s’affiche. C’est notre fenêtre d’exécution. C’est ici que notre code s’exécute et affiche sa traduction graphique.

Maintenant que nous avons vu l’interface nous reste à faire un tour dans la façon dont processing fonctionne. Nous n’avons fasse à nous qu’une simple zone d’écriture, or nous voulons dessiner.

Il faut donc avant tout comprendre comment processing fonctionne afin de pouvoir traduire nos visuels en design par le code.

Si nous devions faire un rapprochement avec un des logiciels que nous connaissons en graphisme alors ce serait Illustrator. En effet, tout comme illustrator, il faut savoir que processing est un environnement de dessin vectoriel.

Et tout comme illustrator (et les autres logiciels de la suite adobe) Processing possède un système de cordonnées cartésien qui nous permet de situer un point dans l’espace (x, y, z).

Enfin, tout comme les logiciels que nous connaissons en design, l’origine de ce repère cartésien se trouvera en haut à gauche de notre fenêtre d’exécution.

2_environnement

Processing : les bases

Dans son écriture, un sketch processing s’articule principalement entre deux fonctions.

La première : setup(), est la fonction constructeur. Cette fonction, appelée une seule fois lors du lancement du sketch permettra de définir la taille de notre skecth, ses propriétés de rendu, de frame rate, de position….

La seconde : draw() est la boucle, c’est dans cette fonction, qui se rappelle à chaque fois que la précédente est terminée, que nous dessinerons. Par défaut processing s’exécute avec un frame rate de 6O, c’est à dire que en 1 seconde la boucle draw() se sera effectuée 60 fois.

Tout langage de programmation repose sur des notions communes à tous, tous se rapportant aux mathématiques. Il est important de maitriser ces notions de bases afin de pouvoir faire ce que l’on veut de processing et non l’inverse.

La première notion de base et la notion de variables commune à tout les langages. Une variable est un « mot clé » associé à une valeur. Cela permet de stocker en mémoire des valeurs sous un nom.

Par exemple si je place deux points de cordonnées 10, 10 et 20, 20 il me sera difficile en code de changer les coordonnées de mon deuxième point car je ne sais pas comment les appeler. Or, si je dis que mon premier point est placé en coordonnée x1, y2 et le seconde x2, y2 et que je défini par la suite x1 = 10 , y1= 10 et x2 = 20, y2 = 20 il m’est alors facile de changer la valeur de x1 en 30.

Il existe plusieurs types de variables dans processing mais nous ne commencerons ici que par les principales. Nous avons donc :

  • boolean etat = true; Variable de type boolean ou binaire, il s’agit d’une variable ne pouvant renvoyer que deux valeurs : Vrai/Faux. Elle permet de faire des comparaisons par exemple « Cette robe est-elle rouge? » est un question demandant un réponse booleenne. « Oui elle est rouge » « non elle ne l’est pas »
  • int x = 10; est une variable renvoyant un nombre entier
  • float x = 3.141592; est une variable renvoyant un nombre décimal
  • String nom = « moi »; est une variable renvoyant une chaine de caractères, elle permet de stocker des mots.
  • char nom = « a »; est une variable ne pouvant stocker que un caractère.

La seconde notion commune est la notion d’opérateurs. Ils permettent d’effectuer divers calculs ou comparaisons. Nous sommes ici dans des notions de bases des mathématiques.

Nous avons différents types d’opérateurs.

  • +, , *, / qui permettent d’addition, soustraire, multiplier ou diviser des valeurs.
  • % le modulo qui permet de connaitre la valeur d’un reste d’une division (valeur résiduelle) par exemple 17%3 = 2 car 2 est le reste de la division de 17/3

Les opérateurs d’assignation :

  • = permet d’attribuer une valeur à un variable
  • +=, -=, *=, /= permettent d’incrémenter, soustraire, multiplier ou diviser des valeurs par exemple x = x+1 est la même chose que x +=1;

Les opérateurs relationnels

  • >, <, <=, =>, == , != permettent de comparer deux valeurs afin de savoir si celles-ci sont supérieur, inférieur, supérieur ou égale, inférieur ou égale, égale ou différentes. Ces opérateur ne renvoi que des valeurs booléennes, c’est à dire vrai ou faux. Par exemple 1 == 2 renverra la valeur « false » car 1 n’est pas égale à 2;

Les opérateurs logiques

  • &&, || permettent d’effectuer des opérations booléenne de type ET et OU.

Enfin les dernières notions communes aux trois quart des langages et indispensable sont les structures conditionnelles et itératives.

Les structures conditionnelles permettent de comparer des valeur de d’attribuer des conditions.

Par exemple j’aimerai pouvoir marcher que si mes lacets sont fait sinon je tombe pourrai s’écrire :

boolean lacetsFait = true; //ici mes lacets sont fait, si lacetFait = false alors il ne sont pas fait

boolean tombe; //ici la variable booléenne n'est pas encore définie car je ne sais pas encore si je vais tomber ou pas

if(lacetFait == true)
{
tombe = false;
}
else{
tombe = true;
}

En mathématique, un itération est l’action de répéter un processus. La structure itérative permet d’effectuer une suite d’opération. Très utiliser pour effectuer plusieurs actions simultanées.

Par exemple, si je désire dessiner des lignes verticales de 100 pixels de haut et ce tout les 10 pixels de large je pourrais très bien écrire :

line(0, 0, 0, 100);
line(10, 0, 10, 100);
line(20, 0, 20, 100);
line(30, 0, 30, 100);
line(40, 0, 40, 100);
…

Et ce pour autant de largeur que je veux pour mon dessins. C’est assez simple pour un dessin de 100 pixel de large mais que ce passe-t-il pour 1920 pixels de large.

Je ne vais pas écrire 1920/10, soit 192 fois la même ligne? C’est là où la structure itérative entre en action.

Elle va me permette de dire que pour une variable i égale 0 (mon départ); i toujours inférieur à 1920 (la limite de mon dessin) et i s’incrémentant de 10 pixels (cad pour i=0 puis 10, puis 20…)

alors je dessine mes lignes. Ce qui donne

for(int i = 0; i<1920; i+=10)
{
line(i, 0, i, 100);
}

Et me voila avec un dessin de 192 lignes de 100 pixels de haut espacées de 10 pixels entre elles. Bref l’itération est notre amie.

Nous venons donc de voir les bases communes aux trois quart des langages de programmation, il est grand temps d’entrer dans le vif du sujet avec les bases de processing.

Comme nous avons vu au début, processing est un dérivé du JAVA, il s’agit d’une version simplifiée de ce langage. Il nous offre donc plusieurs fonctions permettant de dessiner en quelques secondes.

Les deux premières fonctions que nous allons voir permettent de définir la taille de notre sketch, son moteur de rendu et son antialiasing.

Ces deux fonctions seront appelées dans le setup() de notre sketch.

Nous savons que processing est un environnement vectoriel mais de base il va tenter d’économiser un maximum de mémoire. C’est la raison pour laquelle il possède différents moteurs de rendu permettant de dessiner avec plus ou moins de lissage ou seulement en 2D ou avec de la 3D. Processing 2 possède 2 moteurs de rendus (basés sur OpenGL) :

P2D – moteur de rendu de processing pour des dessins 2D.

P3D – moteur de rendu 3D de processing.

Même si nous avons appelé un moteur de rendu performant (2D ou 3D) nous nous rendrons compte que notre ligne reste pixelisée. Cela parce que nous devrons appeler l’antialiasing afin de lisser notre dessin.

Voici donc comment ces fonctions vont s’écrire :

size(largeur, hauteur, moteur de rendu); //permet de définir la taille du sketch et son moteur de rendu, par exemple size(100, 200, JAVA2D);

smooth(); //permet d'activer l'antialiasing

Les prochaines fonctions que nous verrons sont des fonctions de dessins.

background(valeur, valeur, valeur) permet de définir la couleur de notre fond.

Attardons-nous un peu sur cette fonction.Celle-ci est assez particulière car il s’agit d’une fonction permettant de libérer de la mémoire. Nous le savons, notre code se lit successivement en boucle. Cela veut dire que processing va dessiner 25 fois par seconde. Imaginons un sketch de 710*200 où nous dessinerons un cercle de taille 20 à la position de la souris et ce sans background :

void setup()
{
size(710, 200, JAVA2D);
smooth();
}

void draw()
{
ellipse(mouseX, mouseY, 20, 20);
}

ixd_processingIntro_00

lors de l’exécution nous remarquons qu’au fur est à mesure les cercles que nous dessinons restent sur scène et forment une trainée. Nous savons que processing exécute la boucle draw 25 par seconde, or cette boucle dit de dessiner un cercle à la position de la souris. Il est donc logique que en 1 seconde j’ai 25 cercles, en 2seconde 50 et ainsi de suite…. Au bout d’un moment notre ordinateur va se mettre à chauffer et c’est normal car nous demandons pas mal de calculs.

C’est là que la fonction background entre en fonction. En insérant un background (noir par exemple) dans mon draw, je demande a processing d’effacer tout les cercle précédents sur scène pour n’afficher que celui de la boucle actuelle. Cela aura pour effet de nettoyer la mémoire de processing.
Cela nous donnera :

ixd_processingIntro_01

Pour écrire un background dans ma boucle draw j’utiliserai la syntaxe suivante :

background(valeur, valeur, valeur);

Attardons nous un peu sur ces « valeurs » entre parenthèse. Processing me demande 3 valeurs pour définir la couleur de mon fond. Par défaut processing fonctionne en RVB, c’est à dire qu’il chaque couleurs est définie par ces valeurs rouge, verte et bleu, chacune d’entre-elles étant définie entre 0 et 255.

Ainsi background(0, 0, 0) sera noir alors que background(255, 0, 0) sera rouge.

Il faut aussi savoir que processing permet de simplifier son code. Ainsi si nous mettons background(0) processing comprendra que nos 3 valeurs seront égales à 0

Entrons maintenant plus en détail dans les outils de dessin. Processing va nous permettre de dessiner ce que l’on veut mais il nous offre aussi des fonctions pour dessiner des formes géométriques simples. Ainsi nous avons :

point(x, y); //qui permet de dessiner un point dans un espace 2D

point(x, y, z); //qui permet de dessiner un point dans un espace 3D

rect(x, y, largeur, hauteur); //permettant de dessiner un rectangle

ellipse(x, y, largeur, hauteur); //permettant de dessiner une ellipse	

quad(x1, y1, x2, y2, x3, y3, x4, y4); // permettant de dessiner un quadrilatère simple

triangle(x1, y1, x2, y2, x3, y3); //permettant de dessiner un triangle.

line(x1, y1, x2, y2); //permettant de dessiner une ligne dans un espace 2D

line(x1, y1, z1, x2, y2, z2);//permettant de dessiner une ligne dans un espace 3D

//Il existe d'autre formes simple tels que les arcs ou les courbes de bézier

Enfin, pour terminer ce chapitre d’initiation sur processing regardons un peu les deux éléments de base qu’il nous manque pour faire notre premier dessin.

Nous avons vu les notions de base communes aux trois quarts des langages, nous avons vu les deux fonctions de base de processing setup() et draw() permettant de définir notre skecth et de dessiner à l’intérieur. Nous avons aussi vu comment définir la taille de notre sketch, son moteur de rendu, son antiliasing et comment dessiner un fond et des formes simples. Autrement dis il ne nous manque que de la mise en forme de ces dessins, à savoir comment définir leurs couleurs de fond et/ou de contour.

Pour cela processing nous met à disposition deux fonctions :

fill(valeur, valeur, valeur); //permet de définir la couleur de fond d'un objet en RVB, cette fonction doit être écrite avant la forme.

stroke(valeur, valeur, valeur); permet de définir la couleur de contour d'un objet en RVB, cette fonction doit être écrite avant la forme.

Mais que ce passe-t-il dans le cas où je veuille avoir un fond ou un contour avec de l’alpha? Qu’à cela ne tienne, processing à tous prévu. Il nous suffira d’ajouter une quatrième valeur à notre fill() ou notre stroke. Nous aurons ainsi défini des valeurs en RVB+Alpha. Mais n’oublions pas une chose, processing fonctionne sur des valeurs RGB de 0 à 255, il en va de même pour l’alpha. Ainsi un alpha de 50% correspondra à la valeur 255/2 soit 127.

enfin dans le cas où je ne veuille pas mettre de fond ou de contour à mon dessin il nous suffira d’écrire l’une des fonctions suivante : noFill() ou noStroke();

Nous avons donc vu toute les bases de processing pour effectuer notre premier skecth. Profitons en alors pour faire notre premier dessin en utilisant ce que l’on connait.

Nous allons dessiner une sketch de 710*500, contenant un grille de point espacer de 10 pixels chacun et avec un cercle de 100 pixel de diametre sans contour et de remplissage blanc à 50% d’alpha.

Cela nous donne donc

int space = 10;

void setup()
{
size(710, 500, P2D);
smooth();
}

void draw()
{
background(0);
for(int i = 0; i<width; i+=space)
{
for(int j = 0; j<height; j+=space)
{
stroke(255);
point(i, j);
}
}

noStroke();
fill(255, 127);
ellipse(width/2, height/2, 100, 100);
}

ce qui nous donnera :

ixd_processingIntro_02

Design Génératif et Expériences Interactives [Introduction et support]

Lors du premier cours de l’année en option digital lab, les étudiants ont découvert ce qu’était le design génératif et l’expérimentation interactive au travers divers rappels historique.

Ce rappel historique porte à la fois sur les grandes évolutions technologiques, théoriques et de langages de programmation que sur de grandes pièces de l’art numérique ou de son application au travers de la communication visuelle.

Le document ci-joint, support de cours, présente différentes frises chronologiques non-exhaustif, ainsi qu’une présentation du processus créatif génératif. La fin de ce document présente aussi de nombreux liens vers des conférences, interviews, films..

DigitalLab_DesignGeneratifEtExperimentationsInteractives

Programmation orientée objet & Générateur de Particules

La programmation orientée objet est l’une des bases de programmation moderne et se retrouve dans un grand nombre de langages de programmation. Elle permet de créer des « objets » mais aussi de nous simplifier grandement la vie.

Prenons un sketch simple de 200*200 dans lequel nous souhaitons dessiner une balle se déplaçant à une vitesse aléatoire et rebondissant sur les bords de notre scène. Nous écrirons :

//Variables de notre balles
float x; //position x de la balle
float y; //position y de la balle
float vx; //vitesse x de la balle
float vy; //vitesse y de la balle
float t; // taille de la balle

void setup()
{
  size(200, 200, JAVA2D);
  smooth();
  
  //initialisation des variables.
  t = random(5, 10);
  x = random(t, width-t);
  y = random(t, height-t);
  vx = random(-3, 3);
  vy = random(-3, 3);
}

void draw()
{
  background(255);
  
  //mise à jour des variables
  x += vx;
  y += vy;
  
  //detection des murs
  if(x <= t || x >= width-t)
  {
    vx = -vx;
  }
  if(y <= t || y >= height-t)
  {
    vy = -vy;
  }
  
  ellipse(x, y, t*2, t*2);
}

ixd_classe_00-01

Mais que ce passe-t-il si nous voulons non plus une balle mais 10, 40 ou 100?
La première solution consisterait à créer autant de variables que nous avons de balles mais cela n’est pas envisageable. Notre code dépassera de loin le nombre de page de la constitution américaine et nous serons vite perdu.
La seconde solution serait d’utiliser des tableaux pour stocker chacune de nos variables mais là aussi nous allons avoir de nombreux tableaux et cela n’est toujours pas envisageable.
C’est là que la programmation orientée objet entre en jeux. Elle va nous permettre de créer aisément nos 10, 40 ou 100 balles en un minimum de temps.

Qu’est ce qu’un objet?

Un objet est directement inspiré du monde réel. En effet nous sommes entouré d’objets et chacun de ces objets à des propriétés propres. Par exemple nous avons les objets « téléphones ». Chacun de ses objets « téléphones » sont définis par le fait qu’ils ont tous des variables communes, à savoir le fait de pouvoir passer des communications vocales par le biais d’un réseau téléphonique. Mais ils possèdent aussi des variables qui leur sont propre comme le fait de pouvoir envoyer des sms ou non, d’être tactile ou pas, d’avoir des poids et des tailles qui diffèrent… Bref nos téléphones sont des objets appartenant à une même classe, la classe « Téléphone ».

Revenons maintenant à notre skecth, nous avons imaginé avoir un espace de 200*200 dans lequel des balles de tailles différentes se déplacent à des vitesses différentes. Nous avons donc des « objets » qui ont un comportement commun, à savoir le fait d’être des balles et de se déplacer, mais aussi différents puisque leur vitesses diffèrent. Nous avons l’exemple parfait pour créer une « classe » d’objets.

Notre première classe.

Précédemment nous avons parlé de « classe » sans même l’expliquer. Une classe est une matrice, un patron, un plan de construction… d’un objet. La classe d’un objet permet de créer et d’instancier les variables communes à tous nos objet, de développer leur comportements et de créer la méthode de construction de notre objet. D’un point de vu développement, elle est assez proche de la façon dont nous construisons un sketch.

Lorsque nous créons nos sketchs nous avons l’habitude de créer des variables, puis une méthode setup() qui nous permettra de créer notre sketch (taille, moteur de rendu…) et enfin une méthode draw() (notre boucle). Lorsque nous créons une classe nous obtenons à peu de choses près la même syntaxe :

class maClasse
{
	//Ici mes variables

	//Mon constructeur, l'équivalent de mon setup() pour mon sketch
	Balle()
	{
	}

	//Mes méthode
	void display()
	{
	}
}

Entrons maintenant dans le vif du sujet en créant notre classe « Balle ». Pour cela nous allons reprendre l’ensemble de nos variables, nous les initialiserons dans notre constructeur et enfin nous dessinerons notre balle dans une méthode display(). Cela nous donne donc :

class Balle
{
  //variables globales
  float x; //position x de la balle
  float y; //position y de la balle
  float vx; //vitesse x de la balle
  float vy; //vitesse y de la balle
  float t; // taille de la balle

  //Mon constructeur, celui doit toujours avoir le même nom que la class afin d'être reconnu comme un constructeur.
  Balle()
  {
    //initialisation des variables.
    t = random(5, 10);
    x = random(t, width-t);
    y = random(t, height-t);
    vx = random(-3, 3);
    vy = random(-3, 3);
  }

  void display()
  {
    //mise à jour des variables
    x += vx;
    y += vy;

    //detection des murs
    if (x <= t || x >= width-t)
    {
      vx = -vx;
    }
    if (y <= t || y >= height-t)
    {
      vy = -vy;
    }
    fill(255);
    stroke(0);
    ellipse(x, y, t*2, t*2);
  }
}

Nous venons d’écrire notre classe mais comme nous avons vu, il s’agit ici de la matrice qui nous permettra de créer nos objets.
Il nous faut maintenant créer nos objets et les instancier pour ensuite les afficher sur la scène. Dans notre sketch, nous allons créer un nouvel objet « maBalle » faisant partie de la classe « Balle ».

Balle maBalle;

Dans notre setup() nous instancierons notre objet.

maBalle = new Balle();

Enfin dans notre draw, nous appellerons la méthode display() de notre balle

maBalle.display();

Cela nous donne donc

Balle maBalle;

void setup()
{
  size(400, 200, JAVA2D);
  smooth();
  maBalle = new Balle();
}

void draw()
{
  background(255);
  maBalle.display();
}

class Balle
{
  //variables globales
  float x; //position x de la balle
  float y; //position y de la balle
  float vx; //vitesse x de la balle
  float vy; //vitesse y de la balle
  float t; // taille de la balle

  //Mon constructeur, celui doit toujours avoir le même nom que la class afin d'être reconnu comme un constructeur.
  Balle()
  {
    //initialisation des variables.
    t = random(5, 10);
    x = random(t, width-t);
    y = random(t, height-t);
    vx = random(-3, 3);
    vy = random(-3, 3);
  }

  void display()
  {
    //mise à jour des variables
    x += vx;
    y += vy;

    //detection des murs
    if (x <= t || x >= width-t)
    {
      vx = -vx;
    }
    if (y <= t || y >= height-t)
    {
      vy = -vy;
    }
    fill(255);
    stroke(0);
    ellipse(x, y, t*2, t*2);
  }
}

ixd_classe_00-01

On remarque que pour appeler une méthode de notre classe nous procédons de la manière suivante objet.methode(). Il en va de même pour accéder aux variables. Ainsi println(maBalle.x) me renverra la valeur x de ma balle. On peut par la suite aisément modifier ces valeurs.

On avait pas parlé de plusieurs balles?

Nous venons de créer notre première classe mais nous obtenons 1 balle et non 100 comme promis précédemment. Pour cela rien de plus simple, et c’est là le grand pouvoir des classes, il nous suffit de créer un tableau d’objets et non plus un seul objet.

Balle[] maBalle;
int nbBalle;

void setup()
{
  size(200, 200, JAVA2D);
  smooth();
  nbBalle = 100;
  maBalle = new Balle[nbBalle];
  for(int i = 0; i < maBalle.length; i++)
  {
    maBalle[i] = new Balle();
  }
}

void draw()
{
  background(255);
  for(int i = 0; i < maBalle.length; i++)
  {
    maBalle[i].display();
  }
}

ixd_classe_02

Allons plus loin dans la possibilité qu’offrent les classes.

Nous savons maintenant comment créer une classe et instancier des objets mais allons plus loin dans les possibilités que nous offre la programmation orientée objet. Nous venons de créer 100 objets mais que ce passe-t-il dans le cas où nous voulions en créer à chaque clic?
Il faudrait que nous rajoutions des éléments à notre tableau.

Imaginons un sketch simple, de 400*200 dans lequel nous pourrions rajouter une balle à chaque clic dont la position de départ sera définie par la position de la souris.

Pour cela nous allons toujours utiliser un tableau mais d’un autre type puisque nous utiliserons un ArrayList(). À l’inverse d’un tableau classique maBalle[] qui possède une taille fixe, les ArrayList sont des tableaux dont la taille n’est pas définie et peut être en constante expansion. Cependant sa déclaration diffère d’un tableau classique. Ainsi pour passer notre classe balle en ArrayList pour devrons changer notre code comme il suit :

ArrayList<Balle> maBalle;
int nbBalle;

void setup()
{
  size(200, 200, JAVA2D);
  smooth();
  nbBalle = 100;
  maBalle = new ArrayList<Balle>();
}

De même la methode nous permettant d’ajouter des balles sur notre scène diffère d’un tableau classique

for (int i=0; i<nbBalle; i++)
  {
    maBalle.add(new Balle());
  }

Afin la dernière différence avec le tableau classique est la façon nous nous parcourons notre ArrayList afin d’appeler la méthode display() de notre classe.

for(int i = 0; i < maBalle.size(); i++)
  {
    Balle b = maBalle.get(i);
    b.display();
  }

Maintenant que nous avons vu comment créer et parcourir un ArrayList, il ne nous reste plus qu’à modifier notre code précédent afin de créer nos balles à chaque clic de la souris

ArrayList<Balle> maBalle;
int nbBalle;

void setup()
{
  size(400, 200, JAVA2D);
  smooth();
  nbBalle = 10;
  maBalle = new ArrayList<Balle>();
  for (int i=0; i<nbBalle; i++)
  {
    maBalle.add(new Balle()));
  }
}

void draw()
{
  background(255);
  
  if(mousePressed)
  {
     maBalle.add(new Balle());
  }
 
  for(int i = 0; i < maBalle.size(); i++)
  {
    Balle b = maBalle.get(i);
    b.display();
  }
}

Le résultat est là mais cependant il nous manque une règle à respecter. Nous voulions que nos balles apparaissent à la position de la souris, hors pour le moment elle apparaissent à des positions aléatoire. En effet lorsque l’on retourne dans le constructeur de la classe nous remarquons que notre x et y sont définies de manière aléatoire.

Balle()
  {
    //initialisation des variables.
    t = random(5, 10);
    x = random(t, width-t);
    y = random(t, height-t);
    vx = random(-3, 3);
    vy = random(-3, 3);
  }

Nous pourrions changer cela par un mouseX mouseY mais en faisant cela nos 10 première balles présentes sur scène auront le même point d’origine. Nous allons donc légèrement changer notre constructeur en lui indiquant qu’à son appel il devra recevoir des variables. Nous pourrons alors par la suite définir aisément la position de chacune de nos balles à la création.

Balle(float x_, float y_)
  {
    //initialisation des variables.
    t = random(5, 10);
    x = x_;
    y = y_;
    vx = random(-3, 3);
    vy = random(-3, 3);
  }

Il nous faudra alors changer la façon dont nous instancions nos balles comme il suit

maBalle.add(new Balle(position x, position y));

Nous avons maintenant un tableau dynamique (ArrayList) et nous créons autant de balles que nous voulons.
Ajoutons maintenant une dernière possibilité, à savoir fixer une limite à notre tableau dynamique. En effet cela nous permettra d’économiser notre ordinateur qui risquerait, au bout d’un millions de particules, de ralentir.

Pour cela nous rajoutons une condition permettant de supprimer la première balle (la plus vieille) créée lorsque nous atteignons un maximum de 30 balles

if(maBalle.size() >= 30)
    {
      maBalle.remove(0);
    }

Nous voila donc avec notre première classe de particules.

ArrayList<Balle> maBalle;
int nbBalle;

void setup()
{
  size(400, 200, JAVA2D);
  smooth();
  nbBalle = 10;
  maBalle = new ArrayList<Balle>();
  for (int i=0; i<nbBalle; i++)
  {
    maBalle.add(new Balle(random(width), random(height)));
  }
}

void draw()
{
  background(255);
  
  if(mousePressed)
  {
     maBalle.add(new Balle(mouseX, mouseY));
  }
  
  if(maBalle.size() >= 30)
    {
      maBalle.remove(0);
    }
  
  for(int i = 0; i < maBalle.size(); i++)
  {
    Balle b = maBalle.get(i);
    b.display();
  }
}

class Balle
{
  //variables globales
  float x; //position x de la balle
  float y; //position y de la balle
  float vx; //vitesse x de la balle
  float vy; //vitesse y de la balle
  float t; // taille de la balle

  //Mon constructeur, celui doit toujours avoir le même nom que la class afin d'être reconnu comme un constructeur.
  Balle(float x_, float y_)
  {
    //initialisation des variables.
    t = random(5, 10);
    x = x_;
    y = y_;
    vx = random(-3, 3);
    vy = random(-3, 3);
  }

  void display()
  {
    //mise à jour des variables
    x += vx;
    y += vy;

    //detection des murs
    if (x <= 0 || x >= width)
    {
      vx = -vx;
    }
    if (y <= 0 || y >= height)
    {
      vy = -vy;
    }
    fill(255);
    stroke(0);
    ellipse(x, y, t*2, t*2);
  }
}

ixd_classe_03

Nous venons de voir comment créer et utiliser des objets pour créer un premier générateur de particules. Cependant nous pouvons aller beaucoup plus loin avec les classes en créant des objets réagissant entre eux et ayant leur propres comportement.

Imaginons un sketch dans lequel se déplaceraient des atomes. Chaque atome est défini par une masse aléatoire qui influencera sa vitesse, plus sa masse sera grande plus il sera lent. Chacun d’entre eux possédera un nombre aléatoire d’électrons gravitant autour d’eux. Ces atomes se déplaceront sur la scène et rebondiront sur les « murs » de celle-ci. Leurs électrons tourneront autour en se rapprochant de l’atome puis en s’en éloignant jusqu’à une certaines limite définie par la masse de l’atome.

Lorsque deux atomes entrent en collision ceux-ci créaient alors un nouvel atome au design différent dont la masse sera l’addition de la masse des deux atomes d’origine. De même le nombre d’électrons gravitant autour de ce nouvel atome sera défini par l’addition du nombre d’électrons gravitant autour des deux atomes d’origine.

Enfin lorsque deux nouveaux atomes entrent en collision, ils se divisent pour recréer les deux atomes d’origines qui les composaient.

atoms

Un skecth comme celui ci est l’exemple parfait pour réaliser des classes. La première sera notre classe Atoms qui aura ses caractéristique propre :

• Position
• Masse
• Vitesse
• Nombre d’électrons

La seconde sera notre classe NewAtoms qui aura les caractéristiques suivante :

• Position (Position de la collision des deux atomes d’origine)
• Masse (Addition des deux masses)
• Vitesse
• Nombre d’électrons (Addition des électrons)

Nous obtenons donc les classe suivantes :

class Atom
{
  //variables de l'atome
  float x, y; //position de l'atome
  float masse; //masse de l'atome, ici son diamètre
  float velocite, vx, vy; //velocité des atome, celles-ci seront dépandantent de la masse des atomes
  color cF, cS; //couleur de l'atome

  //variable des electrons
  int nbElectrons;
  float[] posX; //position des electrons
  float[] posY; //position des electrons
  float[] r; //rayon des electrons sur le cercle trigonométrique
  float[] angle; //angle des electrons sur le cercle trigonométrique
  float[] vr; //vitesse des electrons
  float[] vAngle; //vitesse de l'angle des electrons
  float[] origin; //rayon d'origine qui établir notre limite haute de mouvement

  //constructeur
  Atom(float _x, float _y, float _masse, int _nbElec)
  {
    this.x = _x;
    this.y = _y;
    this.masse = _masse;
    this.cF = color(random(160, 180), random(70, 100), random(2, 10), random(70, 100));
    this.cS = color(random(160, 180), random(70, 100), random(10, 20), random(70, 100));

    //calcul des velocités
    this.vx = this.x;
    this.vy = this.y;
    this.velocite = map(this.masse, 5, 30, 5, 1);
    this.vx = this.velocite*random(-1, 1);
    this.vy = this.velocite*random(-1, 1);

    //electrons
    createElectron(_nbElec);
  }

  //--------
  //methodes
  //--------

  //constructeur des Electrons
  void createElectron(int _nbElectrons)
  {
    nbElectrons = _nbElectrons;
    posX = new float[nbElectrons];
    posY = new float[nbElectrons];
    r = new float[nbElectrons];
    angle = new float[nbElectrons];
    vr = new float[nbElectrons]; 
    vAngle = new float[nbElectrons];
    origin = new float[nbElectrons]; 
    for (int i=1; i<nbElectrons; i++) { 
      angle[i] = ((2*PI)/nbElectrons)*i; //Ici nous divisons le cercle 2PI par le nombre de particule multiplier par i pour obtenir l'angle de chaque particule et les placer à équidistance les une des autres
      vr[i] = random(-0.5, -0.1);
      vAngle[i] =radians(1);// radians(random(-1, 1));
      r[i] = random(this.masse/2, this.masse);
      origin[i] = r[i]; 
      posX[i] = this.x+cos(angle[i])*r[i]; //position x sur le cercle x = cos(angle)*rayon
      posY[i] = this.y+sin(angle[i])*r[i]; //position y sur le cercle y = cos(angle)*rayon
    }
  }

  //deplacement
  void motion()
  { 
    this.x += this.vx; 
    this.y += this.vy;
  }

  void checkEdge()
  {
     if (this.x<0)
    {
      this.x = 1;
      this.vx *=-1;
    }
    else if(this.x>width)
    {
      this.x = width-1;
      this.vx *=-1;
    }

    if (this.y<0)
    {
      this.y = 1;
      this.vy *=-1;
    }
    else if(this.y>height)
    {
      this.y = height-1;
      this.vy *=-1;
    }
  }

  //dessin et mouvement des electrons
  void electrons()
  {
    for (int i=1; i<nbElectrons; i++) { 
      r[i] += vr[i]; // reduction du rayon
      angle[i] += vAngle[i];
      if (r[i]>origin[i]+1 || r[i]<this.masse/2-1)
      {
        vr[i] *=-1;
      }
      //position des partciules
      posX[i] = this.x+cos(angle[i])*r[i];
      posY[i] = this.y+sin(angle[i])*r[i];
      stroke(0, 0, 100, 30);
      noFill();
      //dessin de la fleur
      ellipse(posX[i], posY[i], 3, 3);
      line(posX[i], posY[i], this.x, this.y);
      if (i != nbElectrons-1)
      {
        line(posX[i], posY[i], posX[i+1], posY[i+1]);
      }
      else
      {
        line(posX[i], posY[i], posX[1], posY[1]);
      }
    }
  }

  //affichages
  void display()
  {

    //dessin l'atome
    //noStroke();
    electrons();
    fill(0, 0, 100, 5);//this.cF);
    stroke(0, 0, 100, 20);//this.cS);
    ellipse(this.x, this.y, this.masse/2, this.masse/2);
    point(this.x, this.y);
  }
}
class newAtom
{
  //variable du nouvel atome
  float x, y; //position du nouvel atome
  float masse; //masse du nouvel atome
  float velocite, vx, vy; //velocité des atome, celles-ci seront dépandantent de la masse des atomes

  //valeur d'origine de l'atome
  float masse1, masse2;
  int nbElec1, nbElec2;


  //variable des electrons
  int nbElectrons;
  float[] posX; //position des electrons
  float[] posY; //position des electrons
  float[] r; //rayon des electrons sur le cercle trigonométrique
  float[] angle; //angle des electrons sur le cercle trigonométrique
  float[] vr; //vitesse des electrons
  float[] vAngle; //vitesse de l'angle des electrons
  float[] origin; //rayon d'origine qui établir notre limite haute de mouvement

  //constructeur
  newAtom(float _x, float _y, float _masse, int _nbElec, float _masse1, float _masse2, int _nbElec1, int _nbElec2)
  {
    this.x = _x;
    this.y = _y;
    this.masse = _masse;

    //origine de l'atome
    this.masse1 = _masse1;
    this.masse2 = _masse2;
    this.nbElec1 = _nbElec1;
    this.nbElec2 = _nbElec2;

    //vitesse
    this.velocite = map(this.masse, 10, 60, 3, 1);
    this.vx = this.velocite*random(-1, 1);
    this.vy = this.velocite*random(-1, 1);

    //electrons
    createElectron(_nbElec);
  }

  //--------
  //methodes
  //--------
  //constructeur des Electrons
  void createElectron(int _nbElectrons)
  {
    nbElectrons = _nbElectrons;
    posX = new float[nbElectrons];
    posY = new float[nbElectrons];
    r = new float[nbElectrons];
    angle = new float[nbElectrons];
    vr = new float[nbElectrons]; 
    vAngle = new float[nbElectrons];
    origin = new float[nbElectrons]; 
    for (int i=1; i<nbElectrons; i++) { 
      angle[i] = ((2*PI)/nbElectrons)*i; //Ici nous divisons le cercle 2PI par le nombre de particule multiplier par i pour obtenir l'angle de chaque particule et les placer à équidistance les une des autres
      vr[i] = random(-0.5, -0.1);
      vAngle[i] =radians(1);// radians(random(-1, 1));
      r[i] = random(this.masse/2, this.masse);
      origin[i] = r[i]; 
      posX[i] = this.x+cos(angle[i])*r[i]; //position x sur le cercle x = cos(angle)*rayon
      posY[i] = this.y+sin(angle[i])*r[i]; //position y sur le cercle y = cos(angle)*rayon
    }
  }

  //deplacement
  void motion()
  { 
    this.x += this.vx; 
    this.y += this.vy;
  }

  void checkEdge()
  {
    if (this.x<0)
    {
      this.x = 1;
      this.vx *=-1;
    }
    else if(this.x>width)
    {
      this.x = width-1;
      this.vx *=-1;
    }

    if (this.y<0)
    {
      this.y = 1;
      this.vy *=-1;
    }
    else if(this.y>height)
    {
      this.y = height-1;
      this.vy *=-1;
    }
  }


  //dessin et mouvement des electrons
  void electrons()
  {
    for (int i=1; i<nbElectrons; i++) { 
      r[i] += vr[i]; // reduction du rayon
      angle[i] += vAngle[i];
      if (r[i]>origin[i]+1 || r[i]<this.masse/2-1)
      {
        vr[i] *=-1;
      }
      stroke(0, 0, 100, 15);
      noFill();
      
      posX[i] = this.x+cos(angle[i])*r[i];
      posY[i] = this.y+sin(angle[i])*r[i];
      ellipse(posX[i], posY[i], 4, 4);
      
      posX[i] = this.x+cos(angle[i])*(r[i]-4);
      posY[i] = this.y+sin(angle[i])*(r[i]-4);
      ellipse(posX[i], posY[i], 2, 2);
      
      posX[i] = this.x+cos(angle[i])*(r[i]+10);
      posY[i] = this.y+sin(angle[i])*(r[i]+10);
      line(posX[i], posY[i], this.x, this.y);
      
    }
  }

  void display()
  {
    electrons();
    fill(0, 0, 100, 10);
  }
}

Il nous faut ensuite créer nos objets, les instancier et calculer leur collisions. Pour ce dernier point nous utiliserons la méthode dist() nous permettant de calculer la distance entre nos objets. Lorsque cette distance sera égale ou inférieure à 0 nous définirons une collision, créerons nos nouveaux atomes et supprimerons les atomes d’origine. Enfin nous calculerons les collisions entre les nouveaux atomes pour faire l’inverse en les supprimant et en recréant nos atomes d’origine

/*"Fusion atome" est un skecth permettant de découvrir la programation orientée objet dans processing
 Dans ce skecth nous verrons comment créer un classe et gérer des comportements
 
 Nous allons créer un système de particule se deplacement à des vitesses et des sens différents et aléatoires.
 Lorsque deux particules se rencontre, une nouvelle se créer. Cette nouvelle particules est le resultat de la fusion des
 deux premières. Lorsque deux particules fusionnées se rencontre elles redeviennent des particules simples.*/
//--------------------------------------------------------------------------------------------------------//


ArrayList<Atom> myAtom; //declaration de mon tableau dynamique, ici nous utilisons un tableau dynamique de sorte à gérer sa taille de manière dynamique.
int nbAtomBegin; //nombre d'atome au départ du sketch

ArrayList<newAtom> myNewAtom; //déclarationd d'un tableau dynamique de nouveau atoms

void setup()
{
  size(displayWidth/2, displayHeight/2, OPENGL);
  smooth(8);
  colorMode(HSB, 360, 100, 100, 100);

  nbAtomBegin = 10; // nombre d'atome au départ
  myAtom = new ArrayList<Atom>(); //création de mon tableau d'atome
  for (int i=0; i<nbAtomBegin; i++)
  {
    myAtom.add(new Atom(random(width), random(height), random(5, 40), int(random(5, 11))));
  }

  myNewAtom = new ArrayList<newAtom>(); //création de mon tableau de nouveaux Atomes
  background(0);

}

void draw()
{
  fill(200, 100, 15, 50);
  noStroke();
  rect(0, 0, width, height);
  
//rotate(radians(50), 1, 0, 0);
  for (int i=0; i<myAtom.size(); i++)
  {
    Atom pi = myAtom.get(i);

    for (int j=i+1; j<myAtom.size(); j++)
    {
      Atom pj = myAtom.get(j);
      float distanceBrute = dist(pi.x, pi.y, pj.x, pj.y);
      float distance = distanceBrute-(pi.masse/2+pj.masse/2);
      //float alphaP = map(d, 0, 50, 100, 0);

      if (distance<=0)
      {
        myNewAtom.add(new newAtom(pi.x, pi.y, pi.masse+pj.masse, pi.nbElectrons+pj.nbElectrons, pi.masse, pj.masse, pi.nbElectrons, pj.nbElectrons));      
        myAtom.remove(pi);
        myAtom.remove(pj);
      }
    }
  }

  for (int i=0; i<myNewAtom.size(); i++)
  {
    newAtom pi = myNewAtom.get(i);

    for (int j=i+1; j<myNewAtom.size(); j++)
    {
      newAtom pj = myNewAtom.get(j);
      float distanceBrute = dist(pi.x, pi.y, pj.x, pj.y);
      float distance = distanceBrute-(pi.masse/2+pj.masse/2);

      if (distance<=0)
      {
        myAtom.add(new Atom(pi.x-10, pi.y-10, pi.masse1, pi.nbElec1)); 
        myAtom.add(new Atom(pi.x+10, pi.y+10, pi.masse2, pi.nbElec2));
        myAtom.add(new Atom(pj.x-10, pj.y-10, pj.masse1, pj.nbElec1)); 
        myAtom.add(new Atom(pj.x+10, pj.y+10, pj.masse2, pj.nbElec2)); 
        myNewAtom.remove(pi);
        myNewAtom.remove(pj);
      }
    }
  }


  //affichage des atomes
  for (Atom a: myAtom)
  {
    a.display();
    a.motion();
    a.checkEdge();
  } 

  for (newAtom na: myNewAtom)
  {
    na.display();
    na.motion();
    na.checkEdge();
  }
}

void mouseReleased()
{

  myAtom.add(new Atom(mouseX, mouseY, random(5, 30), int(random(5, 11))));
}

atoms_billboard

Retour sur les bases de trigonométrie

Il n’est pas rare de voir les étudiants l’air dépités à l’idée de devoir faire de la trigonométrie, et pourtant celle-ci revient très souvent sur le devant de la scène lorsque l’on commence à vouloir faire des attracteurs ou des mouvements circulaires.

La trigonométrie et ses fonctions sont des éléments indispensables quand on commence à vouloir faire des visuels plus évolués. Même si son nom parait horrible et semble nous renvoyer à nos cours de maths de 3ème nous verrons que processing va vite nous la faire re-aimer.

Ici nous nous attarderons sur le cercle trigonométrique qui nous permettra de voir comment on effectue le calcul d’un angle ou même comment positionner un point sur un cercle. En bref nous verrons nos vieux amis des cours de maths que sont sinus, cosinus et tangente.

Nous appliquerons cela à deux skecth. Le premier nous permettra de réaliser un attracteur/repulseur simple. Le seconde, plus appliquer au design génératif nous permettra de découvrir comment réaliser des visuel simple à l’aide de la trigonométrie.

Petit rappel de trigonométrie

La trigonométrie traite des relations entre distances et angles dans les triangles et notamment dans le triangle rectangle. C’est ce dernier qui va nous intéresser plus particulièrement.

Faisons un rapide bon dans le passé concernant ce triangle. Un triangle rectangle se caractérise par un angle droit (90°) et la somme de ses angles est égale à 180°. L’angle droit est donc son angle le plus grand. Enfin le côté opposé à cet angle s’appel l’hypoténuse et se caractérise par le fait qu’il est le côté le plus grand de ce triangle.

Capture d’écran 2013-03-17 à 18.24.47

Si on s’interesse à l’angle BAC et aux fonctions trigonométriques alors nous remarquons que :

  • sin(BAC) = a/c
  • cos(BAC) = b/c
  • tan(BAC) = a/b

Nous avons ici les fonctions auxquelles nous allons prêter attention dans processing.

Allons maintenant plus loin en traçant un cercle dont l’origine sera A et de rayon AB (notre hypothenuse). Partons du principe que notre hypothenuse AB a un valeur de 1. Nous obtenons alors un cercle trigonométrique.

Capture d’écran 2013-03-17 à 18.31.18

Si on observe ce cercle nous remarquons que les cordonnées du point B peuvent être définies de la sorte

  • x = cos(t)
  • y = sin(t)

Or nous savons ici que notre hypothenuse à un valeur de 1. Si nous voulons être correcte dans nos cordonnées, nous obtenons

  • x = cos(t)*1
  • y = sin(t)*1

Nous venons de voir la formule permettant de calculer les coordonnées d’un point sur un cercle. Nous avons ici l’une des formules que nous utiliserons le plus dans processing par la suite.

  • x = cos(angle)*rayon
  • y = sin(angle)*rayon

Nous venons de voir à quoi les fonctions trigonométriques Sinus et Cosinus pouvaient nous être utile, attardons nous maintenant sur la dernière, la Tangente.

La tangente est le rapport du sinus au cosinus, par définition :

  • tan(angle) = sin(angle)/cos(angle)

On appelle tangente de l’angle aigu , le nombre noté tan(angle) défini par BC/AC. Nous verrons par la suite comment nous pouvons nous servir de la tangente pour calculer un angle.

Capture d’écran 2013-03-17 à 18.24.47

Trigonométrie et processing

Nous venons de voir un résumer des règles de trigonométrie dont nous allons nous servir le plus. Retournons maintenant sur processing pour faire un rapide état des lieux des méthodes disponible. Si nous venons de revoir toute ces bases c’est bien parce que nous allons utiliser les angles afin de positionner des points sur un cercle. Là dessus processing est très bien fait puisqu’il va nous proposer différentes fonctions trigonométriques tel que :

float s = sin(a); //sinus d'un angle
float c = cos(a); //cosinus d'un angle
float t = tan(a); //tangente d'un angle

float as = asin(a); //inverse du sinus
float ac = acos(a); //inverse du cosinus
float at = atan(a); //inverse de la tangente

Une des particularités à savoir sur processing et qu’il effectue ses calculs d’angle en radians là où nous avons plutôt tendance utiliser des dégrés. Le radians est l’unité de mesure internationale d’un angle. Ainsi, si en degrés nous divisons un cercle en 360° en radians celui-ci se divise entre 0 et 2PI

cercle_trigonom_trique

Lorsque nous travaillons en degrés on pourrait se dire qu’il va nous falloir entrer dans des calculs fastidieux mais là aussi processing nous offre une fonction permettant de convertir un angle de degrés en radians

float a = radians(valeur en degré);

Nous avons maintenant tout ce qu’il nous faut pour tester la trigonométrie dans processing.

Attracteur et répulseur simple

Pour voir à quoi peut nous servir la trigonométrie prenons un exemple d’un projet d’un étudiant de 4ème année.
Ce dernier réalisait une installation interactive avec la wiimote. Cette dernière lui servait de pointeur et il souhaitait que régulièrement, les lettres présentent sur le sketch à des positions aléatoires, attaquent le pointeur. Essayons de reproduire cela le plus simplement possible.

Nous allons produire un sketch de 500*500 sur lequel nous placerons des balles à des positions aléatoire. Celles-ci attaqueront notre souris. C’est à dire qu’elles se déplaceront en direction de notre souris et une fois leurs déplacements effectués, retournerons à leurs places avant d’effectuer une autre attaque.

Cela peut sembler compliqué. Comment faire en sorte de rapprocher nos balles vers notre souris sachant que certaines seront plus haute, plus basse, plus à droite ou plus à gauche. Réduisons notre concept à une balle on se rend tout de suite compte qu’il nous suffit d’utiliser la trigonométrie. Nous avons une balle et une cible. Nous connaissons la distance entre ces deux élément et nous savons que la position d’un point sur un cercle est égale à :

  • x = cos(angle)*rayon
  • y = sin(angle)*rayon

Il nous suffit alors de calculer la position de notre balle sur un cercle dont le rayon est égale à sa distance la séparant de sa cible. Nous n’auront plus qu’à incrémenter ou décrémenter notre rayon pour attirer ou repousser notre balle

trigo2

Reproduisons cela à partir d’une balle que nous placerons de maniere aléatoire, nous définirons la position de l’attracteur au centre de notre skecth. La première chose que nous devons faire c’est de replacer cette balle sur un cercle dont le rayons sera égale à la distance entre la balle et sa cible. Pour cela nous allons utiliser la fonction dist() qui nous permettra d’obtenir le rayon de notre cercle. C’est ce rayon que nous incrémenterons puis decrémenterons pour attirer ou repousser notre balle.

d = dist(balle_x, balle_y, cible_x, cible_y);

Nous savons que les coordonnées x, y d’un point sur un cercle sont respectivement égales à cos(angle)*rayon et sin(angle)*rayon. Nous avons déjà le rayon, il ne nous manque donc plus que l’angle.

Si on regarde de plus pres notre schema, nous avons notre balle sur un cercle de centre cible mais nous avons aussi un triangle rectangle dont nous connaissons déjà l’hypothenuse c’est à dire notre rayon.

gabarit_blogeart_3

Il nous est alors facile d’obtenir les autres valeurs puisque nous avons les cordonnées de notre balle et de sa cible. Ainsi :

  • a = balle_x – cible_x
  • b = balle_y – cible_y

Nous savons aussi que la tangente d’un angle est égale au côté opposé / côté adjacent ou dans notre cas à b/a. Pour obtenir notre angle il nous faudra alors obtenir l’inverse de cette tangente. C’est là l’utilité de la fonction atan() de processing qui nous reverra l’inverse de la tangente et donc notre angle en radians

angle = atan2(balle_y - cible_y, balle_x - cible_x);

Nous pouvons donc définir les coordonnées de notre balle sur le cercle comme ceci

  • x = cos(angle)*rayon
  • y = sin(angle)*rayon

Il ne nous reste plus qu’à décrémenter puis incrémenter notre rayons. Nous obtenons alors ceci
(cliquez sur le skecth pour activer l’attracteur ou sur ‘a’ pour repositioner la balle)

//Cliquez pour lancer l'animation
float posX, posY, oX, oY, d, angle, r, v;
boolean etat = true;

void setup()
{
  size(500, 500);
  update();
}

void draw()
{
  background(255);
  //calcul de l'angle
  angle = atan2(posY - oY, posX - oX);
  //deplacement de la balle
  if (etat == true)
  {
    r=d; //si nous n'activons pas l'attracteur le rayon est égale à la distance entre nos éléments
  }
  if (etat == false)
  {
    r+=v; //sinon notre rayon s'incrémente de la vitesse (-1 donc négative)
  }

  if ( r<1 || r>d)
  {
    v = -v;//si nous atteignons les limite rayon ou 0 notre vitesse s'inverse. Nous attirons puis repoussons notre balle
  }

  //position de la balle
  posX = oX+cos(angle)*r;
  posY = oY+sin(angle)*r;

  //balle
  stroke(0);
  strokeWeight(5);
  point(posX, posY);
  strokeWeight(1); 
  stroke(255, 0, 0);
  line(posX, posY, oX, oY);
  ellipse(posX, posY, 15, 15);

  //cercle trigonométrique
  stroke(0);
  point(oY, oY);
  strokeWeight(1);
  noFill();
  ellipse(oX, oY, d*2, d*2);
  fill(255);
  ellipse(oX, oY, 10, 10);
}

void keyPressed()
{
  if (key == 'a')
  {
    update();
  }
}

void mousePressed()
{
  etat = !etat;  
}

//fonction initialisant notre balle
void update()
{
  oX = width/2;
  oY = width/2;
  posX = random(width);
  posY = random(height);
  d = dist(posX, posY, oX, oY);
  v = -1;
}

ixd_trigo_00

Nous avons une balle attirée par notre cible mais celle-ci est fixe et nous n’avons qu’une balle. Comment appliquer cela à plusieurs balles?

Pour cela rien de plus simple, il nous suffit d’utiliser les listes et de replacer chacune de nos balles sur un cercle dont le rayon sera la distance entre la balle et sa cible. Il nous faut utiliser les notions vu dans le cours précédent à savoir les liste et les boucle for (disponible ici).

Nous obtenons alors ceci (cliquez pour activer l’attracteur, appuyez sur ‘a’ pour repositioner les balles et sur ‘z’ pour retirer les tracés)

//Cliquez pour lancer l'animation

float oX;
float oY;
float nbParticles;
float[] d;
float[] angle;
float[] posX;
float[] posY;
float[] posX_2;
float[] posY_2;
float[] v;
float[] r;

boolean etat = true;
boolean dessin = true;

void setup()
{
  size(500, 500);
  //frameRate(2); 
  update();
}

void draw()
{

  background(255);
  oX = mouseX;
  oY = mouseY;
  for (int i = 0; i<nbParticles; i++)
  {
    angle[i] = atan2(posY[i] - oY, posX[i]  - oX);
    d[i] = dist(posX[i], posY[i], oX, oY);
    //deplacement de la lettre
    if (etat == true)
    {
      r[i]=d[i];
    }
    if (etat == false)
    {
      r[i]+=v[i];
    }

    if ( r[i]<1)
    {
      v[i] = -v[i];
    }
    if ( r[i]>d[i])
    {
      v[i]= -v[i];
      r[i]=d[i];
    }

    //position de la lettre 
    posX_2[i] = oX+cos(angle[i])*r[i];
    posY_2[i] = oY+sin(angle[i])*r[i];

    if(dessin == true)
    {
    float a = map(d[i], 0, width, 0, 50);  
     stroke(0, a);
     ellipse(oX, oY, d[i]*2, d[i]*2);
     //dessin de l'origine de point
    stroke(0);
    strokeWeight(5);
    point(posX[i], posY[i] );
    strokeWeight(1);
    line(posX[i], posY[i], oX, oY);
    stroke(255, 0, 0);
    strokeWeight(1);
    line(posX_2[i], posY_2[i], oX, oY);
    }

    //dessin de lettre
    stroke(255, 0, 0);
    strokeWeight(1);
    ellipse(posX_2[i], posY_2[i], 15, 15);

  }

  //ellipse 
  strokeWeight(1);
  stroke(255, 100);
  point(oY, oY);
  noFill();

}

void keyPressed()
{
  if (key == 'a')
  {
    update();
    etat = true;
  }
  if(key == 'z')
  {
    dessin = !dessin;
  }
}

void mousePressed()
{
  etat = !etat;

}

void update()
{

  nbParticles = 20;
  oX = width/2;
  oY = height/2;
  posX = new float[20];
  posY = new float[20];
  d = new float[20];
  v = new float[20];
  angle = new float[20];
  r = new float[20];
  posX_2 = new float[20];
  posY_2 = new float[20];
  for (int i = 0; i<nbParticles; i++)
  {
    posX[i] = random(width);
    posY[i] = random(height);
    d[i] = dist(posX[i], posY[i], oX, oY);
    v[i] = random(-4, -0.5);
  }
}

ixd_trigo_01

Design génératif et trigonométrie

Nous avons vu comment faire un attracteur/repulseur simple mais allons un peu plus loin dans l’utilisation de ces méthodes et appliquons cela à du design génératif. Il nous est facile à partir de là d’imaginer un visuel génératif type fleur.

Reprenons nos balles. Définissons leur position de départ sur un cercle de rayon 200 et toute équidistantes les une des autres.
Nous gardons notre principe d’attracteur et définissons une vitesse aléatoire pour chacune de nos balles. Au fur et à mesure celle-ci se déplaceront vers le centre de notre skecth. À chaque fois que l’une d’entre elles atteindra le centre nous clôturons le cycle et en déclencherons un nouveau. À chaque début de cycle nous définirons une plage de couleur pour notre fleur générative. Ajoutons un peu de piquant à notre sketch et définissant à chaque debut de cycle un sens de rotation pour notre fleur. Enfin définissons pour chaque cycle un temps de vie de 1000 frame avant de se re-initialiser

trigo3

Globalement nous allons utiliser le même code que notre attracteur à plusieurs balles mais nous ne dessinerons pas la même chose. Commençons par définir une fonction update() dans laquelle nous initialiserons toute nos valeurs.

float oX, oY, d; //cible et distance de la cible
float[] posX; //position des partciules
float[] posY; //position des partciules
color[] c; //couleurs des particules
float[] r; //rayon des particules sur le cercle trigonométrique
float[] angle; //angle des particules sur le cercle trigonométrique
float[] vr; //vitesse des particules
float colorRatio; //plage de couleur

boolean etat = false;//animation on/off
int nbParticules;//notre de point

float angleGlobal; //angle de rotation de notre skecth
float vAngle; //sens de la rotation

float count; //notre décompte. Toute les 500 frame nous effacerons notre fleur pour en dessiner une nouvelle.

void setup()
{
  size(500, 500);
  colorMode(HSB, 360, 100, 100, 100); // ici nous passons en mode colorimétrique Teinte Saturation Luminosité. Ce qui nous pemrettra de définir plus aisement nos plages de couleur
  update();  //fonction initialisant nos valeurs
  background(0);
}

void update()
{
  vAngle = random(-0.1, 0.1); //définition du sens de rotation du sketch 
  colorRatio = random(0, 300); //choix d'un plage de couleur entre 0 et 300° - notre plage sera entre cette valeur et cette valeur+60
  nbParticules = 50; 
  oX = width/2;
  oY = width/2;
  posX = new float[nbParticules];
  posY = new float[nbParticules];
  r = new float[nbParticules];
  angle = new float[nbParticules];
  vr = new float[nbParticules];  
  c = new color[nbParticules];

  for (int i=1; i<nbParticules; i++) { 
    angle[i] = ((2*PI)/nbParticules)*i; //Ici nous divisons le cercle 2PI par le nombre de particule multiplier par i pour obtenir l'angle de chaque particule et les placer à équidistance les une des autres
    vr[i] = random(-0.5, -0.1); 
    r[i] = 200; //ici nos particules ont un rayon de départ identique pour former notre cercle mais nous pouvons aussi le définir en random et obtenir un autre visuel random(150, 200);
    posX[i] = oX+cos(angle[i])*r[i]; //position x sur le cercle x = cos(angle)*rayon
    posY[i] = oY+sin(angle[i])*r[i]; //position y sur le cercle y = cos(angle)*rayon
    d = dist(posX[i], posY[i], oX, oY); //distance entre les particules et la cible
    c[i] = color(random(colorRatio, colorRatio+60),random(80, 100), random(80,100)); //définissions de la couleur
  }
}

Maintenant que nous avons notre cercle de départ il ne nous reste plus qu’à le dessiner de la manière suivante

void draw()
{

  pushMatrix();
  /*rotation de la fleur*/
  if (etat == false) //si nous activons la rotation de la fleur
  {

    angleGlobal += vAngle;
  }
  else if (etat == true) //sinon la fleur reste droite
  {
    angleGlobal = angleGlobal;
  }
  translate(oX, oY);
  rotate(radians(angleGlobal));

  /*calcul des nouvelles positions de notre fleur*/
  for (int i=1; i<nbParticules; i++) { 
    r[i] += vr[i]; // reduction du rayon

    if (r[i]>200 || r[i]<0) //limite de la fleur. Si une des particules atteint le centre alors nous re initinalisons un cycle à l'aide de la fonction update();
    {
      update();
    }
    //position des partciules
    posX[i] = cos(angle[i])*r[i];
    posY[i] = sin(angle[i])*r[i];
    stroke(c[i], 5);
    noFill();
   //dessin de la fleur
    if (i != nbParticules-1)
    {
      line(posX[i], posY[i], posX[i+1], posY[i+1]);
    }
    else
    {
      line(posX[i], posY[i], posX[1], posY[1]);
    }
  }
  popMatrix();
  //decompte
  count++;
  if(count >= 1000)
  {
    count = 0;
    background(0);
  }
}

void keyPressed()
{
  if (key == 'a')
  {
    etat = !etat;
  }
}

//interactivité
void mousePressed()
{
  background(0);
  update();
}

Nous obtenons alors la fleur suivante. À partir de là il nous est facile de rajouter des comportements à notre fleur ou de changer sa forme de départ, son sens de rotation, la manière dont elle se dessine… et tout ça sur une base de trigonométrie.
trigonometrie

Les Arrays… premier pas vers les objets

Les tableaux, listes ou arrays font partis des fondamentaux des langages de programmation. Il s’agit d’une «variable» nous permettant de stocker plusieurs variables auxquelles on aura accès via un numero d’index. Sur le papier cela peut faire peur mais par un exemple c’est tout de suite plus simple.

Prenons une classe d’étudiants, chaque étudiant a un prénom. Nous avons donc un tableau d’étudiant de ce type

String nom de l'étudiant = {Pierre, Paul, Jacques};

Je pourrai donc alors dire que le nom de l’étudiant 1 = Pierre, le nom de l’étudiant 2 = Paul et le nom de l’étudiant 3 = Jacques

À quoi ça sert?

En premier lieux à nous simplifier la vie. C’est le premier pas avant le développement orienté objet. Les tableaux vont nous permettre de stocker des valeurs « communes » pour les traiter plus facilement et par groupe.

Imaginons que nous voulions faire un sketch de 100*100 avec 3 balles sur scène. Nous aimerions qu’en fonction des touches du clavier a, z ou e cela change la couleur de la balle 1, 2 ou 3 et restore la couleur des autres balles à leur valeur d’origine. Pour cela, sans tableau nous aurions écris :

float c1=255;
float c2=255;
float c3=255;

void setup()
{
  size(100, 100);
}

void draw()
{
  background(255);
  ellipseMode(CORNER);
  fill(c1);
  ellipse(0, 0, 10, 10);
  fill(c2);  
  ellipse(0, 10, 10, 10);
  fill(c3);
  ellipse(0, 20, 10, 10);
}

void keyReleased()
{
  if (key == 'a')
  {
    c1 = 0; 
    c2 = 255;
    c3 = 255;
  }  
  if (key == 'z')
  {
    c2 = 0;
    c1 = 255;
    c3 = 255;
  }  
  if (key == 'e')
  {
    c3 = 0;
    c1 = 255;
    c2 = 255;
  }
}

ixd_array_00-01

Le résultat fonctionne mais si nous voulons appliquer cela à 10, 50, ou 200 balles alors cela devient très vite laborieux d’écrire 10, 50 ou 200 variables. C’est là où nos tableaux deviennent plus pratiques.

Utilisation d’un tableau

Comme nous l’avons vu plus haut un tableau est une variable, nous verrons donc qu’il n’est pas très différent dans sa déclaration. Quand à son utilisation, nous verrons aussi qu’il nous faudra utiliser notre vieille amie, la boucle for().

Un tableau se déclare assez facilement, très proche de la variable, nous devrons utiliser les [] pour déclarer notre tableau puis les {} pour en déclarer des éléments. Par exemple, pour déclarer les lettres avec lesquelles nous pourrons changer de couleurs, nous écrirons

char[] l = {'a', 'z', 'e'};

Dans le cas où nous voudrions déclarer des éléments identiques dans notre tableau, ou de façon dynamique, notre syntaxe changera. Il nous faudra toujours déclarer notre tableau avec nos [] mais nous déclarons les éléments dans une boucle for(). Cela nous facilite la vie, comme dans le cas de nos balles où nous voulons qu’elles soient toute blanche dès les départ.

flaot[] c = new float[3]; // ici nous déclarons un tableau c de 3 éléments

for(int i=0; i<3; i++) // puis nous définisons chaque element comme un valeur de 255;
  {
    c[i] = 255;    
  }

Nous avons donc créé deux tableaux, nous allons maintenant les appliquer à notre Sketch de 100*100. Commençons par dessiner nos ellipses et définir leur remplissage. Dans mon premier skecth, nous avions écris 3 valeurs fill() et 3 ellipses, ce qui était assez laborieux. Maintenant toute ces lignes peuvent se résumer comme il suit :

for(int i = 0; i<c.length; i++) // Notons que la méthode .length nous renvoie la taille de notre tableau. Ici c.lenght = 3 car notre tableau contient 3 éléments.
  {
    ellipseMode(CORNER);
    fill(c[i]);
    ellipse(0, i*10, 10, 10);
  }

Appliquons maintenant cette même méthode à la partie interactive. La seule différence ici c’est qu’il va nous falloir savoir sur quelle touche nous avons appuyé. Précédemment nous utilisions des condition if(key == ‘a’). Nous allons faire la même chose mais de manière dynamique à l’aide de notre boucle for

void keyReleased()
{
 for(int i = 0; i<c.length; i++)
 {
  if (key == l[i])
  {
    c[i] = 0;
  }
  else
  {
    c[i] = 255;
  }
 }
}

Nous obtenons alors le même résultat pour moins de ligne et surtout en étant plus flexible. Il nous sera facile de créer 100 balles et les rendre interactive.
ixd_array_00-01

Application à des random walker?

Nous avons vu comment faire des tableaux sur 3 balles mais allons plus loin en appliquant ces nouvelles connaissances à des random walkers.

Un random walker ou marcheur aléatoire est un objet, ici un cercle, se déplaçant de manière aléatoire. Imaginons le sketch suivant :

Nous aimerions avoir plusieurs cercles (50), effectuant un déplacement du bas vers le haut à une vitesse aléatoire et un déplacement latéral aléatoire entre -1 et 1 pixel. Nous aimerions aussi que nos cercles aient une taille aléatoire et que cette même taille varie de manière aléatoire. Enfin nous aimerions que le contour de nos cercles soit plus ou moins noir en fonction de leur distance avec la souris.

Si nous étudions un peu ce que nous devons faire nous nous rendons compte qu’il nous faut des valeurs différentes mais commune à tous tel que :

  • La position en x d’un cercles
  • La position en y d’un cercles
  • La taille minimale d’un cercle
  • La taille maximale d’un cercle
  • la vitesse en y d’un cercle

Nous avons donc besoins de 5 tableaux. Commençons par déclarer nos tableaux et le nombre de nos particules

float[] x;
float[] y;
float[] taille1;
float[] taille2;
float[] vy;
int nbParticules;

Nos tableaux étant créés il va nous falloir les remplir. Là encore regardons ce que nous voulons faire.
Nous voulons que nos cercles soit placés les un à coté des autres en x. Cela veut donc dire que chaque cercle sera placé à équidistance les un des autres au départ.Nous savons aussi qu’au départ ils seront tous positionnés en bas de notre skecth. Enfin nous savons qu’ils auront une vitesse aléatoire et des tailles variantes et aléatoires entre eux. Nous aurons donc dans notre setup()

void setup()
{
  size(700, 500, JAVA2D);
  nbParticules = 50; //nb de particules
  x = new float[nbParticules];
  y = new float[nbParticules];
  taille1 = new float[nbParticules];
  taille2 = new float[nbParticules];
  vy = new float[nbParticules];

  for (int i=0; i<nbParticules; i++)
  {
      x[i] = width/nbParticules*i; //position les cercle tout les 14 pixels
      y[i] = height; //placer les cercles en bas du skecth
      taille1[i] = random(0, 5); //definir une taille minimal aléatoire
      taille2[i] = random(10, 20); //définir une taille maximal aléatoire
      vy[i] = random(0.2, 1); //définir une vitesse aléatoire
   }
   background(255);
}

Il nous faut maintenant dessiner nos cercles. Là encore regardons ce que nous voulons faire.
Nous aimerions que la couleur de contour de nos cercles dépendent de leur distance avec la souris. Pour cela attardons nous un peu sur deux fonctions de processing dist() et map().

Pour réaliser cela nous avons besoins de connaitre la distance entre la souris et un cercle. On pourrait entrer dans des calculs plus ou moins complexe avec des x et des y ou nous pouvons profiter d’une des fonctions de processing nous permettant de calculer la distance entre deux points. Pour cela il nous suffit d’appeler la méthode dist().

float d = dist(x1, y1, x2, y2); //notons que cette methode peut aussi calculer des distance dans un espace 3D

Nous obtenons alors une distance comprise entre 0 et 860 pixels environs (diagonale du skecth).
On pourrait dire que le contour de notre cercle est égale à cette valeur mais nous savons que le mode de colorimétrie de processing fonctionne sur des valeur RVB comprissent entre 0 et 255. Il nous faut donc appliquer une règle de proportionnalité afin de dire que les valeur 0-255 de notre contour sont proportionnelles à la distance d. Une fois de plus processing nous propose une méthode très simple nous permettant de réaliser cette règle de 3, la méthode map().

float m = map(valeur à mapper, minimale de départ, maximale de départ, minimale d'arrivée et maximale d'arrivée);

Maintenant que nous savons comment faire varier notre couleur de contour essayons de déplacer nos cercles. Nous savons qu’ils doivent de déplacer en latérale de manière aléatoire entre -1 et 1. il nous faut donc la valeur x suivante

x = random(-1, 1);

Pour le mouvement vertical, nos cercles doivent se déplacer à des vitesses différentes et oscillent entre le haut et le bas. Nous allons donc incrémenter notre y de la valeur de notre vitesse et inverser cette valeur lorsque y attendra ses limites haute et basse

y -= vy;

    if(y < 0 || y > height)
    {
      vy *= -1;
    }

Nous avons alors tout ce dont nous avons besoins pour la boucle for() de notre draw.

for (int i=0; i<nbParticules; i++)
  {
    float d = dist(mouseX, mouseY, x[i], y[i]);
    float c = map(d, 0, width, 0, 255);

    float taille = random(taille1[i], taille2[i]);

    x[i] += random(-1, 1);
    y[i] -= vy[i];

    if(y[i] < 0 || y[i] > height)
    {
      vy[i] *= -1;
    }

    stroke(random(c));
    fill(255);
    ellipse(x[i], y[i], taille, taille);
  }

Si nous observons notre code dans la globalité et le résultat nous obtenons alors

float[] x;
float[] y;
float[] taille1;
float[] taille2;
float[] vy;
int nbParticules;

void setup()
{
  size(700, 500, JAVA2D);
  nbParticules = 50;
  x = new float[nbParticules];
  y = new float[nbParticules];
  taille1 = new float[nbParticules];
  taille2 = new float[nbParticules];
  vy = new float[nbParticules];

  for (int i=0; i<nbParticules; i++)
  {
      x[i] = width/nbParticules*i;
      y[i] = height;
      taille1[i] = random(0, 5);
      taille2[i] = random(10, 20);
      vy[i] = random(0.2, 1);
   }
   background(255);
}

void draw()
{

  for (int i=0; i<nbParticules; i++)
  {
    float d = dist(mouseX, mouseY, x[i], y[i]);
    float c = map(d, 0, width, 0, 255);

    float taille = random(taille1[i], taille2[i]);

    x[i] += random(-1, 1);
    y[i] -= vy[i];

    if(y[i] < 0 || y[i] > height)
    {
      vy[i] *= -1;
    }

    stroke(random(c));
    fill(255);
    ellipse(x[i], y[i], taille, taille);
  }
}

 Array