As a
user interface fan, I could not miss the opportunity to develop with HTML5
Canvas. It unlocks a whole new set of ways to visualize images and data on the
web. In this tutorial, I’ll walk you through how to create one for your site.
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).
Note:
Image and data visualization is hardware intensive. Learn about HTML5
hardware acceleration and why it’s important here.
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).
Tools
To
write our application, we will use Visual Studio 2010 SP1 with Web
Standards Update. This extension adds IntelliSense support
in HTML5 page (which is a really important thing ).
So,
our solution will contain an HTML5 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. Try using the F12
Developer tools in Internet Explorer 9.
Debug with Visual Studio 2010
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 HTML5
page.
The HTML5 Page
Our
page will be built around an HTML5 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:17.<div id="cardsCount">
18.</div>
19.</div>
20.<div id="leftHeader">
21.<img id="pictureCell" src="http://www.codeproject.com/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:
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 9 and use
the selector to choose the tooltip:
Once
the selection is done, we can see the styles hierarchy:
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:
|
|
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 .
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 };
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 HTML5 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.24.clearCanvas();
25.
26.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.37.var imageCache = getCorrectImageCache();
38.
39.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.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.64.var img = imageCache.getImageForCard(card);
65.
66.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.81.drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY);
82.
83.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. 2. var img = imageCache.getImageForCard(card);
3.
4. 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. 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.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):
Browser |
FPS |
Internet Explorer 9M |
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 HTML5.
Here we can see the inner power of HTML5 browsers that can handle a full screen
of cards more than 30 times per second! This is possible
through hardware
acceleration.
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) {
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 .
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. 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. 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.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. 6. window.localStorage["zoom"] = visuControl.zoom;
7.
8. 9. window.localStorage["offsetX"] = visuControl.offsetX;
10.window.localStorage["offsetY"] = visuControl.offsetY;
11.}
12.
13.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. 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):
Conclusion
HTML5
/ CSS 3 / JavaScript and Visual Studio 2010
allow to develop portable and efficient solutions (within the limits of
browsers that support HTML5 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