Le mythe de Turtlestein

Notre tortue a la classe

Pour créer notre tortue, nous allons créer une classe pour un approche orienté-objet, plus simple à coder. Il s'agit là de la partie la plus longue de notre projet.

Il existe plusieurs façons de déclarer des classes en JavaScript, j'adopterai la plus proche de celle de python. On commence donc par la déclaration du constructeur :

class Tortue { constructor() { // On définit les centres de l'écran this.center_x = Math.floor(screen.width / 2); this.center_y = Math.floor(screen.height / 2); // On pose la tortue au centre de l'écran this.x = this.center_x; this.y = this.center_y; } }

A l'aide de ces déclarations, la création d'une tortue permettra :

Le mot clé this permet d'identifier l'instance de classe, c'est à dire, la variable que l'on va déclarer. On peut ainsi définir des "sous-variables" propres à chaque variables. C'est d'ailleurs pourquoi on ne rajoute pas le mot clé var, this est une variable pour l'objet.

Apprenons lui à dessiner

Abordés à la page précédente, nous allons utiliser les chemins pour faire dessiner notre tortue.

Ainsi, il sera utile de rajouter une variable d'état à notre contructeur permettant de savoir si notre tortue est en train de dessiner.

Première fonction : pendown

On crée une fonction toute simple qui crée un nouveau chemin et actualise l'état de la tortue. Dans le module turtle, pendown a plusieurs équivalents syntaxiques, on les rajoute donc aussi à notre classe :

class Tortue { constructor() { // On définit les centres de l'écran this.center_x = Math.floor(screen.width / 2); this.center_y = Math.floor(screen.height / 2); // On pose la tortue au centre de l'écran this.x = this.center_x; this.y = this.center_y; // Variable d'état this.is_drawing = false; } pendown() { this.new_shape = new Path2D(); // Premier point du dessin this.new_shape.moveTo(Math.round(this.x), Math.round(this.y)); this.is_drawing = true; } // Equivalents pd() { return this.pendown(); } down() { return this.pendown(); } }

On remarquera qu'il est necessaire de déplacer le chemin au début du tracé pour avoir un point de départ.

Un problème nous vient alors : Il faut stocker tout nos chemins si nous voulons les redessiner après.

Deuxième fonction : penup

L'interêt de cette fonction est de stocker le chemin en cours dans un tableau de chemins et d'arrêter de dessiner.

On commence donc par créer un tableau de chemins dans le constructeur :

// Chemins à stocker this.all_shapes = Array();

Attention cependant !

JavaScript gère la plupart de ses variables à l'aide de references.

Or si l'on ajoute le chemin directement à notre tableau, il risque de n'ajouter que la reference pour économiser de la place en mémoire.

Ainsi, modifier la variable d'origine modifirais nos chemins que nous avons stocké, ce qui n'est pas souhaitable pour faire de beaux dessins.

penup() { // On ferme le chemin this.new_shape.closePath(); // On ajoute une copie du chemin crée pour éviter les bugs de reference this.all_shapes.push(new Path2D(this.new_shape)); this.is_drawing = false; } // Equivalents pu() { return this.penup(); } up() { return this.penup(); }

Un autre problème vient alors s'ajouter, les chemins n'ont pas de couleurs qui leur sont propres. Plusieurs solutions sont alors possibles, on pourrait ajouter des tableaux au tableau de chemins à stocker afin de récuperer la couleur...

Mais JavaScript peut nous offrir des solutions plus élégantes, qui nous permet même de généraliser à autant de paramètres que l'on veut, les hérédités de classes :

// Nouvel objet : class CheminTortue extends Path2D { constructor(old) { super(old); if (old !== undefined) { this.color = old.color; this.width = old.width } else { this.color = "#000"; this.width = 2; } } }

On remplace donc les Path2D de notre code par notre nouvelle classe : CheminTortue

Il est d'abord nécessaire d'initialiser un objet Path2D avec l'appel de super()

Faire avancer la tortue : forward et backward

Il est désormais l'heure d'entrer dans le monde merveilleux de la trigonométrie !

Il faut tout d'abord rajouter un angle à notre tortue. On ajoute ainsi la variable angle au constructeur. Puis on ajoute notre fonction pour avancer.

Seulement, sur un repère normal, l'axe des ordonnées est orienté vers le haut, or sur le canvas, c'est l'inverse. Il faut donc appliquer l'opposé de l'angle pour le mouvement.

Formules trigonométriques de base :
cos(-a) = cos(a)
sin(-a) = -sin(a)

On peut donc ajouter les fonctions qui permettent d'avancer :

forward(len) { // On change de position this.x += len * Math.cos(Math.PI * (this.angle / 180)); this.y -= len * Math.sin(Math.PI * (this.angle / 180)); // On dessine la ligne if (this.is_drawing) this.new_shape.lineTo(Math.round(this.x), Math.round(this.y)); } // Equivalents et opposés fd(len) { return this.forward(len); } backward(len) { this.angle = 180 + this.angle; this.forward(len); this.angle = this.angle - 180; } bk(len) { return this.backward(len); } back(len) { return this.backward(len); } goto(x, y) { this.x = x; this.y = y; // On dessine la ligne if (this.is_drawing) this.new_shape.lineTo(Math.round(this.x), Math.round(this.y)); }

Faire tourner la tortue

De la même manière, nous pouvons créer une fonction pour faire tourner notre tortue vers la gauche et créer l'opposé pour tourner vers la droite.

Tourner vers la gauche, c'est tourner dans le sens trigonométrique direct, c'est à dire, augmenter l'angle.

On pourrait se limiter à ces fonctions :

left(angle) { this.angle = (this.angle + angle) % 360; } // Equivalents et opposés lt(angle) { return this.left(angle); } right(angle) { return this.left(-angle); } rt(angle) { return this.left(-angle); }

Sauf que ce modulo pourrait retourner un angle négatif (qui serait bon, bien evidemment). Mais nous preferrons des angles entre 0 et 360°.

C'est pourquoi nous allons utiliser ces fontions :

left(angle) { // Modulo toujours positif this.angle = (((this.angle + angle) % 360) + 360) % 360; } // Equivalents et opposés lt(angle) { return this.left(angle); } right(angle) { return this.left(-angle); } rt(angle) { return this.left(-angle); }

Afficher tout nos jolis dessins

Je ne detaillerai pas les fonctions permettant de changer la couleur et la taille du trait, ainsi que celles qui permettent d'effacer tout les dessins car elles sont assez simples. Il faut juste penser à recréer un nouveau chemin car le contexte (ce qui permet le rendu de l'image) de ne peux pas faire de chemins de largeurs ou couleurs différentes.

Code à ajouter :

color(clr) { if (this.has_moved) { this.new_shape.closePath(); this.all_shapes.push(new CheminTortue(this.new_shape)); this.new_shape = new CheminTortue(); this.new_shape.moveTo(Math.round(this.x), Math.round(this.y)); } if (this.is_drawing) { this.new_shape.color = clr; } this.has_moved = false; } width(wdt) { if (this.has_moved) { this.new_shape.closePath(); this.all_shapes.push(new CheminTortue(this.new_shape)); this.new_shape = new CheminTortue(); this.new_shape.moveTo(Math.round(this.x), Math.round(this.y)); } if (this.is_drawing) { this.new_shape.width = Math.floor(wdt); } this.has_moved = false; } reset() { this.clear(); this.color("#000"); this.width(2); this.x = this.center_x; this.y = this.center_y; this.angle = 0; } clear() { if (!this.is_drawing) { this.pendown(); } this.penup(); this.all_shapes = [new CheminTortue()]; // Vous verez pourquoi }

Vous noterez l'apparition de la variable has_moved, elle permet de pouvoir modifier l'épaisseur et la couleur sans créer de chemins supplémentaires.

Pour rafraichir notre écran, nous ferons une fonction qui ne nécéssite aucun appel histoire histoire que ce ne soit pas trop embettant.

Nous allons quand même nous permettre d'agir dessus.

Pour commencer, nous allons créer un "catalogue" de tortues : une liste contenant les références des tortues que nous déclarerons :

var tortues = new Array();

Ainsi, à la création d'une tortue, il faut rajouter deux lignes :

this.show_drawing = true; tortues.push(this);

On pourra donc modifier l'état d'une tortue pour rendre ses créations visibles ou non.

On désire maintenant s'occuper de rafraichir l'ecran. On souhaite également rajouter une couleur pour ce dernier.

On déclare alors la variable qui stockera la couleur à afficher au niveau global :

var background_color = "#fff"; // blanc par défaut

Puis on construit la fonction qui affiche les dessins.

Pour cela, on commence par un schéma de pensée, comment faut il faire ?

Il faut :

D'où :

// Pour rafraichir l'ecran function rafraichir() { context.fillStyle = background_color; context.fillRect(0, 0, screen.width, screen.height); for (tortue of tortues) { if (tortue.show_drawing) { for (chemins of tortue.all_shapes) { context.strokeStyle = chemins.color; context.lineWidth = chemins.width; context.stroke(chemins); } } } }

Un tel moteur nécessite cependant qu'il y ait toujours une couleur à prendre, et donc un chemin d'existant.

Toutes les déclarations de all_shapes deviennent donc :

this.all_shapes = [new CheminTortue()];

Il ne nous reste plus qu'à ajouter un taux de rafraichissement. Pour cela, on utilise les intervals :

var taux_de_rafraichissement = setInterval(rafraichir, 1);

Et voila !

Voici un résultat que l'on peut obtenir à partir d'un code simple :

background_color = "#000"; var maTortue = new Tortue(); var angle = 0; const SIDES = 6; function hexagonSpiral() { maTortue.pd(); for (var i = 0 ; i < 360 ; i++) { maTortue.color("hsl("+ (((i % SIDES) * (360 / SIDES))) +",75%,"+Math.round(100*i/360)+"%)"); maTortue.width(i/120 + 1); maTortue.forward(i); maTortue.left(Math.floor(360/SIDES) - 1); } maTortue.pu(); } function boucle() { angle = (angle + 1) % 360; maTortue.reset(); maTortue.right(angle); hexagonSpiral(); } setInterval(boucle, 5);

Passer à la suite du tutoriel


Notre code pour l'instant (apres quelques correctifs)

// Variables globales var screen = document.getElementById("screen"); var context = screen.getContext("2d"); var background_color = "#fff"; var tortues = new Array(); // Pour rafraichir l'ecran function rafraichir() { context.fillStyle = background_color; context.fillRect(0, 0, screen.width, screen.height); for (tortue of tortues) { if (tortue.show_drawing) { for (chemins of tortue.all_shapes) { context.strokeStyle = chemins.color; context.lineWidth = chemins.width; context.stroke(chemins); } } } } var taux_de_rafraichissement = setInterval(rafraichir, 1); // Nouvel objet : class CheminTortue extends Path2D { constructor(old) { super(old); if (old !== undefined) { this.color = old.color; this.width = old.width } else { this.color = "#000"; this.width = 2; } } } // Notre tortue class Tortue { constructor() { // On définit les centres de l'écran this.center_x = Math.floor(screen.width / 2); this.center_y = Math.floor(screen.height / 2); // On pose la tortue au centre de l'écran this.x = this.center_x; this.y = this.center_y; // Variable d'état this.is_drawing = false; this.show_drawing = true; this.has_moved = false; this.angle = 0; this.turtle_color = "black"; this.turtle_width = 2; // Chemins à stocker this.all_shapes = [new CheminTortue()]; tortues.push(this); } pendown() { this.new_shape = new CheminTortue(); this.new_shape.color = this.turtle_color; this.new_shape.width = this.turtle_width; // Premier point du dessin this.new_shape.moveTo(Math.round(this.x), Math.round(this.y)); this.is_drawing = true; this.has_moved = false; } // Equivalents pd() { return this.pendown(); } down() { return this.pendown(); } penup() { // On ferme le chemin this.new_shape.closePath(); // On ajoute une copie du chemin crée pour éviter les bugs de reference this.all_shapes.push(new CheminTortue(this.new_shape)); this.is_drawing = false; this.has_moved = false; } // Equivalents pu() { return this.penup(); } up() { return this.penup(); } color(clr) { if (this.has_moved) { this.new_shape.closePath(); this.all_shapes.push(new CheminTortue(this.new_shape)); this.new_shape = new CheminTortue(); this.new_shape.moveTo(Math.round(this.x), Math.round(this.y)); } if (this.is_drawing) { this.new_shape.color = clr; } this.has_moved = false; } width(wdt) { if (this.has_moved) { this.new_shape.closePath(); this.all_shapes.push(new CheminTortue(this.new_shape)); this.new_shape = new CheminTortue(); this.new_shape.moveTo(Math.round(this.x), Math.round(this.y)); } if (this.is_drawing) { this.new_shape.width = Math.floor(wdt); } this.has_moved = false; } reset() { this.clear(); this.color("#000"); this.width(2); this.x = this.center_x; this.y = this.center_y; this.angle = 0; } clear() { if (!this.is_drawing) { this.pendown(); } this.penup(); this.all_shapes = [new CheminTortue()]; } forward(len) { // On change de position this.x += len * Math.cos(Math.PI * (this.angle / 180)); this.y -= len * Math.sin(Math.PI * (this.angle / 180)); // On dessine la ligne if (this.is_drawing) { this.new_shape.lineTo(Math.round(this.x), Math.round(this.y)); this.has_moved = true; } } // Equivalents et opposés fd(len) { return this.forward(len); } backward(len) { this.angle = 180 + this.angle; this.forward(len); this.angle = this.angle - 180; } bk(len) { return this.backward(len); } back(len) { return this.backward(len); } goto(x, y) { this.x = x; this.y = y; // On dessine la ligne if (this.is_drawing) { this.new_shape.lineTo(Math.round(this.x), Math.round(this.y)); this.has_moved = true; } } left(angle) { // Modulo toujours positif this.angle = (((this.angle + angle) % 360) + 360) % 360; } // Equivalents et opposés lt(angle) { return this.left(angle); } right(angle) { return this.left(-angle); } rt(angle) { return this.left(-angle); } }