FR - Retours sur un développement HTML5 & JavaScript avec Visual Studio 2010 - Eternal Coding - HTML5 / Windows / Kinect / 3D development - Site Home - MSDN Blogs

FR - Retours sur un développement HTML5 & JavaScript avec Visual Studio 2010


 

FR - Retours sur un développement HTML5 & JavaScript avec Visual Studio 2010

  • Comments 10

En tant que fan des interfaces utilisateurs (quelle qu’elles soient) je ne pouvais pas ne pas m’intéresser et me pencher sur la réalisation d’une application en HTML 5.

Le but de cet article va donc être de parcourir ensemble le développement d’une application utilisant HTML 5 et JavaScript. Au fur et à mesure des fonctionnalités nous pourrons ainsi voir par l’exemple une manière (parmi d’autres) d’appréhender ce genre de projet.

Présentation de l’application image
Les outils
La page HTML 5
Récupération des données
Chargement des cartes et gestion du cache
Affichage des cartes
Gestion de la souris
Stockage de l’état
Animations
Gestion des différents types de clients
Conclusion
Pour aller plus loin

Présentation de l’application

Nous allons donc réaliser une application permettant de visualiser dans un navigateur une collection de cartes à jouer de la collection Magic the Gathering © (www.wizards.com/Magic). Le but est de permettre à l’utilisateur de se promener avec sa souris dans la collection et de pouvoir zoomer sur une carte en particulier ou au contraire de reculer pour embrasser un maximum de cartes. En gros nous allons essayer de reproduire le comportement de Bing Maps mais avec des cartes à jouer.

image

Vous pouvez d’ores et déjà voir le résultat ici : http://bolaslenses.catuhe.com

Les sources du projet sont disponibles ici : http://www.catuhe.com/msdn/bolaslenses.zip

Les cartes sont stockées sur Windows Azure Storage et utilisent le service de Content Distribution Network (CDN : service qui permet de déployer les données au plus proche des utilisateurs finaux) pour obtenir un maximum de performances. Un service ASP.NET se charge de retourner la liste des cartes au format JSON.

image

Le but ici n’étant pas de parler de la partie serveur, nous ne nous attarderons pas sur la mise en œuvre du serveur.

Les outils

Pour développer notre application, nous allons utiliser Visual Studio 2010 SP1 avec l’extension Web Standards Update. Cette dernière apporte le support de l’IntelliSense dans les pages HTML 5 et permet un gain de temps appréciable.

Notre solution va donc contenir une page HTML 5 associée à des fichiers .js qui contiennent les scripts JavaScript. En ce qui concerne le debug, il est possible de mettre un breakpoint directement dans les fichiers .js au sein de Visual Studio ou bien de passer par les services de la barre de développeurs (F12) d’Internet Explorer 9.

image

Debug sous Visual Studio 2010

image

Debug avec la barre de développeur d’Internet Explorer 9 (F12)

Nous disposons donc d’un environnement de développement avec le support de l’IntelliSense et du debug. Nous sommes donc opérationnel pour commencer et pour cela nous allons démarrer par la mise en place de la page HTML 5.

image

La page HTML 5

Notre page va se centrer autour d’un canvas qui servira de zone de dessin pour les cartes :

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta charset="utf-8" />
  5.     <title>Bolas Lenses</title>
  6.     <link href="Content/full.css" rel="stylesheet" type="text/css" />
  7.     <link href="Content/mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 480px)" />
  8.     <link href="Content/mobile.css" rel="stylesheet" type="text/css" media="screen and (max-device-width: 480px)" />
  9.     <script src="Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
  10. </head>
  11. <body>
  12.     <header>
  13.         <div id="legal">
  14.             Cards scanned by <a href="http://www.slightlymagic.net/">MWSHQ Team</a><br />
  15.             Magic the Gathering official site : <a href="http://www.wizards.com/Magic/TCG/Article.aspx?x=mtg/tcg/products/allproducts">
  16.                 http://www.wizards.com/Magic</a>
  17.             <div id="cardsCount">
  18.             </div>
  19.         </div>
  20.         <div id="leftHeader">
  21.             <img id="pictureCell" src="/Content/MTG Black.png" alt="Bolas logo" id="bolasLogo" />
  22.             <div id="title">
  23.                 Bolas Lenses
  24.             </div>
  25.         </div>
  26.     </header>
  27.     <section>
  28.         <img src="Content/Back.jpg" style="display: none" id="backImage" alt="backImage"
  29.             width="128" height="128" />
  30.         <canvas id="mainCanvas">
  31.                     Your browser does not support HTML5 canvas.
  32.         </canvas>
  33.         <div id="stats" class="tooltip">
  34.         </div>
  35.         <div id="waitText" class="tooltip">
  36.             Loading data...
  37.         </div>
  38.     </section>
  39.     <!--Scripts-->
  40.     <script src="Bolas/bolasLenses.animations.js" type="text/javascript"></script>
  41.     <script src="Bolas/bolasLenses.mouse.js" type="text/javascript"></script>
  42.     <script src="Bolas/bolasLenses.cache.js" type="text/javascript"></script>
  43.     <script src="Bolas/bolasLenses.js" type="text/javascript"></script>
  44. </body>
  45. </html>

