• Vous jouez contre vous même...
  • Un nouveau labyrinthe est créé à chaque partie.
  • Pour vous déplacer vous pouvez soit :
    • Utiliser les 4 touches fléchées (←, →, ↑ et ↓) de votre clavier,
    • Soit utiliser la souris ou vos doigts (dans le cas d’une tablette ou d’un smartphone tactile) en « pointant » le bout de la flèche dans la direction que vous voulez prendre.
  • Le but est de se faire plaisir en tuant un peu le temps (qui lui prend un malin plaisir à nous tuer à petit feu...).
  • Fonctionne dans tout navigateur moderne : Firefox, Safari, Chrome, Opera...
  • Par contre sous Internet Explorer, il vous faudra la toute dernière version, et encore... (vous changez quand de butineur pour quelque chose de robuste et de conforme aux spécifications Web ?)

 Mais c’est quoi un canvas ?

L’attribut <canvas> est un élément HTML, apparu en 2005 (!), servant au dessin de graphiques, du type bitmap, en utilisant des scripts (habituellement en JavaScript).
Il peut être ainsi utilisé pour le dessin de graphes, pour créer des compositions de photos ou encore de simples (voire pas si simples) animations.

L’attribut <canvas> fait partie de la spécification Web applications 1.0 du WhatWG également appelée HTML 5.

Il a été initialement créé par Apple pour le Dashboard de Mac OS X et ensuite implémenté dans le browser Safari.
Les navigateurs basés sur le moteur Gecko supportent ce nouvel élément depuis la version 1.8. Ainsi les navigateurs modernes comme Firefox, Safari, Chrome et Opera interprètent correctement cet attribut.
Des initiatives visant à faire fonctionner <canvas> dans Internet Explorer existent aussi, par exemple le projet ExplorerCanvas sur SourceForge.

Comme on utilise des scripts pour contrôler les éléments canvas, il est également très facile de créer des animations, interactives ou pas.
Malheureusement, l’élément canvas n’a pas été conçu pour être utilisé de cette façon (au contraire de Flash), il y a donc certaines limitations ; en particulier, c’est qu’une fois qu’une forme a été dessinée, elle le reste.
Si on veut la faire bouger, il faut la redessiner ainsi que tout ce qui avait été dessiné avant elle.
Redessiner des formes complexes prend beaucoup de temps et les performances dépendent fortement de la vitesse de l’ordinateur sur lequel on se trouve.

Pour ceux et celles qui ont connu le langage Logo, il y a un air de ressemblance dans l’élaboration du dessin, surtout dans les méthodes utilisées...

 Mini simulation du système solaire

Nous allons voir ici comment animer le mouvement de la terre, accompagnée de son satellite la lune, autour du soleil.

Lorsqu’on dessine une image complète faisant partie d’une animation, il y a toujours un nombre défini d’étapes nécessaires :

    • Effacer le calque
      À moins que les formes que vous allez dessiner remplissent toute la surface du canevas (par exemple une image de fond), il est nécessaire d’effacer toute forme qui peut avoir été dessinée précédemment. La manière la plus simple de le faire est d’utiliser la méthode clearRect.
    • Enregistrer l’état du calque
      Si vous changez des réglages (styles, transformations, etc.) qui affectent l’état du canevas et voulez vous assurer que l’état original du canevas est utilisé à chaque nouvelle image, il est nécessaire de l’enregistrer.
    • Dessiner les formes animées
      L’étape où l’image complète est réellement dessinée.
    • Restaurer l’état du calque
      Si l’état a été enregistré, il faudra le restaurer avant de dessiner une nouvelle image.
    • Interaction avec l’utilisateur
      Récupérer les actions et les évènements de la souris et du clavier pour modifier en conséquence le déroulement de l’animation.
    • Et... On recommence !
      Un appel à la fonction setInterval qui exécute le code fourni de manière répétée permet de boucler l’animation.
      Dans l’exemple ci-après, la fonction affiche_systeme_solaire est exécutée toutes les 100 millisecondes (environ dix fois par seconde).
<!-- Le conteneur de l'animation -->
<canvas id="sysol" width="300" height="300"></canvas>

<script type="text/javascript">

function init() {
 var soleil = 25; // Le diamètre du soleil
 var terre = 15; // La terre
 var lune = 6; // La lune
 setInterval(affiche_systeme_solaire,100); // Affichage des positions toutes les 100 ms
}

