Introduction aux vecteurs

Observer le monde qui nous entoure et à trouver des moyens astucieux de simuler ce monde avec du code.

Algorithmiejavascript

Ce cours consiste à observer le monde qui nous entoure et à trouver des moyens astucieux de simuler ce monde avec du code. Nous commencerons par examiner la physique de base - comment une pomme tombe d’un arbre, comment un pendule se balance dans l’air, comment la terre tourne autour du soleil, etc. Tout ce dont nous allons parler ici nécessite l’utilisation de l’élément de base de la programmation du mouvement, le vecteur. Et c’est donc ici que nous commençons notre histoire. Le mot “vecteur” peut signifier beaucoup de choses différentes. Vector est le nom d’un groupe de rock new wave formé à Sacramento, en Californie, au début des années 1980. C’est le nom d’une céréale pour petit-déjeuner fabriquée par Kellogg’s Canada. Dans le domaine de l’épidémiologie, un vecteur est utilisé pour décrire un organisme qui transmet une infection d’un hôte à un autre. Dans le langage de programmation C++, un vecteur (std::vector) est une implémentation d’une structure de données de type tableau redimensionnable dynamiquement. Bien que toutes ces définitions soient intéressantes, elles ne sont pas ce que nous recherchons.

Ce que nous voulons est appelé un vecteur euclidien (du nom du mathématicien grec Euclide et également connu sous le nom de vecteur géométrique). Lorsque vous verrez le terme “vecteur” dans ce cours, vous pourrez supposer qu’il s’agit d’un vecteur euclidien, défini comme une entité ayant à la fois une magnitude et une direction. Un vecteur est généralement dessiné sous la forme d’une flèche ; la direction est indiquée par l’endroit où pointe la flèche, et la magnitude par la longueur de la flèche elle-même.

Pourquoi utiliser les vecteurs?

Avant de nous plonger dans les détails des vecteurs, examinons un programme de base qui démontre pourquoi nous devrions nous intéresser aux vecteurs en premier lieu.

// Adapted from Dan Shiffman, natureofcode.com

var x = 100;
var y = 100;
var xspeed = 1;
var yspeed = 3.3;

var draw = function() {
    background(255, 255, 255);
    
    // Move the ball according to its speed.
    x = x + xspeed;
    y = y + yspeed;
    
    // Check for bouncing.
    if ((x > width) || (x < 0)) {
        xspeed = xspeed * -1;
    }
    if ((y > height) || (y < 0)) {
        yspeed = yspeed * -1;
    }
    
    noStroke();
    fill(181, 181, 181);
    // Display the ball at the location (x,y).
    ellipse(x, y, 32, 32);
};

Dans l’exemple ci-dessus, nous avons un monde très simple - un écran blanc avec une forme circulaire (une “balle”) qui se déplace. Cette balle a quelques propriétés, qui sont représentées dans le code comme des variables.

$Position: x ,y$

$Vitesse: xSpeed, ySpeed$

On pourrait imaginer d’autres variables:

$Acceleration: xAcceleration, yAcceleration$

$Position de la cible: xTarget, yTarget$

$Vent: xWind, yWind$

$Force de friction: xFriction, yFriction$

Il devient de plus en plus clair que pour chaque concept de ce monde (vent, position, accélération, etc.), nous aurons besoin de deux variables. Et ceci n’est qu’un monde à deux dimensions. Dans un monde en 3D, nous aurons besoin de x, y, z, xSpeed, ySpeed, zSpeed, et ainsi de suite.

Ne serait-il pas agréable de simplifier notre code et d’utiliser moins de variables ? Au lieu de:

    var x = 5;
    var y = 10;
    var xSpeed;
    var ySpeed;

On pourrait n’avoir que 2 variables:

    var position;
    var speed;

Cette première étape dans l’utilisation des vecteurs ne nous permettra pas de faire quelque chose de nouveau. Le simple fait d’utiliser des objets vectoriels pour vos variables ne permettra pas à votre programme de simuler la physique comme par magie. Cependant, ils simplifieront votre code et fourniront un ensemble de fonctions pour les opérations mathématiques courantes qui se produisent encore et encore et encore lors de la programmation du mouvement.

En guise d’introduction aux vecteurs, nous allons vivre en deux dimensions. Tous ces exemples peuvent être assez facilement étendus à trois dimensions (et l’objet que nous utiliserons, PVector, permet d’utiliser trois dimensions).

Programmer avec PVector