Si nous décortiquons cette page, nous pouvons noter qu’elle se découpe en deux parties :

  • La partie d’entête (header) qui contient le titre, le logo et les mentions spéciales
  • La partie principale (section) qui va contenir le canvas et les bulles d’aides qui afficheront le statut de l’application. Il y a également une image cachée (backImage) qui servira de sources pour les images non encore chargées.

Pour mettre en page cette page, une feuille de style (full.css) est appliquée. Cette dernière constitue un mécanisme permettant de modifier le style des balises (En HTML, un style définit l’intégralité des options d’affichage d’une balise ainsi que son comportement par rapport au layout) :

  1. html, body
  2. {
  3.     height: 100%;
  4. }
  5.  
  6. body
  7. {
  8.     background-color: #888888;
  9.     font-size: .85em;
  10.     font-family: "Segoe UI, Trebuchet MS" , Verdana, Helvetica, Sans-Serif;
  11.     margin: 0;
  12.     padding: 0;
  13.     color: #696969;
  14. }
  15.  
  16. a:link
  17. {
  18.     color: #034af3;
  19.     text-decoration: underline;
  20. }
  21.  
  22. a:visited
  23. {
  24.     color: #505abc;
  25. }
  26.  
  27. a:hover
  28. {
  29.     color: #1d60ff;
  30.     text-decoration: none;
  31. }
  32.  
  33. a:active
  34. {
  35.     color: #12eb87;
  36. }
  37.  
  38. header, footer, nav, section
  39. {
  40.     display: block;
  41. }
  42.  
  43. table
  44. {
  45.     width: 100%;
  46. }
  47.  
  48. header, #header
  49. {
  50.     position: relative;
  51.     margin-bottom: 0px;
  52.     color: #000;
  53.     padding: 0;
  54. }
  55.  
  56. #title
  57. {
  58.     font-weight: bold;
  59.     color: #fff;
  60.     border: none;
  61.     font-size: 60px !important;
  62.     vertical-align: middle;
  63.     margin-left: 70px
  64. }
  65.  
  66. #legal
  67. {
  68.     text-align: right;
  69.     color: white;
  70.     font-size: 14px;
  71.     width: 50%;
  72.     position: absolute;
  73.     top: 15px;
  74.     right: 10px
  75. }
  76.  
  77. #leftHeader
  78. {
  79.     width: 50%;
  80.     vertical-align: middle;
  81. }
  82.  
  83. section
  84. {
  85.     margin: 20px 20px 20px 20px;
  86. }
  87.  
  88.     #mainCanvas{
  89.     border: 4px solid #000000;
  90. }
  91.  
  92. #cardsCount
  93. {
  94.     font-weight: bolder;
  95.     font-size: 1.1em;
  96. }
  97.  
  98. .tooltip
  99. {
  100.     position: absolute;
  101.     bottom: 5px;
  102.     color: black;
  103.     background-color: white;
  104.     margin-right: auto;
  105.     margin-left: auto;
  106.     left: 35%;
  107.     right: 35%;
  108.     padding: 5px;
  109.     width: 30%;
  110.     text-align: center;
  111.     border-radius: 10px;
  112.     -webkit-border-radius: 10px;
  113.     -moz-border-radius: 10px;
  114.     box-shadow: 2px 2px 2px #333333;
  115. }
  116.  
  117. #bolasLogo
  118. {
  119.     width: 64px;
  120.     height: 64px;
  121. }
  122.  
  123. #pictureCell
  124. {
  125.     float: left;
  126.     width: 64px;
  127.     margin: 5px 5px 5px 5px;
  128.     vertical-align: middle;
  129. }