function affiche_systeme_solaire() {
 var context = document.getElementById('sysol').getContext('2d'); // On récupère le contexte du calque

 context.globalCompositeOperation = 'destination-over'; // Les affichages se font avec la méthode du peintre
 context.clearRect(0, 0, 300, 300); // On efface le "canevas" (le calque)
 
 context.fillStyle = 'rgba(0,0,0,0.4)';
 context.strokeStyle = 'rgba(0,153,255,0.4)';
 context.save(); // On empile ("push") le contexte

 context.translate(150, 150); // Translation au centre du dessin

 // La belle planète bleue
 var time = new Date();
 context.rotate( ((2*Math.PI)/60)*time.getSeconds() + ((2*Math.PI)/60000)*time.getMilliseconds() );
 context.translate(105,0);
 context.fillRect(0, -20, 35, 40); // Ombre sur la terre et ombre portée sur la lune
 context.beginPath();
 context.fillStyle = "#0000FF";
 context.arc(0, 0, terre, 0, .01, 1);
 context.fill();
 context.closePath();

 // Et son satellite
 context.save();  // On empile ("push") le contexte
 context.beginPath();
 context.strokeStyle = 'rgba(200,200,200,0.4)';
 context.arc(0, 0, 27, 0, Math.PI*2, false); // Affichage de l'orbite lunaire
 context.stroke();
 context.closePath();
 context.rotate( ((2*Math.PI)/6)*time.getSeconds() + ((2*Math.PI)/6000)*time.getMilliseconds() );
 context.translate(0, 28.5); // Translation
 context.beginPath();
 context.fillStyle = "#FFFFFF";
 context.arc(-2, -2, lune, 0, .01, 1);
 context.fill();
 context.closePath();
 context.restore();  // On dépile ("pop") le contexte

 context.restore();  // On dépile ("pop") le contexte
 
 context.beginPath();
 context.arc(150, 150, 105, 0, Math.PI*2, false); // Affichage de l'orbite terrestre
 context.stroke();
 context.closePath();

 // Le soleil enfin...
 context.beginPath();
 context.fillStyle = "#FFFF00";
 context.arc(150, 150, soleil, 0, .01, 1);
 context.fill();
 context.closePath();
}

init(); // Et on lance l'animation !

</script>

Et voici le résultat :

 Une petite course automobile ?

Une animation, c’est bien.
Mais une animation interactive (aka un jeu), c’est mieux !

Ici le but est de faire le meilleur temps pour un tour de circuit. L’interactivité avec le joueur repose sur les 4 touches fléchées du clavier :

  • « ← » pour se diriger à gauche
  • « → » pour aller à droite
  • « ↑ » pour accélérer
  • « ↓ » pour ralentir et même passer la marche arrière

Pour jouer il suffit de cliquer sur le circuit et de commencer à utiliser les flèches.

Je vous laisse découvrir le script correspondant à ce petit jeu :

