Exemple: une application MVC en Javascript avec un Canvas

Je vous propose de découvrir une implémentation du fameux pattern MVC en javascript. L’application suivante titrée Equipe Type permet d’ajouter des joueurs sur un terrain de football. Chaque joueur doit avoir un nom et peut se voir attribuer une note. L’affichage est effectué dans un canvas. L’utilisateur peut interagir avec les joueurs ajouter en les plaçant à sa convenance sur le terrain. Les fichiers sont relativement bien commentés et il vous sera sans nul doute facile de comprendre la logique de l’application.

Vous pouvez dès à présent tester Equipe Type sur cette page et ensuite télécharger les sources ici.

Voici une capture d’écran d’Equipe Type

Equipe Type en action

Equipe Type en action

et la structure des fichiers de cette modeste application.

structure_equipe_type

la structure d’équipe type

et les codes sources des principaux fichiers:

Tout d’abord la vue, qui a uniquement en charge l’affichage du terrain et des joueurs. Il reçoit des notifications du modèle Equipe, vous reconnaîtrez le pattern observer, et se met ainsi à jour.

view/Vue.js

function Vue(mController) {

    // model
    _this = this;
    monController = mController;

    // canvas
    canvas = null, ctx = null;
    // les modeles
    maillot = null, terrain = null;
    // test si Init();
    initTest = false;

    // on cache les input non nécessaires
    $("#addNote").hide();
    $("#addDelete").hide();
    $("#addMoreButton").hide();

    // les images
    imgMaillot = new Image();
    imgTerrain = new Image();
    imgTerrain.src = 'images/terrain_original_800_500.jpg';
    imgMaillot.src = 'images/maillot-france-50-50.png';
    imgTerrain.onload = function() {
        // quand l'image du terrain est chargée on initialise la vue
        initialisation();
    }

    function initialisation() {
        initTest = true;
        // le canvas
        canvas = document.getElementById("myCanvas");
        ctx = canvas.getContext("2d");

        // les objets
        maillot = new Maillot(imgMaillot.width, imgMaillot.height);
        terrain = new Terrain(imgTerrain.width, imgTerrain.height);
        
        // on effectue le premier affichage
        _this.update();
    }

    this.update = function(joueurs, id) {
        // on actualise l'affichage
        if (initTest == true) {
            nettoyerCanvas();
            afficherTerrain();
            if (joueurs) {
                afficherJoueurs(joueurs, id);
            }
        } else {
            initialisation();
        }
        // on actualise les boutons
        if (monController.getCurrentJoueurIndex() != null)
        {
            $("#addNote").show();
            $("#addDelete").show();

            if (monController.getCurrentJoueurIndexNote()) {
                $("#addNote").attr("value", "Modifier la Note");
            } else {
                $("#addNote").attr("value", "Noter");
            }
            $("#addButton").attr("value", "Modifier le Nom");
            $("#addMoreButton").show();
        }
        else
        {
            $("#addNote").hide();
            $("#addButton").attr("value", "Ajouter un Joueur");
            $("#addDelete").hide();
            $("#addMoreButton").hide();
        }
    }

    // les fonctions d'affichages
    var nettoyerCanvas = function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    var afficherTerrain = function() {
        ctx.drawImage(imgTerrain, 0, 0);
    }

    var afficherJoueurs = function(joueurs, id) {
        if (joueurs) {
            for (i = 0; i < joueurs.length; i++) {
                afficherUnJoueur(joueurs[i], i, id);
            }
        }
    }

    var afficherUnJoueur = function(joueur, i, id) {
        if (joueur) {

            ctx.drawImage(imgMaillot, joueur.x, joueur.y);
            
            // on encercle le joueur s'il est sélectionné
            if (i == id) {
                ctx.beginPath();
                ctx.strokeStyle = "cyan";
                ctx.lineWidth = 15;
                ctx.arc(joueur.x + maillot.w / 2, joueur.y + maillot.h / 2, maillot.w / 1.3, 0, 2 * Math.PI);
                ctx.stroke();
            }
            if (joueur.name != "") {
                ctx.fillStyle = "black";
                ctx.font = "14pt bold Arial";
                ctx.fillText(joueur.name, joueur.x, joueur.y + maillot.h + 20);
            }

            if (joueur.note != "") {
                ctx.fillStyle = "yellow";
                ctx.font = "15pt bold sans-serif";
                ctx.fillText(joueur.note, joueur.x + maillot.w, joueur.y + maillot.h / 2);
            }
        }
    }
    
}