A noter que les les entrées référencent :

  • Soit une balise directement en utilisant son nom comme par exemple la balise body
  • Soit une classe (lorsque le nom commence par un .) comme par exemple .tooltip qui affectera les deux div stats et waitText de la page
  • Soit un identifiant spécifique (lorsque le nom commence par un #) comme par exemple #bolasLogo qui affectera l’icône de l’application

Ainsi, cette feuille se charge de mettre en place l’affichage suivant:

image

Les feuilles de styles sont des outils puissants qui permettent une infinité de présentations. Il est toutefois parfois complexe de les régler quand par exemple une balise est à la fois affectée par une classe, un identifiant et son container. Pour simplifier cette mise en place, la barre de développement d’Internet Explorer 9 est particulièrement utile car elle permet de voir la hiérarchie de styles qui se sont appliqués sur un élément.

Prenons dans notre cas, la bulle d’aide waitText et voyons ce que la barre de développement nous propose. Pour ce faire, il faut appuyer sur F12 sous Internet Explorer 9 et utiliser le sélecteur par clic pour choisir notre bulle d’aide:

imageUne fois la sélection effectuée, nous pouvons visualiser la hiérarchie des styles :

imageOn peut ainsi constater que notre div a reçu ses styles à la fois de la balise body dont il est l’enfant et de l’entrée .tooltip de la feuille de styles.

Grâce à cet écran, il devient possible de voir l’effet de chaque style (que l’on peut désactiver). Il est également possible de rajouter à la volée de nouveaux styles.

Un autre point important de cette fenêtre est la possibilité de changer le mode de fonctionnement d’Internet Explorer 9. En effet, nous pouvons tester comment, par exemple, pourrait se comporter Internet Explorer 8 avec la même page. Pour cela, il faut aller dans le menu Mode de Navigation (Browser Mode) et choisir le moteur d’Internet Explorer 8. Cette modification va surtout impacter notre bulle d’aide car cette dernière fait appel dans son style à la notion de border-radius (bordure arrondie) et de box-shadow (ombre portée) qui sont des fonctionnalités de CSS 3. Ainsi en basculant sur le moteur d’Internet Explorer 8, nous allons découvrir comment la page se comporte sur un navigateur ne supportant pas HTML 5/ CSS 3:

image image

Internet Explorer 9

Internet Explorer 8

On parle ici de gracefull degradation en ce sens que la page continue à fonctionner (avec des différences visuelles non gênantes) lorsque le navigateur ne supporte pas toutes les technologies nécessaires.

Maintenant que notre interface est posée nous allons nous intéresser à la source de données pour récupérer les cartes à afficher.

Récupération des données

Notre serveur propose de retourner la liste des cartes au format JSON sur cette URL:

http://bolaslenses.catuhe.com/Home/ListOfCards/?colorString=0

Il accepte un paramètre “colorString” qui permet de ne retourner que les cartes de la couleur précisée (0 = toutes).

Lorsque l’on développe en JavaScript, il y a un bon réflexe à avoir (réflexe qu’il est bon d’avoir avec d’autres langages également mais qui est particulièrement important en JavaScript) : il faut se poser la question de savoir si ce que l’on veut développer n’a pas déjà été fait dans un framework existant. En effet, il existe une multitude de projets libres (ou non) autour de JavaScript. Parmi ceux-là il faut bien sûr nommer jQuery qui fournit pléthore de services pratiques.

Ainsi, dans notre cas, pour aller se connecter à l’URL de notre serveur et récupérer nos informations, nous pourrions passer par une XmlHttpRequest et nous amuser à parser le JSON obtenu. Ou alors, nous pouvons passer par jQuery (Vous verrez que vous vous ferez souvent ce genre de remarques Sourire).

Nous allons donc passer par la méthode getJSON qui va se charger de tout pour nous et qui appellera une fonction que l’on lui donne lorsque le travail sera fait :

  1. function getListOfCards() {
  2.     var url = "http://bolaslenses.catuhe.com/Home/ListOfCards/?jsoncallback=?";
  3.     $.getJSON(url, { colorString: "0" }, function (data) {
  4.         listOfCards = data;
  5.         $("#cardsCount").text(listOfCards.length + " cards displayed");
  6.         $("#waitText").slideToggle("fast");
  7.     });
  8. }

Comme nous pouvons le voir, notre fonction stocke la liste des cartes dans la variable listOfCards et appelle deux fonctions de jQuery :

  • text qui permet de changer le texte d’une balise
  • slideToggle qui permet de faire disparaitre (ou apparaitre selon) une balise en animant sa hauteur

La liste listOfCards contient des objets dont le format est le suivant :

  • ID : Identifiant unique de la carte
  • Path : Chemin relatif de la carte (sans l’extension)

Pour la petite histoire, il est à noter que l’url est appelée avec le suffixe “?jsoncallback=?”. En effet, les appels Ajax sont contraints en termes de sécurité à ne fonctionner qu’avec un serveur sur la même adresse que le script d’appel. Il existe toutefois une solution appelée JSONP qui va nous permettre de faire un appel concerté avec le serveur (qui doit donc accepter et être au courant de l’opération) en fournissant une callback de retour. Heureusement pour nous jQuery gère tout cela tout seul en ajoutant juste le bon suffixe!

Une fois que nous disposons de notre liste, nous allons pouvoir mettre en place les chargements individuels.

Chargement des cartes et gestion du cache

Le principe central de notre application est de ne dessiner que les cartes réellement visibles à l’écran. La fenêtre de visualisation quand à elle est définie par un niveau de zoom et un décalage (x,y) dans le système global.

  1. var visuControl = { zoom : 0.25, offsetX : 0, offsetY : 0 };

image

Ce dernier est défini par un certain nombre de cartes en largeur sur un certain nombre de cartes en hauteur. Dans notre exemple, nous disposons de 14819 cartes qui se répartissent sur 200 colonnes et 75 lignes.

De plus, il faut savoir que chaque carte existe en trois versions :

  • haute définition : 480x680 sans compression (suffixe : .jpg)
  • moyenne définition : 240x340 avec une compression moyenne (suffixe : .50.jpg)
  • basse définition : 120x170 avec une forte compression (suffixe : .25.jpg)

Ainsi, en fonction du niveau de zoom, nous allons charger la bonne version pour optimiser au maximum les transferts réseaux.

Pour ce faire nous allons développer une fonction qui saura donner une image pour une carte définie. Cette classe sera configurée pour aller chercher un certain niveau de qualité. De plus elle sera chainée avec le niveau de qualité inférieur pour retourner ce dernier si la carte pour le niveau courante n’est pas encore téléchargée.

La fonction qui va se charger de cela est définie par le code suivant :

  1. function imageCache(substr, replacementCache) {
  2.     var extension = substr;
  3.     var backImage = document.getElementById("backImage");
  4.  
  5.  
  6.     this.load = function (card) {
  7.         var localCache = this;
  8.  
  9.         if (this[card.ID] != undefined)
  10.             return;
  11.  
  12.         var img = new Image();
  13.         localCache[card.ID] = { image: img, isLoaded: false };
  14.         currentDownloads++;
  15.  
  16.         img.onload = function () {
  17.             localCache[card.ID].isLoaded = true;
  18.             currentDownloads--;
  19.         };
  20.  
  21.         img.onerror = function() {
  22.             currentDownloads--;
  23.         };
  24.         
  25.         img.src = "http://az30809.vo.msecnd.net/" + card.Path + extension;
  26.     };
  27.  
  28.     this.getReplacementFromLowerCache = function (card) {
  29.         if (replacementCache == undefined)
  30.             return backImage;
  31.  
  32.         return replacementCache.getImageForCard(card);
  33.     };
  34.  
  35.     this.getImageForCard = function(card) {
  36.         var img;
  37.         if (this[card.ID] == undefined) {
  38.             this.load(card);
  39.  
  40.             img = this.getReplacementFromLowerCache(card);
  41.         }
  42.         else {
  43.             if (this[card.ID].isLoaded)
  44.                 img = this[card.ID].image;
  45.             else
  46.                 img = this.getReplacementFromLowerCache(card);
  47.         }
  48.  
  49.         return img;
  50.     };
  51. }

Un imageCache se construit en donnant le suffixe à ajouter au chemin de la carte (.50.jpg par exemple pour le niveau intermédiaire) et le cache inférieur.

On peut voir ici deux fonctions importantes :

  • load : Cette fonction va se charger de télécharger la bonne image et va la stocker dans un tableau de cache (l’adresse de téléchargement sur msecnd.net est celle du Azure CDN qui dispose de toutes les images)
  • getImageForCard : Cette fonction retourne l’image du cache si elle est chargée sinon elle demande au cache inférieur de retourner sa version (et ainsi de suite)

Ainsi pour gérer nos trois niveaux de caches, nous pouvons déclarer trois variables:

  1. var imagesCache25 = new imageCache(".25.jpg");
  2. var imagesCache50 = new imageCache(".50.jpg", imagesCache25);
  3. var imagesCacheFull = new imageCache(".jpg", imagesCache50);

La sélection du bon cache est uniquement fonction du zoom :

  1. function getCorrectImageCache() {
  2.     if (visuControl.zoom <= 0.25)
  3.         return imagesCache25;
  4.  
  5.     if (visuControl.zoom <= 0.8)
  6.         return imagesCache50;
  7.  
  8.     return imagesCacheFull;
  9. }

Pour donner un retour utilisateur, nous allons rajouter un timer qui va se charger de faire remonter notre bulle d’aide avec le nombre d’images en cours de chargement:

  1. function updateStats() {
  2.     var stats = $("#stats");
  3.  
  4.     stats.html(currentDownloads + " card(s) currently downloaded.");
  5.  
  6.     if (currentDownloads == 0 && statsVisible) {
  7.         statsVisible = false;
  8.         stats.slideToggle("fast");
  9.     }
  10.     else if (currentDownloads > 1 && !statsVisible) {
  11.         statsVisible = true;
  12.         stats.slideToggle("fast");
  13.     }
  14. }
  15.  
  16. setInterval(updateStats, 200);

Encore une fois on notera l’usage de jQuery tant pour faciliter la sélection que pour gérer l’apparition et la disparition.

Nous allons maintenant nous intéresser à l’affichage proprement dit des cartes.

Affichage des cartes

Pour dessiner nos cartes, nous devons en fait remplir le canvas en utilisant son contexte 2D (qui n’existera que si le navigateur supporte les canvas HTML 5) :

  1. var mainCanvas = document.getElementById("mainCanvas");
  2. var drawingContext = mainCanvas.getContext('2d');

L’affichage des cartes va être réalisé par la fonction processListOfCards qui sera appelée 60 fois par seconde :

  1. function processListOfCards() {
  2.  
  3.     if (listOfCards == undefined) {
  4.         drawWaitMessage();
  5.         return;
  6.     }
  7.  
  8.     mainCanvas.width = document.getElementById("center").clientWidth;
  9.     mainCanvas.height = document.getElementById("center").clientHeight;
  10.     totalCards = listOfCards.length;
  11.  
  12.     var localCardWidth = cardWidth * visuControl.zoom;
  13.     var localCardHeight = cardHeight * visuControl.zoom;
  14.  
  15.     var effectiveTotalCardsInWidth = colsCount * localCardWidth;
  16.  
  17.     var rowsCount = Math.ceil(totalCards / colsCount);
  18.     var effectiveTotalCardsInHeight = rowsCount * localCardHeight;
  19.  
  20.     initialX = (mainCanvas.width - effectiveTotalCardsInWidth) / 2.0 - localCardWidth / 2.0;
  21.     initialY = (mainCanvas.height - effectiveTotalCardsInHeight) / 2.0 - localCardHeight / 2.0;
  22.  
  23.     // Clear
  24.     clearCanvas();
  25.  
  26.     // Computing of the viewing area
  27.     var initialOffsetX = initialX + visuControl.offsetX * visuControl.zoom;
  28.     var initialOffsetY = initialY + visuControl.offsetY * visuControl.zoom;
  29.  
  30.     var startX = Math.max(Math.floor(-initialOffsetX / localCardWidth) - 1, 0);
  31.     var startY = Math.max(Math.floor(-initialOffsetY / localCardHeight) - 1, 0);
  32.  
  33.     var endX = Math.min(startX + Math.floor((mainCanvas.width - initialOffsetX - startX * localCardWidth) / localCardWidth) + 1, colsCount);
  34.     var endY = Math.min(startY + Math.floor((mainCanvas.height - initialOffsetY - startY * localCardHeight) / localCardHeight) + 1, rowsCount);
  35.  
  36.     // Getting current cache
  37.     var imageCache = getCorrectImageCache();
  38.  
  39.     // Render
  40.     for (var y = startY; y < endY; y++) {
  41.         for (var x = startX; x < endX; x++) {
  42.             var localX = x * localCardWidth + initialOffsetX;
  43.             var localY = y * localCardHeight + initialOffsetY;
  44.  
  45.             // Clip
  46.             if (localX > mainCanvas.width)
  47.                 continue;
  48.  
  49.             if (localY > mainCanvas.height)
  50.                 continue;
  51.  
  52.             if (localX + localCardWidth < 0)
  53.                 continue;
  54.  
  55.             if (localY + localCardHeight < 0)
  56.                 continue;
  57.  
  58.             var card = listOfCards[x + y * colsCount];
  59.  
  60.             if (card == undefined)
  61.                 continue;
  62.  
  63.             // Get from cache
  64.             var img = imageCache.getImageForCard(card);
  65.  
  66.             // Render
  67.             try {
  68.  
  69.                 if (img != undefined)
  70.                     drawingContext.drawImage(img, localX, localY, localCardWidth, localCardHeight);
  71.             } catch (e) {
  72.                 $.grep(listOfCards, function (item) {
  73.                     return item.image != img;
  74.                 });
  75.  
  76.             }
  77.         }
  78.     };
  79.  
  80.     // Scroll bars
  81.     drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY);
  82.  
  83.     // FPS
  84.     computeFPS();
  85. }