Une façon d’envisager un vecteur est la différence entre deux points. Réfléchissez à la manière dont vous pourriez donner des instructions pour marcher d’un point à un autre.

Vous l’avez probablement déjà fait en programmant des mouvements. Pour chaque image de l’animation (c’est-à-dire un seul cycle de la boucle draw() de ProcessingJS), vous demandez à chaque objet à l’écran de se déplacer d’un certain nombre de pixels horizontalement et d’un certain nombre de pixels verticalement.

Pour chaque image :

nouvelle position = vitesse appliquée à la position actuelle

Si la vitesse est un vecteur (la différence entre deux points), qu’est-ce que la position ? Est-ce aussi un vecteur ? Techniquement, on pourrait dire que la position n’est pas un vecteur, puisqu’elle ne décrit pas comment se déplacer d’un point à un autre - elle décrit simplement un point singulier dans l’espace.

Néanmoins, une autre façon de décrire une position est le chemin suivi depuis l’origine pour atteindre cette position. Cela signifie qu’une position peut être le vecteur représentant la différence entre la position et l’origine.

Examinons les données sous-jacentes pour la position et la vitesse. Dans l’exemple de la balle rebondissante, nous avions les données suivantes :

$position: x, y$

$vitesse: xSpeed, ySpeed$

Remarquez que nous stockons les mêmes données pour les deux - deux nombres à virgule flottante, un x et un y. Si nous devions écrire nous-mêmes une classe vectorielle, nous commencerions par quelque chose de plutôt basique :

var Vector = function(x, y) {
    this.x = x;
    this.y = y;
};

À la base, un PVector est juste un moyen pratique de stocker deux valeurs (ou trois, comme nous le verrons dans les exemples 3D). Et donc:

    var x = 100;
    var y = 100;
    var xSpeed = 1;
    var ySpeed = 3.3;   

devient:

    var position = new PVector(100,100);
    var velocity = new PVector(1,3.3);  

Maintenant que nous avons deux objets vectoriels (position et vitesse), nous sommes prêts à implémenter l’algorithme de mouvement position = position + vitesse. Sans vecteurs, nous avions :

    x = x + xSpeed;
    y = y + ySpeed; 

Dans un monde idéal, on écrirait maintenant:

    position = position + vitesse;  

Toutefois, en JavaScript, l’opérateur d’addition + est réservé aux valeurs primitives (nombres, chaînes de caractères). Dans certains langages de programmation, les opérateurs peuvent être “surchargés”, mais pas en JavaScript. Heureusement pour nous, l’objet PVector comprend des méthodes pour les opérations mathématiques courantes, comme add().

Addition de vecteurs

Avant de continuer à étudier l’objet PVector et sa méthode add(), examinons l’addition de vecteurs en utilisant la notation que l’on trouve dans les manuels de mathématiques et de physique.

Les vecteurs sont généralement écrits en caractères gras ou avec une flèche en haut. Dans le cadre de ces leçons, pour distinguer un vecteur d’un scalaire (le scalaire fait référence à une valeur unique, telle qu’un nombre entier ou un nombre à virgule flottante), nous utiliserons la notation fléchée :

  • Vecteur : $\vec{u}$

  • Scalaire : $x$

Si $\vec{u} = (5,2)$ et $\vec{v} = (3,4)$ $\vec{w} = \vec{u} + \vec{v} = (8,6)$

Maintenant que nous savons comment additionner deux vecteurs, nous pouvons examiner comment l’addition est implémentée dans l’objet PVector lui-même. Écrivons une méthode appelée add() qui prend un autre objet PVector comme argument, et ajoute simplement les composantes x et y ensemble.

    var Vector = function(x, y) {
        this.x = x;
        this.y = y;
    };
    
    Vector.prototype.add = function(v) {
        this.y = this.y + v.y;
        this.x = this.x + v.x;
    };

Maintenant que nous voyons comment add() est écrit à l’intérieur de PVector, nous pouvons revenir à notre exemple de balle rebondissante avec son algorithme de position + vitesse et implémenter l’addition vectorielle :

    position.add(vitesse);

Et maintenant, nous sommes prêts à réécrire l’exemple de la balle rebondissante en utilisant l’objet PVector ! Jetez un coup d’œil au code et notez les différences par rapport à la version précédente.

    // Adapted from Dan Shiffman, natureofcode.com
    
    var position = new PVector(100, 100);
    var velocity = new PVector(2, 5);
    
    var draw = function() {
        background(255, 255, 255);
        
        position.add(velocity);
        
        // We still sometimes need to refer to the individual components of a PVector and can do so using the dot syntax: location.x, velocity.y, etc.
        if ((position.x > width) || (position.x < 0)) {
            velocity.x = velocity.x * -1;
        }
        if ((position.y > height) || (position.y < 0)) {
            velocity.y = velocity.y * -1;
        }
        
        noStroke();
        fill(179, 179, 179);
        ellipse(position.x, position.y, 16, 16);
    };
    