Le modèle: models/equipe.js

Equipe = function() {

    this.observers = new ObserverList();
    // les variables
    this.joueurs = [];
    this.currentJoueurIndex = null;

    this.addJoueur = function(joueur) {
        this.joueurs.push(joueur);
        this.currentJoueurIndex = this.joueurs.length - 1;
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.getJoueurId = function(i) {
        return this.joueurs[i];
    }

    this.setCurrentJoueurIndex = function(id) {
        this.currentJoueurIndex = id;
        console.log("setCurrentJoueurIndex:" + id);
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.getCount = function() {
        return this.joueurs.length;
    }

    this.getCurrentJoueurIndex = function() {
        return this.currentJoueurIndex;
    }

    this.getJoueurs = function() {
        return this.joueurs;
    }

    this.modifyXYofOneJoueur = function(id, donnees) {
        this.joueurs[id].x = donnees.x;
        this.joueurs[id].y = donnees.y;
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.reset = function() {
        this.joueurs = [];
        this.currentJoueurIndex = null;
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

    this.removeAtIndex = function(i) {
        this.joueurs.splice(i, 1);
        this.notify(this.joueurs, this.currentJoueurIndex);
    }

};

Equipe.prototype.addObserver = function(observer) {
    this.observers.add(observer);
}

Equipe.prototype.removeObserver = function(observer) {
    this.observers.removeAt(this.observers.indexOf(observer, 0));
}

Equipe.prototype.notify = function(context, id) {
    var observerCount = this.observers.count();
    for (var i = 0; i < observerCount; i++) {
        this.observers.get(i).update(context, id);
    }
}

le Contrôleur, qui est interrogé par la vue pour obtenir des infos du modèle et qui informe le modèle des actions de l’utilisateur

controller/Controller.js

function Controller(mequipe) {

    monEquipe = mequipe;
    
    // gestion des actions et des clics
    var clicX, clicX;
    var drag = false;
    
    this.getCurrentJoueurIndex = function(){
        return monEquipe.getCurrentJoueurIndex();
    }
    
    this.getCurrentJoueurIndexNote = function(){
        return monEquipe.getJoueurId(monEquipe.getCurrentJoueurIndex()).getNote()
    }

    // les actions
    $("#addNew").click(function() {
        monEquipe.reset();
    });

    $("#addDelete").click(function() {
        var i = monEquipe.getCurrentJoueurIndex();

        if (i != null) {
            monEquipe.removeAtIndex(i);
            monEquipe.setCurrentJoueurIndex(null);
            $("#addButton").attr("value", "Ajouter un Joueur");
            $("#addNote").hide();
        }
        if (i == null) {
            monEquipe.reset();
        }

    });

    $("#addButton").click(function() {
        var newNom = null;
        newNom = prompt('Nouveau nom:', '');
        if (newNom != null && monEquipe.getCurrentJoueurIndex() == null) {
            joueur = new Joueur(newNom, "", 400, 200);
            monEquipe.addJoueur(joueur);
            monEquipe.setCurrentJoueurIndex(monEquipe.getCount() - 1);
        } else if (newNom != null) {
            monEquipe.getJoueurId(monEquipe.getCurrentJoueurIndex()).setName(newNom);
            _this.update(monEquipe.getJoueurs(), monEquipe.getCurrentJoueurIndex());
        }
    });

    $("#addMoreButton").click(function() {
        monEquipe.setCurrentJoueurIndex(null);
        var newNom = null;
        newNom = prompt('Nouveau nom:', '');
        if (newNom != null && monEquipe.getCurrentJoueurIndex() == null) {
            joueur = new Joueur(newNom, "", 400, 200);
            monEquipe.addJoueur(joueur);
            monEquipe.setCurrentJoueurIndex(monEquipe.getCount() - 1);
        }
    });

    $("#addNote").click(function() {
        if (monEquipe.getCurrentJoueurIndex() != null) {
            var mynote = prompt('Votre note:', '');
            var id = monEquipe.getCurrentJoueurIndex();
            var joueur = monEquipe.getJoueurId(id);
            if (mynote) {
                joueur.setNote(mynote);
                $("#addNote").attr("value", "Modifier la Note");
            }
            _this.update(monEquipe.getJoueurs(), id);
        }

    });

    // les clics sur le canvas

    $("#myCanvas").mouseup(function(event) {
        drag = false;
        $('canvas').css('cursor', 'auto');
    });

    $("#myCanvas").mousemove(function(event) {
        var currentJoueurIndex = monEquipe.getCurrentJoueurIndex();

        if (drag == true) {

            posX = event.pageX - this.offsetLeft;
            posY = event.pageY - this.offsetTop;

            newData = {};
            newData.x = posX - (maillot.w / 2);
            newData.y = posY - (maillot.h / 2);

            monEquipe.modifyXYofOneJoueur(currentJoueurIndex, newData);

        }

    });

    $("#myCanvas").mousedown(function(event) {
        clicX = event.pageX - this.offsetLeft;
        clicY = event.pageY - this.offsetTop;
        $('canvas').css('cursor', 'auto');
        if (testClicOn(clicX, clicY) == true) {
            drag = true;
            $('canvas').css('cursor', 'pointer');
        }
        else {
            drag = false;
            $('canvas').css('cursor', 'auto');
        }

    });


    // test si on clic sur un joueur 
    function testClicOn(mClicX, mClicY) {
        var lesJoueurs = monEquipe.getJoueurs();
        var test = false;
        monEquipe.setCurrentJoueurIndex(null);

        for (i = 0; i < monEquipe.getCount(); i++) {
            console.log("i:" + i);
            if (mClicX >= lesJoueurs[i].x && mClicX <= lesJoueurs[i].x + maillot.w
                    && mClicY >= lesJoueurs[i].y && mClicY <= lesJoueurs[i].y + maillot.h) {
                monEquipe.setCurrentJoueurIndex(i);
                test = true;
            } else {
                test = false;
            }
        }

        // console.log(test);
        return test;
    }
    
}

et enfin le fichier main.js, qui lance l’application

$(function() {

    monEquipe = new Equipe();
    monController = new Controller(monEquipe);
    maVue = new Vue(monController);
    // maVue sera notifiée par monEquipe de ses changements
    monEquipe.addObserver(maVue);
    
}); // end $(function(){});

Je vous laisse découvrir l’ensemble des fichiers dans le .rar. De nombreuses améliorations seraient possibles: système de sauvegarde (images, cookies, base de données, …), choix du maillot, …

Publié dans Canvas, Javascript

quand j’aurai le temps

  • les filtres wordpress
  • plugin wordpress et enregistrement de données
  • les wordpress custom post type
  • la bdd d'un blog wordpress
  • la balise more de wp
  • personnaliser une galerie wp
  • gérer les longueurs des extraits de wp
  • les animations css3
  • le memento symphony2
  • le squelette d'une page html5
  • liste sur plusieurs colonnes
  • le responsive design
  • exemple d'un jeu basique en html5
  • la réplication des bases de données
  • mettre en place une architecture en silo avec wp
  • parser un fichier xml (donc un rss) avec php5
  • mettre en place lightbox sans plugin
  • améliorer les performances de son wp
  • ajouter un bouton à l'éditeur de texte de wp
  • ...