Cette fonction s’architecture autour de plusieurs points clefs:

  • Si la liste des cartes n’est pas encore chargée, nous affichons une bulle d’aide qui indique que le chargement est en cours :
  1. var pointCount = 0;
  2.  
  3. function drawWaitMessage() {
  4.     pointCount++;
  5.  
  6.     if (pointCount > 200)
  7.         pointCount = 0;
  8.  
  9.     var points = "";
  10.  
  11.     for (var index = 0; index < pointCount / 10; index++)
  12.         points += ".";
  13.  
  14.     $("#waitText").html("Loading...Please wait<br>" + points);
  15. }
  • Par la suite, nous devons définir la position de la fenêtre de visualisation (en termes de cartes et de coordonnées), puis nous procédons au nettoyage du canvas :
  1. function clearCanvas() {
  2.     mainCanvas.width = document.body.clientWidth - 50;
  3.     mainCanvas.height = document.body.clientHeight - 140;
  4.  
  5.     drawingContext.fillStyle = "rgb(0, 0, 0)";
  6.     drawingContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
  7. }
  • Le dessin en lui-même consiste à parcourir la liste des cartes à dessiner et à appeler la fonction drawImage du contexte du canvas. L’image en question est fournie par le cache actif (en fonction du zoom) :
  1. // Get from cache
  2. var img = imageCache.getImageForCard(card);
  3.  
  4. // Render
  5. try {
  6.  
  7.     if (img != undefined)
  8.         drawingContext.drawImage(img, localX, localY, localCardWidth, localCardHeight);
  9. } catch (e) {
  10.     $.grep(listOfCards, function (item) {
  11.         return item.image != img;
  12.     });
  • Puis il faut dessiner les barres de scrolling grâce à la fonction maison roundedRectangle qui utilise un path avec des courbes quadratiques :
  1. function roundedRectangle(x, y, width, height, radius) {
  2.     drawingContext.beginPath();
  3.     drawingContext.moveTo(x + radius, y);
  4.     drawingContext.lineTo(x + width - radius, y);
  5.     drawingContext.quadraticCurveTo(x + width, y, x + width, y + radius);
  6.     drawingContext.lineTo(x + width, y + height - radius);
  7.     drawingContext.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  8.     drawingContext.lineTo(x + radius, y + height);
  9.     drawingContext.quadraticCurveTo(x, y + height, x, y + height - radius);
  10.     drawingContext.lineTo(x, y + radius);
  11.     drawingContext.quadraticCurveTo(x, y, x + radius, y);
  12.     drawingContext.closePath();
  13.     drawingContext.stroke();
  14.     drawingContext.fill();
  15. }
  • Grâce à cette fonction le dessin des barres de scrolling est la suivante :
  1. function drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY) {
  2.     drawingContext.fillStyle = "rgba(255, 255, 255, 0.6)";
  3.     drawingContext.lineWidth = 2;
  4.  
  5.     // Vertical
  6.     var totalScrollHeight = effectiveTotalCardsInHeight + mainCanvas.height;
  7.     var scaleHeight = mainCanvas.height - 20;
  8.     var scrollHeight = mainCanvas.height / totalScrollHeight;
  9.     var scrollStartY = (-initialOffsetY + mainCanvas.height * 0.5) / totalScrollHeight;
  10.     roundedRectangle(mainCanvas.width - 8, scrollStartY * scaleHeight + 10, 5, scrollHeight * scaleHeight, 4);
  11.  
  12.     // Horizontal
  13.     var totalScrollWidth = effectiveTotalCardsInWidth + mainCanvas.width;
  14.     var scaleWidth = mainCanvas.width - 20;
  15.     var scrollWidth = mainCanvas.width / totalScrollWidth;
  16.     var scrollStartX = (-initialOffsetX + mainCanvas.width * 0.5) / totalScrollWidth;
  17.     roundedRectangle(scrollStartX * scaleWidth + 10, mainCanvas.height - 8, scrollWidth * scaleWidth, 5, 4);
  18. }
  • Et pour finir, il faut calculer le nombre d’images par seconde :
  1. function computeFPS() {
  2.     if (previous.length > 60) {
  3.         previous.splice(0, 1);
  4.     }
  5.     var start = (new Date).getTime();
  6.     previous.push(start);
  7.     var sum = 0;
  8.  
  9.     for (var id = 0; id < previous.length - 1; id++) {
  10.         sum += previous[id + 1] - previous[id];
  11.     }
  12.  
  13.     var diff = 1000.0 / (sum / previous.length);
  14.  
  15.     $("#cardsCount").text(diff.toFixed() + " fps. " + listOfCards.length + " cards displayed");
  16. }