Nous devons noter un aspect important de la transition ci-dessus vers la programmation avec des vecteurs. Même si nous utilisons des objets PVector pour décrire deux valeurs - les x et y de la position et les x et y de la vitesse - nous devons souvent faire référence aux composantes x et y de chaque PVector individuellement. Lorsque nous voulons dessiner un objet dans ProcessingJS, nous ne pouvons pas le coder comme ceci :

    ellipse(position, 16, 16);

La fonction ellipse() ne permet pas d’utiliser un vecteur PVector comme argument. Une ellipse ne peut être dessinée qu’à l’aide de deux valeurs scalaires, une coordonnée x et une coordonnée y. Nous devons donc creuser dans l’objet PVector et en extraire les composantes x et y en utilisant la notation par points orientée objet :

    ellipse(position.x, position.y, 16, 16);

Le même problème se pose lorsqu’il s’agit de vérifier si le cercle a atteint le bord de la fenêtre, et nous devons accéder aux composantes individuelles des deux vecteurs : position et vitesse.

    if ((position.x > width) || (position.x < 0)) {
        vitesse.x = vitesse.x * -1;
    }

Vous pourriez être quelque peu déçu. Après tout, ce passage à l’utilisation de vecteurs peut sembler au départ avoir rendu le code plus compliqué que la version originale. Bien qu’il s’agisse d’une critique parfaitement raisonnable et valide, il est important de comprendre que nous n’avons pas encore pleinement réalisé la puissance de la programmation avec des vecteurs. Regarder une simple balle rebondissante et n’implémenter que l’addition de vecteurs n’est que la première étape.

Au fur et à mesure que nous avançons dans un monde plus complexe d’objets multiples et de forces multiples (que nous présenterons bientôt), les avantages de PVector deviendront plus évidents. Continuez comme ça !

Vecteurs en mathématiques

L’addition n’était vraiment que la première étape. Il existe de nombreuses opérations mathématiques qui sont couramment utilisées avec les vecteurs. Vous trouverez ci-dessous une liste complète des opérations disponibles sous forme de fonctions dans l’objet PVector de ProcessingJS. Nous allons maintenant passer en revue quelques-unes des principales opérations. Au fur et à mesure que nos exemples deviendront plus sophistiqués dans les sections suivantes, nous continuerons à révéler les détails d’autres fonctions.

  • add() — add vectors

  • sub() — substract vectors

  • mult() — scale the vector with multiplication

  • div() — scale the vector with division

  • mag() — calculate the magnitude of a vector

  • normalize() — normalize the vector to a unit length of 1

  • limit() — limit the magnitude of a vector

  • heading2D() — the 2D heading of a vector expressed as an angle

  • dist() — the Euclidean distance between two vectors (considered as points)

  • angleBetween() — find the angle between two vectors

  • dot() — the dot product of two vectors

  • cross() — the cross product of two vectors (only relevant in three dimensions)

Ayant déjà abordé l’addition, commençons par la soustraction. Celle-ci n’est pas si difficile : il suffit de remplacer le signe plus par un signe moins !

$\vec{w} = \vec{u} - \vec{v} = (5,2) - (3,4) = (2,-2)$

Dans la classe PVector:

    PVector.prototype.sub = function(vector2) {
        this.x = this.x - vector2.x;
        this.y = this.y - vector2.y;
    };
    

Exemple avec la différence entre 2 points : la position de la souris et le centre de l’écran:

    // Adapted from Dan Shiffman, natureofcode.com
    
    mouseMoved = function() {
        background(255, 255, 255);
        // Two PVectors, one for the mouse location and one for the center of the window
        var mouse  = new PVector(mouseX, mouseY);
        var center = new PVector(width/2, height/2);
        // PVector subtraction!
        mouse.sub(center);
        
        // Draw a line to represent the vector - 
        // Simplify drawing it by first translating to center
        // and drawing the line from there
        pushMatrix();
        translate(width/2, height/2);
        stroke(255, 0, 0);
        strokeWeight(3);
        line(0, 0, mouse.x, mouse.y);
        popMatrix();
    };
    

