Dessiner avec l’API Canvas

Avant de commencer à dessiner, parlons un peu de la grille de pixels. Son point d’origine se trouve sur le coin supérieur gauche de l’écran. La hauteur de l’écran est l’axe des Y et sa largeur est l’axe des X.
A savoir également, l’axe des Y est inversé, le positif est vers le bas et le négatif vers le haut, ça peut paraître contre intuitif et mènera certainement à des erreurs.

L’API Canvas est relativement simple à prendre en main, tout ce qu’elle fait c’est dessiner des rectangle ou des chemins de lignes à des coordonnées précises. La complexité des formes dessinées dépend donc de comment l’API est utilisée.

Codons un peu

Reprenons notre code précédent en modifiant un peut quelques trucs.

 /**
 * DrawSystem
 * @description DrawSystem a pour rôle d'exploiter l'API Canvas et
 *              fournir des outils de création de formes complexes
 */
class DrawSystem {
    constructor(id, width, height, context) {
        // Création de l'élément Canvas
        this.canvas = document.createElement("canvas");
        // Création du contexte
        this.ctx = this.canvas.getContext(context || '2d');
        // Initialisation de l'élément Canvas
        this.canvas.id     = id     || "game"; // On lui donne un ID
        this.canvas.width  = width  || 500;    // Une largeur
        this.canvas.height = height || 500;    // Une hauteur
        // Injection de l'élément Canvas
        document.body.appendChild(this.canvas);
    }
}

Créer un moteur 2D demande pas mal d’organisation et il existe pour ça beaucoup de concept qui aide à cette organisation, notamment les System.
Les System ont un seul rôle spécifique et un seul, et il est important de bien réfléchir et savoir si ce qu’on fait ne peut pas être diviser en plusieurs system. Ce concept s’intègre dans un concept plus large, l’ECS, ou Entity Component System, mais on y reviendra.

Dans notre cas donc, on veut un système qui exploite l’API Canvas pour nous et qui dessine ce qu’on lui demande. Rajoutons donc une méthode pour créer des rectangles selon nos besoin :

    /**
     * rectangle
     * @description Méthode permettant de dessiner des carrés ou rectangles.
     */
    rectangle(x, y, w, h, options) {
        // On vérifie si l'option de remplissage est définie
        if(options.fill) {
            // Si c'est le cas, on applique une couleur
            this.ctx.fillStyle = options.fill.color;
        }

        // On vérifie si l'option de bordure est définie
        if(options.stroke) {
            // Si c'est le cas, on applique une couleur de bordure
            this.ctx.strokeStyle = options.stroke.color || 'black';
            // On peut définir la largeur du trait
            this.ctx.lineWidth = options.stroke.width || 5;
        }

        // Et on dessine le rectangle
        this.ctx.fillRect(x, y, w, h);
    }

Stroke permettra de configurer les bordures de notre rectangle pendant que Fill permettra de remplir le rectangle avec une couleur.
Rien de bien compliqué en somme.

Et pourquoi pas des images

Ajoutons une fonction permettant d’afficher une image, Canvas propose une méthode simple appelée drawImage :

    /**
     * image
     * @description Dessine une image donnée
     * @param src
     * @param x
     * @param y
     * @param w
     * @param h
     */
    image(src, x, y, w, h) {
        // On créé un élément image
        let image = new Image();
        
        // On défini une fonction onLoad, qui sera exécutée une
        // fois les données d'image chargées dans l'élément
        image.onload = () => {
            this.ctx.drawImage(image, x, y, w, h);
        };
        
        // On défini la source du fichier
        image.src = src;
    }

La méthode drawImage doit être paramétrée avec un élément Image(), c’est un conteneur qui va pouvoir recevoir une source d’image et contenir cette dernière. La source peut être une url ou une data:URL. On peut évidemment définir sa position sur le plan avec x et y, ainsi que sa largeur et hauteur.

Plus tard on verra comment prendre une image et n’afficher qu’une seule partie en définissant une zone voulue. Ce sera très utile pour créer une curseur d’animation.

La fonction onload définie dans le code permet au script de prendre le temps de charger correctement le fichier image et l’injecter dans son élément conteneur. Une fois chargé complètement, la fonction exécute drawImage pour dessiner l’image.

Gérer les ressources extérieures

Dans le prochain article j’écrirais un petit système simple qui nous permettra de charger et gérer une liste de ressources telle que les images.
En effet, si dans notre code actuel cela parait simple, dans un projet plus gros ça ne tiendrait pas la route, il faut pouvoir charger les images (et d’autre ressources) avant d’exécuter le DrawSystem. Imaginez le résultat si on devait afficher des dizaine d’images, cela fonctionnerait avec notre code actuel, mais qu’en est-il d’un code qui a besoin d’être répété plusieurs fois par seconde pour par exemple faire des animations, les images n’auraient pas le temps d’être chargées.

Il y a d’autre méthode permettant de dessiner des formes plus complexe, je vous invite d’ailleurs à lire la documentation de Canvas 😉

Pour conclure, je voudrais préciser une chose, ce que je compte présenter tout au long de ces articles ce n’est pas un tuto sur Canvas de A à Z, mais plutôt ma méthode et les résultats que j’obtiens en cherchant comment je pourrais arriver à faire un jeu vidéo. Pour parfaire vos connaissances et aller plus loin je vous conseil fortement de lire la documentation et les tutoriels officiel de Canvas juste là :
https://developer.mozilla.org/fr/docs/Tutoriel_canvas

Le code entier

/**
 * Une classe DrawSystem permet d'initialiser le canvas quand on veut
 * et rend le code réutilisable
 */
class DrawSystem {
    constructor(id, width, height, context) {
        // Création de l'élément Canvas
        this.canvas = document.createElement("canvas");
        // Création du contexte
        this.ctx = this.canvas.getContext(context || '2d');

        // Initialisation de l'élément Canvas
        this.canvas.id     = id     || "game"; // On lui donne un ID
        this.canvas.width  = width  || 500;    // Une largeur
        this.canvas.height = height || 500;    // Une hauteur

        // Injection de l'élément Canvas
        document.body.appendChild(this.canvas);
    }

    /**
     * image
     * @description Dessine une image donnée
     * @param src
     * @param x
     * @param y
     * @param w
     * @param h
     */
    image(src, x, y, w, h) {
        // On créé un élément image
        let image = new Image();

        // On défini une fonction onLoad, qui sera exécutée une
        // fois les données d'image chargées dans l'élément
        image.onload = () => {
            this.ctx.drawImage(image, x, y, w, h);
        };

        // On défini la source du fichier
        image.src = src;
    }

    /**
     * rectangle
     * @description Méthode permettant de dessiner des carrés ou rectangles.
     */
    rectangle(x, y, w, h, options) {
        // On vérifie si l'option de remplissage est définie
        if(options.fill) {
            // Si c'est le cas, on applique une couleur
            this.ctx.fillStyle = options.fill.color;
        }

        // On vérifie si l'option de bordure est définie
        if(options.stroke) {
            // Si c'est le cas, on applique une couleur de bordure
            this.ctx.strokeStyle = options.stroke.color || 'black';
            // On peut définir la largeur du trait
            this.ctx.lineWidth = options.stroke.width || 5;
        }

        // Et on dessine le rectangle
        this.ctx.fillRect(x, y, w, h);
    }
}