Le dessin fait fortement appel à la capacité du navigateur à accélérer le rendu du canvas. Pour la petite histoire, voici les performances obtenues sur ma machine avec le zoom au minimum (0.05):

image

Navigateur

FPS

Internet Explorer 9 30
Firefox 5 30
Chrome 12 17
iPad (avec un zoom de 0.8) 7
Windows Phone Mango(avec un zoom de 0.8) 20 (!!)

 

Le site marche même sur téléphones portables et tablettes du moment que ces derniers supportent HTML 5.

C’est ici que nous pouvons voir la puissance de traitements des navigateurs HTML 5 qui arrivent à manipuler autant d’informations plus de 30 fois par seconde !

Gestion de la souris

Pour nous promener dans notre collections d’images, il faut gérer la souris (et notamment sa molette).

Pour la gestion du déplacement, nous allons juste nous brancher sur les évènements onmouvemove, onmouseup et onmousedown.

Les évènements onmouseup et onmousedown servent à savoir si la souris est cliquée ou non :

  1. var mouseDown = 0;
  2. document.body.onmousedown = function (e) {
  3.     mouseDown = 1;
  4.     getMousePosition(e);
  5.  
  6.     previousX = posx;
  7.     previousY = posy;
  8. };
  9.  
  10. document.body.onmouseup = function () {
  11.     mouseDown = 0;
  12. };