Multiplication de vecteurs

Pour passer à la multiplication, nous devons penser un peu différemment. Lorsque nous parlons de multiplier un vecteur, nous parlons généralement de mettre à l’échelle un vecteur. Si nous voulions mettre à l’échelle un vecteur à deux fois sa taille ou à un tiers de sa taille (en laissant sa direction inchangée), nous dirions : “Multipliez le vecteur par 2” ou “Multipliez le vecteur par 1/3”. Notez que nous multiplions un vecteur par un scalaire, un nombre unique, et non par un autre vecteur.

Pour mettre un vecteur à l’échelle, nous multiplions chaque composante (x et y) par un scalaire.

$\vec{w} = \vec{u} * n$

Peut être écrit:

$w_x = u_x *n$

$w_y = u_y *n$

Dans la classe PVector:

    PVector.prototype.mult = function(n) {
        this.x = this.x * n;
        this.y = this.y * n;
    }
    // on utilise la fonction mult():
    var u = new PVector(-3,7);
    // This PVector is now three times the size and is equal to (-9,21).
    u.mult(3);

Voici l’exemple de tout à l’heure, mais nous multiplions le vecteur par 0,5 à chaque fois, pour qu’il soit mis à l’échelle de moitié :

    // Adapted from Dan Shiffman, natureofcode.com
    
    mouseMoved = function() {
        background(255, 255, 255);
        // Two PVectors, one for the mouse location and one for the center of the window
        var mouse  = new PVector(mouseX, mouseY);
        var center = new PVector(width/2, height/2);
        // PVector subtraction!
        mouse.sub(center);
        
        mouse.mult(0.5);
        
        // Draw a line to represent the vector - 
        // Simplify drawing it by first translating to center
        // and drawing the line from there
        resetMatrix();
        translate(width/2, height/2);
        stroke(255, 0, 0);
        strokeWeight(3);
        line(0, 0, mouse.x, mouse.y);
    };

Au lieu de multiplier par 0,5 ci-dessus, nous aurions également pu diviser par 2. La division fonctionne comme la multiplication : il suffit de remplacer le signe de multiplication (astérisque) par le signe de division (barre oblique). C’est ainsi que la méthode div est implémentée en interne :

    PVector.prototype.div = function(n) {
        this.x = this.x / n;
        this.y = this.y / n;
    }
    // on utilise div():
    var u = new PVector(8, -4);
    u.div(2);
    

Vecteurs: magnitude et normalisation

La multiplication et la division, comme nous venons de le voir, sont des moyens par lesquels la longueur du vecteur peut être modifiée sans affecter la direction. Vous vous demandez peut-être : “OK, alors comment puis-je savoir quelle est la longueur d’un vecteur ? Je connais les composantes (x et y), mais quelle est la longueur (en pixels) de la flèche réelle ?”. Comprendre comment calculer la longueur (également appelée magnitude) d’un vecteur est incroyablement utile et important.

Remarquez dans le schéma ci-dessus comment le vecteur, dessiné sous forme de flèche et de deux composantes (x et y), crée un triangle rectangle. Les côtés sont les composantes et l’hypoténuse est la flèche elle-même. Nous avons beaucoup de chance d’avoir ce triangle rectangle, car il était une fois un mathématicien grec nommé Pythagore qui a mis au point une jolie formule pour décrire la relation entre les côtés et l’hypoténuse d’un triangle rectangle.

Le théorème de Pythagore est le suivant : $a^2 + b^2 = c^2$

La magnitude $|\vec{v}| = \sqrt{v_x^2 + v_y^2}$

Dans la classe PVector:

    PVector.prototype.mag = function() {
        return sqrt(this.x*this.x + this.y*this.y);
    };

L’exemple suivant permet de visualiser la magnitude d’un vecteur avec une barre en haut :

    // Adapted from Dan Shiffman, natureofcode.com
    
    mouseMoved = function() {
        background(255, 255, 255);
        resetMatrix();
        
        // Two PVectors, one for the mouse location and
        //  one for the center of the window
        var mouse  = new PVector(mouseX, mouseY);
        var corner = new PVector(width/2, height/2);
        // PVector subtraction!
        mouse.sub(corner);
        
        var m = mouse.mag();
        fill(0, 0, 0);
        rect(0, 0, m, 10);
        
        // Draw a line to represent the vector.
        translate(width/2, height/2);
        stroke(255, 0, 0);
        strokeWeight(3);
        line(0, 0, mouse.x, mouse.y);
    };