<html><head>
<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8">
<title>Super Sprint</title>
<style>a{color:#fff;font-weight:bold;text-decoration:none;}</style>
</head>
<body onkeypress="clavier();" onkeydown="clavier();">
<center>
 <div id="gameframe" style="width:300px;height:300px;background-color:green;">
   <div id="game" style="position:relative;top:0;left:0;width:300px;height:300px;margin:0;padding:0;">  
     <canvas id="circuit_board" width="300" height="300" style="z-index:1;position:absolute;top:0;left:0;margin:0;padding:0;"></canvas>
     <canvas id="car_board" width="300" height="300" style="z-index:2;position:absolute;top:0;left:0;margin:0;padding:0;"></canvas>
     <div style="z-index:3;position:absolute;top:100px;left:130px;color:white;font-weight:bold;font-family:Segoe UI,Calibri,Myriad Pro,Myriad,Trebuchet MS,Helvetica,Arial,sans-serif;">TOP<br><br><br>TOUR</div>
     <div id="top" style="z-index:3;position:absolute;top:120;left:115;color:yellow;font-weight:bold;font-family:Segoe UI,Calibri,Myriad Pro,Myriad,Trebuchet MS,Helvetica,Arial,sans-serif;"></div>
     <div id="chrono" style="z-index:3;position:absolute;top:185;left:115;color:yellow;font-weight:normal;font-family:Segoe UI,Calibri,Myriad Pro,Myriad,Trebuchet MS,Helvetica,Arial,sans-serif;"></div>
   </div>
 </div>
</center>
<script type="text/javascript">
var car_board_X = 300;
var car_board_Y = 300;
var carX = 150-20; // position initiale X
var carY = 255; // position initiale Y
var carD = 0; // direction initiale
var carS = 0; // vitesse initiale
var carH = 15; // longueur voiture
var carW = 5; // largeur voiture
var roueL = 2; // largeur roue
var roueD = 3; // diamètre roue
var top_chrono = 999; // le meilleur tour
var chrono_tour = 0; // le chrono pour chaque tour
var tofa = null;
var drapeau = null; // flag de passage sur la ligne
var symetrie = null; // flag de passage à l'opposé de la ligne de départ

function clavier(){
        event.returnValue=false;
        switch(event.keyCode)
        {
                case 37:carD -=4; if(carD<0) carD=359; break;
                case 40:carS--; break;
                case 39:carD +=4; if(carD>359) carD=0; break;
                case 38:carS++; break;
                default:return;
        }
}

function dessine_circuit(){
 var context = document.getElementById("circuit_board").getContext("2d");
 context.clearRect(0, 0, car_board_X, car_board_Y);
 context.fillStyle = "rgb(150,150,150)"; // Le gris du bitume
 roundedRect(context,25,25,255,255 ,50);
 context.fillStyle = "green"; // La pelouse
 roundedRect(context,70,70,170,170,30);
 context.fillStyle = "rgb(150,255,0)"; // Le vert de la ligne de départ
 for(var i=148;i<155;i+=2)
   for(var j=240;j<280;j+=2)
     if((i+j)%3 == 1) context.fillRect(i,j,2,2);
}

function movePlayer() {
 carX += Math.cos(carD*2*Math.PI/360)*carS/20;
 carY += Math.sin(carD*2*Math.PI/360)*carS/20;
 var context = document.getElementById("car_board").getContext("2d");
 context.clearRect(0, 0, car_board_X, car_board_Y);
 context.save();
 context.translate(carX+carH/2,carY+carW/2);
 context.rotate( carD*2*Math.PI/360 );
 context.translate(-carH/2,-carW/2);
 context.beginPath();
 // La carrosserie
 context.fillStyle = "#FF0000";
 context.fillRect(0, 0, carH, carW);
 // Les roues
 context.fillStyle = "#000";
 context.fillRect(0,carW, roueD, roueL);
 context.fillRect(0,-roueL, roueD, roueL);
 context.fillRect(2*carH/3,carW, roueD, roueL);
 context.fillRect(2*carH/3,-roueL, roueD, roueL);
 // Le casque
        context.fillStyle = "#FFFF00";
        context.arc(0.4*carH,carW/2,2, 0, .01, 1);
        context.fill();
 context.closePath();
 context.restore();
 // test de la sortie de route
 var context = document.getElementById("circuit_board").getContext("2d");
 // A l'avant de la voiture
 var avant_droite = context.getImageData(carX+carH+1, carY, 1, 1).data;
 var avant_centre = context.getImageData(carX+carH+1, carY+carW/2, 1, 1).data;
 var avant_gauche = context.getImageData(carX+carH+1, carY+carW, 1, 1).data;
 // A l'arrière de la voiture
 var arriere_droite = context.getImageData(carX-1, carY+carW, 1, 1).data;
 var arriere_centre = context.getImageData(carX-1, carY+carW/2, 1, 1).data;
 var arriere_gauche = context.getImageData(carX-1, carY, 1, 1).data;
 // Le test sur la composante rouge des pixels de la route
 if( (avant_droite[0]!=150) ||
     (avant_centre[0]!=150) ||  
     (avant_gauche[0]!=150) ||  
     (arriere_droite[0]!=150) ||  
     (arriere_centre[0]!=150) ||  
     (arriere_gauche[0]!=150) ){
   // Rebond de la voiture
   carX -= 6*(Math.cos(carD*2*Math.PI/360)*carS/20);
   carY -= 6*(Math.sin(carD*2*Math.PI/360)*carS/20);
   carD = 360 - carD + Math.floor(Math.random()*30) - Math.floor(Math.random()*30);
   carS = -carS/4;  
 }
 // Gestion du chrono : passage sur la ligne de départ
 if( Math.cos(carD*2*Math.PI/360)>0 &&
     carS>0 &&
     (carX+carH)>149 &&
     (carX+carH)<155 &&
     carY>150){
   if(symetrie==null){
     chrono_tour = 0;
           if(tofa!=null)clearTimeout(tofa);
     tofa = setTimeout("chrono()",100);
   }else if(symetrie==1){
     symetrie = 0;
     if(chrono_tour<top_chrono){
       top_chrono = chrono_tour;
       document.getElementById("top").innerHTML = calculduree(top_chrono);
     }      
     chrono_tour = 0;
           if(tofa!=null)clearTimeout(tofa);
     tofa = setTimeout("chrono()",100);
   }  
 }
 // Gestion du chrono : passage sur la ligne opposée au départ
 if( Math.cos(carD*2*Math.PI/360)<0 &&
     carS>0 &&
     (carX+carH)>149 &&
     (carX+carH)<155 &&
     carY<150){
   symetrie = 1;  
 }
 setTimeout("movePlayer()",10);
}

function calculduree(duree){
 var addZero = function(v) { return v<10 ? "0" + v : v; };
 var n = Math.floor(duree/10);
 var dix = duree-10*n;
 var heures=Math.floor(n/3600);
        var reste = n - heures*3600;
        var minutes=Math.floor(reste/60);
 reste -= minutes*60;
 return addZero(heures)+":"+addZero(minutes)+":"+addZero(reste)+"."+dix;
}

function roundedRect(ctx,x,y,width,height,radius){
 ctx.beginPath();
 ctx.moveTo(x,y+radius);
 ctx.lineTo(x,y+height-radius);
 ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
 ctx.lineTo(x+width-radius,y+height);
 ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
 ctx.lineTo(x+width,y+radius);
 ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
 ctx.lineTo(x+radius,y);
 ctx.quadraticCurveTo(x,y,x,y+radius);
 ctx.fill();
}

function chrono(){
        // if(tofa!=undefined)clearTimeout(tofa);else tofa=undefined;
        chrono_tour++;
        document.getElementById("chrono").innerHTML = calculduree(chrono_tour);
        tofa=setTimeout("chrono()",100);
}

// On y va !
dessine_circuit();
document.getElementById("top").innerHTML = calculduree(top_chrono);
movePlayer();
</script>
</body></html>

Bonnes parties !

P.-S.