L’évènement onmousemove sera branché sur le canvas et servira à déplacer la vue :

  1. var previousX = 0;
  2. var previousY = 0;
  3. var posx = 0;
  4. var posy = 0;
  5.  
  6. function getMousePosition(eventArgs) {
  7.     var e;
  8.  
  9.     if (!eventArgs)
  10.         e = window.event;
  11.     else {
  12.         e = eventArgs;
  13.     }
  14.  
  15.     if (e.offsetX || e.offsetY) {
  16.         posx = e.offsetX;
  17.         posy = e.offsetY;
  18.     }
  19.     else if (e.clientX || e.clientY) {
  20.         posx = e.clientX;
  21.         posy = e.clientY;
  22.     }        
  23. }
  24.  
  25. function onMouseMove(e) {
  26.     if (!mouseDown)
  27.         return;
  28.     getMousePosition(e);
  29.  
  30.     mouseMoveFunc(posx, posy, previousX, previousY);
  31.  
  32.     previousX = posx;
  33.     previousY = posy;
  34. }

Cette fonction (onMouseMove) calcule la position courante et fournit un différentiel avec la précédente valeur. Cela permet de déplacer l’offset de la fenêtre de visualisation.

Le script principal se chargera de donner une fonction à appeler lors du onmousemove :

  1. function Move(posx, posy, previousX, previousY) {
  2.     currentAddX = (posx - previousX) / visuControl.zoom;
  3.     currentAddY = (posy - previousY) / visuControl.zoom;
  4. }
  5. MouseHelper.registerMouseMove(mainCanvas, Move);

A noter que jQuery fournit aussi des outils pour gérer les évènements souris.

Pour la gestion de la molette, nous allons devoir nous adapter aux différents navigateurs qui ne se comportent pas de la même manière sur ce sujet :

  1. function wheel(event) {
  2.     var delta = 0;
  3.     if (event.wheelDelta) {
  4.         delta = event.wheelDelta / 120;
  5.         if (window.opera)
  6.             delta = -delta;
  7.     } else if (event.detail) { /** Mozilla case. */
  8.         delta = -event.detail / 3;
  9.     }
  10.     if (delta) {
  11.         wheelFunc(delta);
  12.     }
  13.  
  14.     if (event.preventDefault)
  15.         event.preventDefault();
  16.     event.returnValue = false;
  17. }

On peut voir que chacun fait un peu ce qu’il veut Sourire.

La fonction qui permet de s’enregistrer auprès de cet évènement est la suivante :

  1. MouseHelper.registerWheel = function (func) {
  2.     wheelFunc = func;
  3.  
  4.     if (window.addEventListener)
  5.         window.addEventListener('DOMMouseScroll', wheel, false);
  6.  
  7.     window.onmousewheel = document.onmousewheel = wheel;
  8. };