Le calcul de la magnitude d’un vecteur n’est qu’un début. La fonction de magnitude ouvre la porte à de nombreuses possibilités, dont la première est la normalisation.

La normalisation est le processus qui consiste à rendre quelque chose “standard” ou, en fait, “normal”. Dans le cas des vecteurs, supposons pour l’instant qu’un vecteur standard a une longueur de 1. Normaliser un vecteur, c’est donc prendre un vecteur de n’importe quelle longueur et, en le gardant orienté dans la même direction, changer sa longueur en 1, le transformant en ce qu’on appelle un vecteur unitaire.

Comme il décrit la direction d’un vecteur sans tenir compte de sa longueur, il est utile d’avoir le vecteur unitaire à portée de main. Nous verrons que cela s’avère pratique lorsque nous commencerons à travailler avec les forces dans la section suivante. Pour tout vecteur donné $\vec{u}$ son vecteur unitaire (écrit comme $\hat{u}$ u avec, chapeau) se calcule comme suit :

$\hat{u} = \frac{\vec{u}}{|\vec{u}|}$

En d’autres termes, pour normaliser un vecteur, il suffit de diviser chaque composante par sa magnitude. C’est assez intuitif. Disons qu’un vecteur a une longueur de 5. 5 divisé par 5 est égal à 1. Donc, en regardant notre triangle rectangle, nous devons réduire l’hypoténuse en la divisant par 5. Dans ce processus, les côtés diminuent, divisés par 5 également.

En code:

    PVector.prototype.normalize = function() {
        var m = this.mag();
        this.div(m);
    };

Voici un programme dans lequel nous normalisons toujours le vecteur qui représente la position de la souris à partir du centre (puis nous le multiplions pour pouvoir le voir, car 1 pixel est minuscule !) :

    // Adapted from Dan Shiffman, natureofcode.com
    
    mouseMoved = function() {
        background(255, 255, 255);
        
        // Two PVectors, one for the mouse location and
        //  one for the center of the window
        var mouse  = new PVector(mouseX, mouseY);
        var corner = new PVector(width/2, height/2);
        // PVector subtraction!
        mouse.sub(corner);
        
        // In this example, after the vector is normalized, it is multiplied by 50 so that it is viewable onscreen. Note that no matter where the mouse is, the vector will have the same length (50) due to the normalization process.
        mouse.normalize();
        mouse.mult(50);
        
        // Draw a line to represent the vector.
        resetMatrix();
        translate(width/2, height/2);
        stroke(255, 0, 0);
        strokeWeight(3);
        line(0, 0, mouse.x, mouse.y);
    };

Le mouvement avec les vecteurs

Toutes ces mathématiques vectorielles semblent être quelque chose que nous devrions connaître, mais pourquoi ? En quoi cela va-t-il nous aider à écrire du code ? La vérité est que nous devons faire preuve d’un peu de patience. Il faudra un certain temps avant que les merveilles de l’utilisation de la classe PVector ne se révèlent pleinement.

C’est en fait un phénomène courant lors de l’apprentissage d’une nouvelle structure de données. Par exemple, lorsque vous apprenez à connaître un tableau, il peut sembler beaucoup plus difficile d’utiliser un tableau que d’avoir plusieurs variables pour représenter plusieurs choses. Mais ce plan s’effondre rapidement lorsque vous avez besoin de cent, mille ou dix mille choses.

La même chose peut être vraie pour PVector. Ce qui peut sembler être un surcroît de travail aujourd’hui sera rentabilisé plus tard, et de manière très satisfaisante. Et vous n’aurez pas à attendre trop longtemps, car votre récompense viendra dans le prochain chapitre.

Vitesse

Pour l’instant, cependant, nous voulons nous concentrer sur la simplicité. Que signifie programmer un mouvement en utilisant des vecteurs ? Nous en avons vu les prémices dans l’exemple de la balle rebondissante. Un objet à l’écran a une position (où il se trouve à tout moment) ainsi qu’une vitesse (instructions sur la façon dont il doit se déplacer d’un moment à l’autre). La vélocité/vitesse est ajoutée à la position :

    // on ajoute la vitesse a la position
    position.add(vitesse);
    // on dessine l'objet
    ellipse(position.x, position.y, 16, 16);

Dans l’exemple de la balle rebondissante, tout ce code se trouvait dans la fonction draw de ProcessingJS. Ce que nous voulons faire maintenant, c’est encapsuler toute la logique du mouvement dans un objet. De cette façon, nous pouvons créer une base pour la programmation d’objets mobiles dans tous nos programmes ProcessingJS.

