Feedback of a graphic development using HTML5 & JavaScript - Eternal Coding - HTML5 / JavaScript / 3D development - Site Home - MSDN Blogs

Feedback of a graphic development using HTML5 & JavaScript


 

Feedback of a graphic development using HTML5 & JavaScript

  • Comments 4

As a user interface fan, I could not miss the development with HTML 5.

So the goal of this post is to walk through a graphic application that uses JavaScript and HTML 5. We will see through examples one way (among others) to develop this kind of project.

Application overview image_thumb6 
Tools 
The HTML 5 page
Data gathering 
Cards loading & cache handling
Cards display
Mouse management  
State storage
Animations
Handling multi-devices
Conclusion
To go further

Application overview

We will produce an application that will let us display a Magic the Gathering ©(courtesy of www.wizards.com/Magic) cards collection. Users will be able to scroll and zoom using the mouse (like Bing Maps, for example).

image

You can see the final result here: http://bolaslenses.catuhe.com

The project source files can be downloaded here: http://www.catuhe.com/msdn/bolaslenses.zip

Cards are stored on Windows Azure Storage and use the Azure Content Distribution Network (CDN : a service that deploys data near the final users) in order to achieve maximum performances. An ASP.NET service is used to return cards list (using JSON format). 

image

Tools

To write our application, we will use Visual Studio 2010 SP1 with Web Standards Update. This extension adds IntelliSense support in HTML 5 page (which is a really important thing Sourire).

So, our solution will contain an HTML 5 page side by side with .js files (these files will contain JavaScript scripts). About debug, it is possible to set a breakpoint directly in the .js files under Visual Studio. It is also possible to use the developer bar of Internet Explorer 9 (use F12 key to display it).

image

Debug with Visual Studio 2010

image

Debug with Internet Explorer 9 (F12/Developer bar)

So, we have a modern developer environment with IntelliSense and debug support. Therefore, we are ready to start and first of all, we will write the HTML 5 page.

image

The HTML 5 page

Our page will be built around an HTML 5 canvas which will be used to draw the cards:

  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>

If we dissect this page, we can note that it is divided into two parts:

  • The header part with the title, the logo and the special mentions
  • The main part (section) holds the canvas and the tooltips that will display the status of the application. There is also a hidden image (backImage) used as source for not yet loaded cards.

To build the layout of the page, a style sheet (full.css) is applied. Style sheets are a mechanism used to change the tags styles (in HTML, a style defines the entire display options for a tag):

  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. }

Thus, this sheet is responsible for setting up the following display:

untitled

Style sheets are powerful tools that allow an infinite number of displays. However, they are sometimes complicated to setup (for example if a tag is affected by a class, an identifier and its container). To simplify this setup, the development bar of Internet Explorer 9 is particularly useful because we can use it to see styles hierarchy that is applied to a tag.
For example let’s take a look at the waitText tooltip with the development bar. To do this, you must press F12 in Internet Explorer and use the selector to choose the tooltip:

imageOnce the selection is done, we can see the styles hierarchy:

image

Thus, we can see that our div received its styles from the body tag  and the .tooltip entry of the style sheet.

With this tool, it becomes possible to see the effect of each style (which can be disabled). It is also possible to add new style on the fly.

Another important point of this window is the ability to change the rendering mode of Internet Explorer 9. Indeed, we can test how, for example, Internet Explorer 8 will handle the same page. To do this, go to the [Browser mode] menu and select the engine of Internet Explorer 8. This change will especially impact our tooltip as it uses border-radius (rounded edge) and box-shadow that are features of CSS 3:


image image

Internet Explorer 9

Internet Explorer 8

Our page provides a graceful degradation  as it still works (with no annoying visual difference) when the browser does not support all the required technologies.

Now that our interface is ready, we will take a look at the data source to retrieve the cards to display.

Data gathering

The server provides the cards list using JSON format on this URL:

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

It takes one parameter (colorString) to select a specific color (0 = all).

When developing with JavaScript, there is a good reflex to have (reflex also good in other languages too, but really important in JavaScript): one must ask whether what we want to develop has not been already done in an existing framework.

Indeed, there is a multitude of open source projects around JavaScript. One of them is jQuery which provides a plethora of convenient services.

Thus, in our case to connect to the URL of our server and get the cards list, we could go through a XmlHttpRequest and have fun to parse the returned JSON. Or we can use jQuery Sourire.

So we will use the getJSON function which will take care of everything for us:

  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. }

As we can see, our function stores the cards list in the listOfCards variable and calls two jQuery functions:

  • text that change the text of a tag
  • slideToggle that hides (or shows) a tag by animating its height

The listOfCards list contains objects whose format is:

  • ID: unique identifier of the card
  • Path: relative path of the card (without the extension)

It should be noted that the URL of the server is called with the “?jsoncallback=?” suffix. Indeed, Ajax calls are constrained in terms of security to connect only to the same address as the calling script. However, there is a solution called JSONP that will allow us to make a concerted call to the server (which of course must be aware of the operation). And fortunately, jQuery can handle it all alone by just adding the right suffix.

Once we have our cards list, we can set up the pictures loading and caching.

Cards loading & cache handling

The main trick of our application is to draw only the cards effectively visible on the screen. The display window is defined by a zoom level and an offset (x, y) in the overall system.

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

image

The overall system is defined by 14819 cards that are spread over 200 columns and 75 rows.

Also, we must be aware that each card is available in three versions:

  • High definition: 480x680 without compression (.jpg suffix)
  • Medium definition: 240x340 with standard compression (.50.jpg suffix)
  • Low definition: 120x170 with strong compression (.25.jpg suffix)