Le script principal va donc gérer le zoom avec la molette :

  1. // Mouse
  2. MouseHelper.registerWheel(function (delta) {
  3.     currentAddZoom += delta / 500.0;
  4. });

Au final, la portabilité est ici un peu mise à mal mais avec un peu de JavaScript on s’en sort bien.

Pour finir nous allons rajouter un peu d’inertie sur le déplacement de la souris (et le zoom) pour donner un effet de souplesse. La fonction d’inertie va être appelé plusieurs fois par seconde et va se charger de continuer les déplacements un peu à chaque appel en réduisant à chaque fois la distance parcourue :

  1. // Inertia
  2. var inertia = 0.92;
  3. var currentAddX = 0;
  4. var currentAddY = 0;
  5. var currentAddZoom = 0;
  6.  
  7. function doInertia() {
  8.     visuControl.offsetX += currentAddX;
  9.     visuControl.offsetY += currentAddY;
  10.     visuControl.zoom += currentAddZoom;
  11.  
  12.     var effectiveTotalCardsInWidth = colsCount * cardWidth;
  13.  
  14.     var rowsCount = Math.ceil(totalCards / colsCount);
  15.     var effectiveTotalCardsInHeight = rowsCount * cardHeight
  16.  
  17.     var maxOffsetX = effectiveTotalCardsInWidth / 2.0;
  18.     var maxOffsetY = effectiveTotalCardsInHeight / 2.0;
  19.  
  20.     if (visuControl.offsetX < -maxOffsetX + cardWidth)
  21.         visuControl.offsetX = -maxOffsetX + cardWidth;
  22.     else if (visuControl.offsetX > maxOffsetX)
  23.         visuControl.offsetX = maxOffsetX;
  24.  
  25.     if (visuControl.offsetY < -maxOffsetY + cardHeight)
  26.         visuControl.offsetY = -maxOffsetY + cardHeight;
  27.     else if (visuControl.offsetY > maxOffsetY)
  28.         visuControl.offsetY = maxOffsetY;
  29.  
  30.     if (visuControl.zoom < 0.05)
  31.         visuControl.zoom = 0.05;
  32.     else if (visuControl.zoom > 1)
  33.         visuControl.zoom = 1;
  34.  
  35.     processListOfCards();
  36.  
  37.     currentAddX *= inertia;
  38.     currentAddY *= inertia;
  39.     currentAddZoom *= inertia;
  40.  
  41.     // Epsilon
  42.     if (Math.abs(currentAddX) < 0.001)
  43.         currentAddX = 0;
  44.     if (Math.abs(currentAddY) < 0.001)
  45.         currentAddY = 0;
  46. }

Ce genre de petite fonction ne coute pas cher à mettre en place mais ajoute beaucoup à la qualité du ressenti de l’utilisateur qui se trouve face à un système qui apparait comme fluide (et non brutal dans ses modifications).

Stockage de l’état

Pour fournir également une meilleure expérience utilisateur, nous allons sauvegarder la position de la fenêtre de visualisation ainsi que le zoom courant. Pour ce faire, nous allons faire appel au service du localStorage (qui permet de sauvegarder des paires de clefs/valeurs pour une durée longue (la donnée est conservée après la fermeture du navigateur) et accessible uniquement par l’objet window courant) :

  1. function saveConfig() {
  2.     if (window.localStorage == undefined)
  3.         return;
  4.  
  5.     // Zoom
  6.     window.localStorage["zoom"] = visuControl.zoom;
  7.  
  8.     // Offsets
  9.     window.localStorage["offsetX"] = visuControl.offsetX;
  10.     window.localStorage["offsetY"] = visuControl.offsetY;
  11. }
  12.  
  13. // Restore data
  14. if (window.localStorage != undefined) {
  15.     var storedZoom = window.localStorage["zoom"];
  16.     if (storedZoom != undefined)
  17.         visuControl.zoom = parseFloat(storedZoom);
  18.  
  19.     var storedoffsetX = window.localStorage["offsetX"];
  20.     if (storedoffsetX != undefined)
  21.         visuControl.offsetX = parseFloat(storedoffsetX);
  22.  
  23.     var storedoffsetY = window.localStorage["offsetY"];
  24.     if (storedoffsetY != undefined)
  25.         visuControl.offsetY = parseFloat(storedoffsetY);
  26. }

Animations

Pour rajouter encore plus de dynamisme à notre application nous allons permettre à nos utilisateurs de double-cliquer sur une carte afin de zoomer et de se centrer dessus.

Notre système doit donc animer 3 valeurs : les deux offsets (X,Y) et le zoom. Pour ce faire, nous allons utiliser une fonction qui se chargera d’animer une variable d’une valeur source à une valeur destination le tout sur une certaine durée :

  1. var AnimationHelper = function (root, name) {
  2.     var paramName = name;
  3.     this.animate = function (current, to, duration) {
  4.         var offset = (to - current);
  5.         var ticks = Math.floor(duration / 16);
  6.         var offsetPart = offset / ticks;
  7.         var ticksCount = 0;
  8.  
  9.         var intervalID = setInterval(function () {
  10.             current += offsetPart;
  11.             root[paramName] = current;
  12.             ticksCount++;
  13.  
  14.             if (ticksCount == ticks) {
  15.                 clearInterval(intervalID);
  16.                 root[paramName] = to;
  17.             }
  18.         }, 16);
  19.     };
  20. };