Dans ce cas, nous allons créer un objet Mover générique qui décrira un objet se déplaçant à l’écran. Et donc nous devons considérer les deux questions suivantes :

  • Quelle data contient un objet Mover?

  • Quelles fonctionnalités contient un objet Mover?

Un objet Mover possède deux données : la position et la vélocité, qui sont toutes deux des objets PVector. Nous pouvons commencer par écrire la fonction du constructeur qui initialise ces propriétés à des valeurs aléatoires appropriées :

    var Mover = function() {
        this.position = new PVector(random(width), random(height));
        this.velocity = new PVector(random(-2, 2), random(-2, 2));
    };

Sa fonctionnalité est tout aussi simple. Le Mover doit se déplacer et il doit être vu. Nous implémenterons ces besoins sous forme de méthodes nommées update() et display(). Nous placerons tout notre code de logique de mouvement dans update() et dessinerons l’objet dans display().

    Mover.prototype.update = function() {
        this.position.add(this.velocity);
    };
    
    Mover.prototype.display = function() {
        stroke(0);
        strokeWeight(2);
        fill(127);
        ellipse(this.position.x, this.position.y, 48, 48);
    };

Si la programmation orientée objet vous est totalement inconnue, un aspect de ce chapitre peut sembler un peu déroutant. Après tout, nous avons passé le début de ce chapitre à parler de PVector. L’objet PVector est le modèle pour créer l’objet position et l’objet vélocité. Alors que font-ils à l’intérieur d’un autre objet, l’objet Mover ? En fait, c’est la chose la plus normale qui soit. Un objet est simplement quelque chose qui contient des données (et des fonctionnalités). Ces données peuvent être des nombres, des chaînes de caractères, des tableaux ou d’autres objets ! Nous le verrons maintes et maintes fois dans ce cours. Par exemple, dans le tutoriel sur les particules, nous allons écrire un objet pour décrire un système de particules. Cet objet ParticleSystem aura comme données un tableau d’objets Particle... et chaque objet Particle aura comme données plusieurs objets PVector !

Terminons l’objet Mover en incorporant une fonction pour déterminer ce que l’objet doit faire lorsqu’il atteint le bord de la fenêtre. Pour l’instant, faisons quelque chose de simple, et faisons en sorte qu’il s’enroule autour des bords :

    Mover.prototype.checkEdges = function() {
        
        if (this.position.x > width) {
            this.position.x = 0;
        } 
        else if (this.position.x < 0) {
            this.position.x = width;
        }
        
        if (this.position.y > height) {
            this.position.y = 0;
        } 
        else if (this.position.y < 0) {
            this.position.y = height;
        }
    };

Maintenant que l’objet Mover est terminé, nous pouvons voir ce que nous devons faire dans notre programme principal. Nous commençons par déclarer et initialiser une nouvelle instance de Mover :

    var mover = new Mover();
    // puis nous appelons les fonctions dans draw()
    draw = function() {
        background(255, 255, 255);
        
        mover.update();
        mover.checkEdges();
        mover.display(); 
    };

Fonctions statiques VS. méthodes d’instance

Nous devons aborder un autre aspect assez important du travail avec les vecteurs et l’objet PVector : la différence entre l’utilisation de fonctions statiques et de méthodes d’instance. Oublions les vecteurs pour un instant et regardons le code suivant :

    var x = 0;
    var y = 5;
    x = x + y;

Plutôt simple, non ? x a la valeur 0, nous lui ajoutons y, et maintenant x est égal à 5. Nous pourrions écrire le code correspondant assez facilement en nous basant sur ce que nous avons appris sur PVector.

    var v = new PVector(0,0);
    var u = new PVector(4,5);
    v.add(u);

Le vecteur v a la valeur de (0,0), nous lui ajoutons u, et maintenant v est égal à (4,5). Facile, non ?

Voyons un autre exemple de mathématiques simples :

    var x = 0;
    var y = 5;
    var z = x + y;

x a la valeur 0, nous lui ajoutons y et stockons le résultat dans une nouvelle variable z. La valeur de x ne change pas dans cet exemple, et celle de y non plus ! Ce point peut sembler trivial et assez intuitif lorsqu’il s’agit d’opérations mathématiques avec des nombres. Cependant, ce n’est pas si évident avec les opérations mathématiques dans PVector. Essayons d’écrire le code sur la base de ce que nous savons jusqu’à présent.

    var v = new PVector(0,0);
    var u = new PVector(4,5);
    var w = v.add(u); // Ceci n'est pas correct