Thus, depending on the zoom level, we will load the correct version to optimize networks transfer.

To do this we will develop a function that will give an image for a given card. This function will be configured to download a certain level of quality. In addition it will be linked with lower quality level to return it if the card for the current level is not yet uploaded:

  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. }

An ImageCache is built by giving the associated suffix and the underlying cache.

Here you can see two important functions:

  • load: this function will load the right picture and will store it in a cache (the msecnd.net url is the Azure CDN address of the cards)
  • getImageForCard: this function returns the card picture from the cache if already loaded. Otherwise it requests the underlying cache to return its version (and so on)

So to handle our 3 levels of caches, we have to declare three variables:

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

Selecting the right cover is only depending on 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. }

To give a feedback to the user, we will add a timer that will manage a tooltip that indicates the number of images currently loaded:

  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);

Again we note the use of jQuery to simplify animations.

We will now discuss the display of cards.

Cards display

To draw our cards, we need to actually fill the canvas using its 2D context (which exists only if the browser supports HTML 5 canvas):

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

The drawing will be made by processListOfCards function (called 60 times per second):

  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. }

This function is built around many key points:

  • If the cards list is not yet loaded, we display a tooltip indicating that download is in progress::
  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. }
  • Subsequently, we define the position of the display window (in terms of cards and coordinates), then we proceed to clean the 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. }
  • Then we browse the cards list and call the drawImage function of the canvas context. The current image is provided by the active cache (depending on the 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.     });
  • We also have to draw the scroll bar with the RoundedRectangle function that uses quadratic curves:
  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. }
  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. }
  • And finally, we need to compute the number of frames per second:
  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. }

Drawing cards relies heavily on the browser's ability to speed up canvas rendering. For the record, here are the performances on my machine with the minimum zoom level (0.05):

image

Browser

FPS

Internet Explorer 9 30
Firefox 5 30
Chrome 12 17
iPad (with a zoom level of 0.8) 7
Windows Phone Mango (with a zoom level of 0.8) 20 (!!)

The site even works on mobile phones and tablets as long as they support HTML 5.

Here we can see the inner power of HTML 5 browsers that can handle a full screen of cards more than 30 times per second!

Mouse management

To browse our cards collection, we have to manage the mouse (including its wheel).

For the scrolling, we'll just handle the onmouvemove, onmouseup and onmousedown events.

Onmouseup and onmousedown events will be used to detect if the mouse is clicked or not:

  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. };

The onmousemove event is connected to the canvas and used to move the view:

  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. }

This function (onMouseMove) calculates the current position and provides also the previous value in order to move the offset of the display window:

  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);

Note that jQuery also provides tools to manage mouse events.

For the management of the wheel, we will have to adapt to different browsers that do not behave the same way on this point:

  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. }

We can see that everyone does what he wants Sourire.

The function to register with this event is:

  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. };

And we will use this function to change the zoom with the wheel:

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

Finally we will add a bit of inertia when moving the mouse (and the zoom) to give some kind of smoothness:

  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. }

This kind of small function does not cost a lot to implement, but adds a lot to the quality of user experience.

State storage

Also to provide a better user experience, we will save the display window’s position and zoom. To do this, we will use the service of localStorage (which saves pairs of keys / values ​​for the long term (the data is retained after the browser is closed) and only accessible by the current window object):

  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

To add even more dynamism to our application we will allow our users to double-click on a card to zoom and focus on it.

Our system should animate three values: the two offsets (X, Y) and the zoom. To do this, we will use a function that will be responsible of animating a variable from a source value to a destination value with a given duration:

  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. };

The use of this function is:

  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);

The advantage of the AnimationHelper function is that it is able to animate as many parameters as you wish (and that only with the setTimer function!)

Handling multi-devices

Finally we will ensure that our page can also be seen on tablets PC and even on phones.

To do this, we will use a feature of CSS 3: The media-queries. With this technology, we can apply style sheets according to some queries such as a specific display size:

  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)" />

Here we see that if the screen width is less than 480 pixels, the following style sheet will be added:

  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. }

This sheet will reduce the size of the header to keep the site viewable even when the browser width is less than 480 pixels (for example, on a Windows Phone):image

Conclusion

HTML 5 / CSS 3 / JavaScript and Visual Studio 2010 allow to develop portable and efficient solutions (within the limits of browsers that support HTML 5 of course) with some great features such as hardware accelerated rendering.

This kind of development is also simplified by the use of frameworks like jQuery.

Also, I am especially fan of JavaScript that turns out to be a very powerful dynamic language. Of course, C# or VB.NET developers have to change theirs reflexes but for the development of web pages it's worth.

In conclusion, I think that the best to be convinced is to try!

To go further

Leave a Comment
  • Please add 4 and 7 and type the answer here:
  • Post
  • It's a nice idea but entirely non-useful for several reasons:

    1. No-one looks at a collection of Magic cards like that, it's simply not useful to do, the information is overwhelming and hard to track.

    2. You need to be able to sort cards by various values (cost, rarity, type, etc) which this does not look like it can do.

    3. Collections are not lots of single cards, they are lots of *multiple* cards. This does not indicate how many of the same card a collection has.

    IN SUMMARY: Nice idea, nice execution, not practical in any sense for Magic the Gathering cards.

  • I totally agree with that. The goal of the application ismainly educational to serve as a sample for the blog

  • If you want a tool to manage EFFECTIVELY your collection, please have a loolk at another tool I wrote :

    urzagatherer.codeplex.com

  • the goal wasn't to manage magic cards - it seems to me it was just to demonstrate some capabilities of HTML5 and I think you did a great job with that.  Very useful to me as I'm just starting to get a foothold on this new stuff

Page 1 of 1 (4 items)