L’usage de cette fonction est le suivant :

  1. // Prepare animations parameters
  2. var zoomAnimationHelper = new AnimationHelper(visuControl, "zoom");
  3. var offsetXAnimationHelper = new AnimationHelper(visuControl, "offsetX");
  4. var offsetYAnimationHelper = new AnimationHelper(visuControl, "offsetY");
  5. var speed = 1.1 - visuControl.zoom;
  6. zoomAnimationHelper.animate(visuControl.zoom, 1.0, 1000 * speed);
  7. offsetXAnimationHelper.animate(visuControl.offsetX, targetOffsetX, 1000 * speed);
  8. offsetYAnimationHelper.animate(visuControl.offsetY, targetOffsetY, 1000 * speed);

L’avantage de la fonction AnimationHelper est qu’elle est capable d’animer autant de paramètres que l’on souhaite (et cela uniquement avec la fonction setTimer!)

Gestion de différents types de clients

Pour finir nous allons faire en sorte que notre page puisse aussi bien être vue sur PC que sur tablettes et même sur téléphones.

Pour ce faire, nous allons utiliser une fonctionnalité de CSS 3 : Les media-queries. Grâce à cette technologie, nous allons pouvoir appliquer des feuilles de styles spécifiques en fonction (par exemple) de la taille d’affichage :

  1. <link href="Content/full.css" rel="stylesheet" type="text/css" />
  2. <link href="Content/mobile.css" rel="stylesheet" type="text/css" media="screen and (max-width: 480px)" />
  3. <link href="Content/mobile.css" rel="stylesheet" type="text/css" media="screen and (max-device-width: 480px)" />

Ici nous voyons que si l’écran fait moins de 480 pixels, la feuille de styles suivante va être rajoutée :

  1. #legal
  2. {
  3.     font-size: 8px;    
  4. }
  5.  
  6. #title
  7. {
  8.     font-size: 30px !important;
  9. }
  10.  
  11. #waitText
  12. {
  13.     font-size: 12px;
  14. }
  15.  
  16. #bolasLogo
  17. {
  18.     width: 48px;
  19.     height: 48px;
  20. }
  21.  
  22. #pictureCell
  23. {
  24.     width: 48px;
  25. }

Cette feuille va réduire la taille de l’entête pour garder la place utile pour le canvas lorsque le navigateur fait une largeur de moins de 480 pixels (un Windows Phone par exemple) :

image

Comme on peut le voir ci-dessus, les medias-queries permettent en jouant avec les feuilles de styles d’avoir plusieurs rendus pour la même page.

Conclusion

HTML 5 / CSS 3 / JavaScript et Visual Studio 2010 permettent donc de développer des solutions clientes performantes (notamment grâce à l’utilisation du compilateur JavaScript et de l’accélération matérielle) et portable (dans la limite des navigateurs supportant le HTML 5 bien sûr).

Ce développement est également facilité par l’utilisation de frameworks tels que jQuery et par l’intégration avec des services REST proposant du JSON.

De plus, je suis particulièrement amateur de JavaScript qui s’avère être un langage dynamique très puissant. Bien entendu, il faut changer ses réflexes quand on est habitué au développement en C# par exemple, mais pour le développement de pages web cela vaut le coup. Surtout quand on mixe le développement HTML 5 / JavaScript avec de l’ASP.NET.

Pour conclure, je pense que le mieux pour se convaincre c’est encore d’essayer !

Pour aller plus loin

Leave a Comment
  • Please add 5 and 5 and type the answer here:
  • Post
  • C'est super sympa, si je puis me permettre juste une toute petite remarque : Je trouve que la molette dans son zoom est trop "brutale". Peut-être faudrait-il implémenter une sorte de "Easing Function" à la Silverlight pour rendre le zoom plus fluide ?

    En tout cas ça risque de me servir cet article !

  • Merci du retour :) Pour la molette je me demandais si je n'allais pas mettre de l'inertie dessus pour l'adoucir mais je n'ai pas trouvé le résultat top. Faut que je vois ce que je peux faire^^

  • J'ai fais la modif :) ca le fait pas mal^^

    merci !

  • Bonjour,

    Petite application sympa pour tester un peu les nouveautés html5 !

    Toutefois, petite remarque aussi : c'est pas un peu caca le <table> dans le header ? Tant qu'à faire, autant respecter les recommandations d'accessibilité ! Allez, deux chtite <div> floatante à 50% suffisent !

    Voilà, j'ai fait mon chieur :)

  • Mais carrément, je suis la honte de la jungle :D

  • J'ai corrigé du coup:) Merci! Je n'avais pas trop forcé sur le HTML :D

  • ça le fait ;)

    Ah ! Sinon, en passant, tu devrais tester Firebug avec Mozilla, qui est bien bien mieux que l'outil d'internet explorer ! Une perle même.

    En tout cas, bravo pour l'appli, y'as un tas de tites fonctionnalités à ajouter dans le temps pour les collectionneurs. Tu risque d'en ravir certains.

    Bye ! A tanto

  • je n'arrive pas à utiliser le site sur mon iPad. Comment fonctionne la navigation?

  • Hello il n'est pas prévu pour gérer le touch :(

  • Bravo pour ce site sympa pour zoomer sur les cartes !!!

    J'ADORE! :)

    Bonne continuation

Page 1 of 1 (10 items)