Ce qui précède peut sembler être une bonne supposition, mais ce n’est pas la façon dont l’objet PVector fonctionne. Si nous regardons la définition de add()...

    PVector.prototype.add = function(v) {
        this.x = this.x + v.x;
        this.y = this.y + v.y;
    };

...nous voyons que ce code n’atteint pas notre objectif. Premièrement, il ne renvoie pas un nouveau PVector (il n’y a pas d’instruction de retour) et deuxièmement, il modifie la valeur du PVector sur lequel il est appelé. Afin d’ajouter deux objets PVector ensemble et de retourner le résultat comme un nouveau PVector, nous devons utiliser la fonction “static” add().

Une fonction “statique” est une fonction qui est définie sur un objet, mais qui ne modifie pas les propriétés de l’objet. Alors pourquoi la définir sur l’objet ? En général, elle a quelque chose à voir avec l’objet, il est donc logique de l’y rattacher. L’objet est traité comme un espace de noms. Par exemple, toutes les fonctions statiques de PVector effectuent une sorte de manipulation sur des objets PVector passés et renvoient toujours une valeur. Nous pourrions également définir ces fonctions de manière globale, mais de cette façon, nous évitons les fonctions globales et disposons de meilleurs moyens de regrouper les fonctionnalités connexes.

Faisons un contraste. Voici comment nous utilisons la méthode d’instance add() :

    var w = PVector.add(u, v);

Si nous n’avions pas enregistré le résultat de cette fonction dans une variable, cette ligne de code serait inutile, car la version statique ne modifie pas les objets eux-mêmes. Les fonctions statiques de PVector nous permettent d’effectuer des opérations mathématiques génériques sur les objets PVector sans avoir à ajuster la valeur de l’un des PVectors d’entrée.

Voilà comment on aurait écrit la fonction add():

    PVector.add = function(v1, v2) {
        var v3 = new PVector(v1.x + v2.x, v1.y + v2.y);
        return v3;
    };

Il y a plusieurs différences ici :

  • Nous définissons la fonction directement sur l’objet, et non sur son prototype.

  • Nous n’accédons jamais au mot-clé this à l’intérieur de la fonction.

  • Nous retournons une valeur à partir de la fonction

L’objet PVector possède des versions statiques de add(), sub(), mult() et div(). Il possède également des fonctions statiques supplémentaires qui n’existent pas en tant que méthodes d’instance, comme angleBetween(), dot() et cross(). Nous serons amenés à utiliser ces fonctions au fur et à mesure que nous créerons des programmes avec PVector.

Mouvement interactif avec les vecteurs

Pour terminer cette section, essayons quelque chose d’un peu plus complexe et de beaucoup plus utile. Nous allons calculer dynamiquement l’accélération d’un objet selon la règle : l’objet accélère en direction de la souris.

Chaque fois que nous voulons calculer un vecteur sur la base d’une règle ou d’une formule, nous devons calculer deux choses : la magnitude et la direction. Commençons par la direction. Nous savons que le vecteur accélération doit pointer de la position de l’objet vers la position de la souris. Disons que l’objet est situé au point (x,y) et la souris à (mouseX,mouseY).

$dx = mouseX - x$

$dy = mouseY - y$

Réécrivons ce qui précède en utilisant la syntaxe PVector. En supposant que nous sommes dans la définition de l’objet Mover et que nous avons donc accès à la position du PVector de l’objet, nous avons alors :

var mouse = new PVector(mouseX, mouseY);
// Look! We're using the static sub() 
// because we want a completely new PVector
var dir = PVector.sub(mouse, position);

Nous avons maintenant un PVector qui pointe depuis la position du mover jusqu’à la souris. Si l’objet devait réellement accélérer en utilisant ce vecteur, il apparaîtrait instantanément à la position de la souris. Cela ne permet pas de faire une bonne animation, bien sûr, et ce que nous voulons faire maintenant, c’est décider à quelle vitesse l’objet doit accélérer vers la souris.

Afin de définir la magnitude (quelle qu’elle soit) de notre vecteur PV d’accélération, nous devons d’abord ?????????? ce vecteur directionnel. C’est vrai, vous l’avez dit. Normaliser. Si nous pouvons réduire le vecteur à son vecteur unitaire (de longueur un), nous avons alors un vecteur qui nous indique la direction et qui peut facilement être mis à l’échelle à n’importe quelle valeur. Un multiplié par n’importe quoi est égal à n’importe quoi.

    var anything = ??;
    dir.normalize();
    dir.mult(anything);

Pour résumer, nous suivons les étapes suivantes :

  • Calculer un vecteur qui pointe de l’objet vers la position cible (souris).

  • Normaliser ce vecteur (en réduisant sa longueur à 1)

  • Mettre à l’échelle ce vecteur à une valeur appropriée (en le multipliant par une certaine valeur)

  • Assigner ce vecteur à l’accélération

Voici à quoi ressemble le programme, avec ces étapes entièrement implémentées :

    // Adapted from Dan Shiffman, natureofcode.com
    
    var Mover = function() {
        this.position = new PVector(width/2, height/2);
        this.velocity = new PVector(0, 0);
        this.acceleration = new PVector(0, 0);
    };
    
    Mover.prototype.update = function() {
        var mouse = new PVector(mouseX, mouseY);
        var dir = PVector.sub(mouse, this.position);
        dir.normalize();
        dir.mult(0.5);
        this.acceleration = dir;
        this.velocity.add(this.acceleration);
        this.velocity.limit(5);
        this.position.add(this.velocity);
    };
    
    Mover.prototype.display = function() {
        stroke(0);
        strokeWeight(2);
        fill(127);
        ellipse(this.position.x, this.position.y, 48, 48);
    };
    
    Mover.prototype.checkEdges = function() {
        
        if (this.position.x > width) {
            this.position.x = 0;
        } else if (this.position.x < 0) {
            this.position.x = width;
        }
        
        if (this.position.y > height) {
            this.position.y = 0;
        } else if (this.position.y < 0) {
            this.position.y = height;
        }
    };
    
    var mover = new Mover();
    
    var draw = function() {
        background(255, 255, 255);
        
        mover.update();
        mover.checkEdges();
        mover.display(); 
    };

Vous vous demandez peut-être pourquoi le cercle ne s’arrête pas lorsqu’il atteint la cible. Il est important de noter que l’objet en mouvement ne sait pas qu’il doit s’arrêter à une destination ; il sait seulement où se trouve la destination et essaie de s’y rendre aussi vite que possible. En allant aussi vite que possible, il dépassera inévitablement la position et devra faire demi-tour, pour aller à nouveau aussi vite que possible vers la destination, la dépasser à nouveau, et ainsi de suite. Restez à l’écoute ; dans les sections suivantes, nous apprendrons comment programmer un objet pour qu’il arrive à une position (ralentissement à l’approche).

Cet exemple est remarquablement proche du concept d’attraction gravitationnelle (dans lequel l’objet est attiré par la position de la souris). L’attraction gravitationnelle sera traitée plus en détail dans la section suivante. Toutefois, il manque ici une chose : la force de la gravité (l’ampleur de l’accélération) est inversement proportionnelle à la distance. Cela signifie que plus l’objet est proche de la souris, plus il accélère.

Voyons à quoi ressemblerait cet exemple avec un tableau de movers (plutôt qu’un seul).

    // Adapted from Dan Shiffman, natureofcode.com
    
    var Mover = function() {
        this.position = new PVector(random(width), random(height));
        this.velocity = new PVector(0, 0);
        this.acceleration = new PVector(0, 0);
    };
    
    Mover.prototype.update = function() {
        var mouse = new PVector(mouseX, mouseY);
        var dir = PVector.sub(mouse, this.position);
        dir.normalize();
        dir.mult(0.2);
        this.acceleration = dir;
        this.velocity.add(this.acceleration);
        this.velocity.limit(5);
        this.position.add(this.velocity);
    };
    
    Mover.prototype.display = function() {
        stroke(0);
        strokeWeight(2);
        fill(127);
        ellipse(this.position.x, this.position.y, 10, 10);
    };
    
    Mover.prototype.checkEdges = function() {
        
        if (this.position.x > width) {
            this.position.x = 0;
        } 
        else if (this.position.x < 0) {
            this.position.x = width;
        }
        
        if (this.position.y > height) {
            this.position.y = 0;
        } 
        else if (this.position.y < 0) {
            this.position.y = height;
        }
    };
    
    var movers = [];
    
    for (var i = 0; i < 20; i++) {
        movers[i] = new Mover(); 
    }
    
    draw = function() {
        background(255, 255, 255);
        for (var i = 0; i < movers.length; i++) {
            movers[i].update();
            movers[i].display(); 
        }
    };
    
    
av logo

© 2021 Gilles Avraam. Tous droits réservés.