> Manuales > Manual de Canvas del HTML 5

En este tutorial vamos a recorrer todos los pasos necesarios para crear una biblioteca de imágenes que podemos incluir en nuestro sitio web.

Soy un entusiasta de la interfaz de usuario y no puedo resistirme a la tentación de desarrollar algo con el canvas de HTML5. Es un elemento que nos abre toda una serie de nuevas posibilidades para visualizar imágenes y datos en la web.

Introducción a la aplicación

Hoy vamos a crear una aplicación que nos permita ver una colección de tarjetas “Magic the Gathering” © (cortesía de www.wizards.com/Magic). Los usuarios podrán moverse por las imágenes y hacer zoom con el ratón (al modo de los mapas de Bing, por ejemplo).

Nota: La visualización de imágenes y datos consume muchos recursos de hardware. Revisa antes la aceleración por hardware con HTML5 para saber por qué es importante en este caso.

El resultado final lo puedes ver aquí: http://bolaslenses.catuhe.com

Los archivos fuente del proyecto se pueden descargar desde http://www.catuhe.com/msdn/bolaslenses.zip

Las tarjetas están almacenadas en Windows Azure Storage y utilizamos el Azure Content Distribution Network (CDN : un servicio de distribución de datos para usuarios finales) para conseguir los mejores rendimientos. Utilizamos un servicio de ASP.NET para recuperar la lista de tarjetas (en formato JSON).

Herramientas

Para escribir nuestra aplicación voy a utilizar Visual Studio 2010 SP1 con la Actualización de Web Standards. Esta extensión añade soporte de IntelliSense a las páginas HTML5 (algo que, como podréis ver con el tiempo, es realmente importante ).

Bien, nuestra solución va a tener una página HTML5 con archivos .js (estos son los archivos que contienen el código Javascript). Con respecto al debug, podemos fijar un punto de interrupción directamente en los archivos .js dentro de Visual Studio. Podemos hacerlo también con las Herramientas de Desarrollo F12 que tenemos en Internet Explorer 9.

Así que tenemos un entorno de desarrollo moderno, con IntelliSense y soporte para debug. Por tanto ya podemos empezar y lo primero que haremos es escribir la página HTML5.

La página HTML5

El corazón de nuestra página será un canvas HTML5 que vamos a utilizar para mostrar en él las tarjetas:

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. Tarjetas escaneadas por <a href="http://www.slightlymagic.net/">El Equipo MWSHQ</a><br />
15. Sitio web oficial de Magic the Gathering : <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. Tu navegador no soporta el canvas de HTML5.
32. </canvas>
33. <div id="stats" class="tooltip">
34. </div>
35. <div id="waitText" class="tooltip">
36. Recuperando los datos...
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 analizamos en detalle esta página, podemos ver que se divide en dos secciones:

La parte de cabecera con el título, el logo y menciones especiales

La sección principal que lleva el canvas y los mensajes que muestran el estado de la aplicación. Hay también una imagen oculta (backImage) que se utiliza como falso origen para las tarjetas que aún no se han cargado.

Para el diseño visual de la página aplicamos una hoja de estilo (full.css). Las hojas de estilo son un mecanismo que se utilizan para cambiar la apariencia de las etiquetas (en HTML un estilo define todas las opciones de presentación en pantalla de una etiqueta):

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

Así, esta hoja de estilos se encarga de dejarnos la página web con este aspecto:

Los estilos CSS son un medio extraordinariamente potente que nos permiten infinidad de combinaciones, pero algunas veces su diseño es bastante complicado (por ejemplo cuando una etiqueta queda modificada por una clase, un identificador y por su contenedor). Para simplificar este diseño, la barra de desarrollo de Internet Explorer 9 es particularmente útil, ya que nos permite ver los estilos que se aplican a una etiqueta siguiendo su estructura jerárquica.

Por ejemplo, veamos el mensaje waitText en la barra de desarrollo. Pulsamos la tecla F12 en Internet Explorer 9 y utilizamos el selector para elegir esta etiqueta:

Una vez seleccionado, podemos ver la aplicación de los estilos en orden jerárquico:

Así, por ejemplo, podemos ver que nuestro div ha recibido su estilo de la etiqueta body y la entrada .tooltip indicada en la hoja de estilos.

Con esta herramienta tenemos la posibilidad de ver los efectos de cada estilo (que además podemos desactivar si queremos). También nos permite añadir nuevos estilos sobre la marcha.

Otro punto importante en esta ventana es que nos permite cambiar el modo de presentación de Internet Explorer 9. Podemos probar, por ejemplo, cómo se ve la misma página en Internet Explorer 8. Para ello nos vamos al menú [Browser mode] y elegimos el modo de Internet Explorer 8. Este cambio va a provocar un impacto notable en nuestro mensaje porque utiliza los modificadores border-radius (para redondear esquinas) y box-shadow que son funcionalidades de CSS 3:

Nuestra página incluye una “degradación elegante” que le permite seguir funcionando (sin diferencias visuales sustanciales) cuando el navegador no soporta las tecnologías necesarias.

Ahora que tenemos lista nuestra interfaz, vamos a ver el código fuente que recupera las tarjetas y las muestra en pantalla.

Recuperación de los datos

El servidor nos suministra la lista de tarjetas utilizando el formato JSON en esta URL: http://bolaslenses.catuhe.com/Home/ListOfCards/?colorString=0

Lleva un parámetro (colorString) para elegir un color concreto (en este caso 0 = todos).

Al desarrollar con Javascript siempre tenemos que hacer una reflexión preliminar (reflexión que también se aplica a todos los demás lenguajes, pero que es ciertamente importante en el caso de Javascript): tenemos que preguntarnos primero si lo que queremos programar no estará ya hecho en algún entorno conocido.

Lo que suele ocurrir es que hay montones de proyectos de código abierto hechos en Javascript. Uno de ellos es jQuery que nos aporta infinidad de servicios sumamente cómodos y útiles.

Así, en nuestro caso por ejemplo, para conectar la URL de nuestro servidor y obtener la lista de las tarjetas, podríamos utilizar una sentencia XmlHttpRequest y pasar un rato programando la traducción del JSON que nos devuelve. O bien podemos utilizar jQuery .

Así que voy a utilizar la función getJSON que dará buena cuenta de todo esto evitándonos el trabajo:

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 + " tarjetas mostradas");
6. $("#waitText").slideToggle("fast");
7. });
8. }

Como puedes ver, nuestra función guarda la lista de tarjetas en la variable listOfCards y hace llamadas a un par de funciones jQuery:

La variable listOfCards contiene objetos cuyo formato es:

Conviene recordar que la URL del servidor se invoca con el sufijo “?jsoncallback=?”. Por lo que respecta a las llamadas Ajax, tienen ciertas restricciones en términos de seguridad, de modo que solo nos dejan conectarnos a la misma dirección que la del script que hace la llamada. No obstante, tenemos una solución para este inconveniente, llamado JSONP que nos permite hacer una llamada concertada al servidor (que, obviamente, tiene que aceptar esta operación). Y por suerte, jQuery puede manejar todo ello por sí solo simplemente añadiendo el sufijo correspondiente.

Una vez que tenemos ya a mano la lista de tarjetas, podemos empezar con la parte encargada de la descarga y cacheo de las fotos.

Manejo de la carga y cacheo de imágenes

El truco de nuestra aplicación consiste en pintar únicamente las tarjetas que realmente se pueden ver en la pantalla. La ventana se define por un nivel de ampliación (zoom) y un desplazamiento (x,y) con respecto al sistema general.

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

El sistema general viene definido por un total de 14.819 tarjetas que se distribuyen en 200 columnas y 75 filas.

Además tenemos que tener en cuenta que cada tarjeta la tenemos disponible en tres versiones:

Así que, dependiendo del nivel de zoom, tendremos que cargar la versión correspondiente para optimizar la transferencia de datos por la red.

Para ello vamos a escribir una función que muestra una imagen para una tarjeta dada. Esta función se configurará para que descargue la imagen con un nivel de calidad concreto. Además estará enlazada con la misma imagen en menor resolución para devolverla en caso de que la imagen no estuviera disponible en el nivel de resolución actual:

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

El valor de ImageCache se genera poniendo el sufijo asociado y la cache subyacente.

Aquí tenemos dos funciones importantes:

Por tanto, para mantener nuestros 3 niveles de cache tenemos que declarar tres variables:

1. var imagesCache25 = new imageCache(".25.jpg");
2. var imagesCache50 = new imageCache(".50.jpg", imagesCache25);
3. var imagesCacheFull = new imageCache(".jpg", imagesCache50);
La selección de la versión depende únicamente del 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. }

Para informar al usuario, hemos añadido un timer que controla un mensaje de texto donde se va indicando el número de imágenes que ya tenemos cargadas:

1. function updateStats() {
2. var stats = $("#stats");
3. 
4. stats.html(currentDownloads + " tarjeta(s) cargada(s).");
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);

Como antes, vemos que con jQuery se simplifica el trabajo de animación.

Y ahora vamos a ver cómo se muestran las tarjetas en la pantalla.

Visualización de las tarjetas

Para mostrar las tarjetas necesitamos rellenar el canvas mediante el uso de su contexto 2D (que solo podemos utilizar si el navegador soporta el canvas de HTML5):

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

La función encargada de representar las imágenes es processListOfCards (a la que se llama 60 veces por segundo):

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. // Limpieza
24. clearCanvas();
25. 
26. // Calcula el área visible
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. // Acceso a la cache
37. var imageCache = getCorrectImageCache();
38. 
39. // Restitución de imágenes
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. // Recorte
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. // Saca la imagen de la cache
64. var img = imageCache.getImageForCard(card);
65. 
66. // Muestra en pantalla
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. // Barras de desplazamiento
81. drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY);
82. 
83. // FPS 
84. computeFPS();
85. }

En esta function ocurren muchas cosas de gran importancia:

Si la lista de tarjetas aún no está cargada, se muestra un mensaje indicando que la descarga está en proceso:

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

El siguiente paso consiste en definir la posición de la ventana (en términos de tarjetas y coordenadas) y después procedemos a limpiar el 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. }

A continuación recorremos la lista de tarjetas y llamamos a la función drawImage en el contexto del canvas. La imagen en curso se extrae de la cache activa (que depende del zoom que esté aplicado en ese momento):

1. // Obtiene de la cache
2. var img = imageCache.getImageForCard(card);
3. 
4. // La pone en el canvas
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. });

Tenemos que mostrar también la barra de desplazamiento con la función RoundedRectangle que aplica curvas cuadráticas:

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

Y finalmente, necesitamos calcular el número de frames por segundo:

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

La restitución de las tarjetas en pantalla depende de manera directa de la capacidad del navegador para acelerar el refresco del mapa de bits que compone el canvas. A efectos informativos, los rendimientos que obtengo en mi máquina con el nivel mínimo de zoom (0,05) son estos:

Navegador FPS
Internet Explorer 9 30
Firefox 5 30
Chrome 12 17
iPad (con nivel de zoom 0,8) 7
Windows Phone Mango (con nivel de zoom 0,8) 20 (!!)

Este sitio web funciona incluso en teléfonos móviles y tablets compatibles con HTML5.

Aquí es donde se ve la potencia que desarrollan los navegadores HTML5, capaces de mostrar una pantalla llena de tarjetas ¡más de 30 veces por segundo! y esto es posible gracias a la aceleración por hardware.

Manejo del ratón

Para movernos por la galería de tarjetas necesitamos controlar el ratón (incluyendo, además de los botones y el cursor, la rueda también).

Para ello vamos a manejar los eventos onmouvemove, onmouseup y onmousedown.

Los eventos Onmouseup y onmousedown los vamos a utilizar para detectar si el usuario ha pulsado el botón izquierdo o no:

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

El evento onmousemove se asocia al canvas y se utiliza para desplazar la vista:

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

Esta función (onMouseMove) calcula la posición actual y nos devuelve también el valor anterior para calcular el desplazamiento total de la ventana:

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

Como puedes ver, jQuery dispone también de herramientas para el manejo de eventos del ratón.

Para controlar la rueda tenemos que contar con que no todos los navegadores lo hacen de la misma forma:

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

Al final, ya ves que cada uno hace lo que le da la gana .

La función para registrar el evento es:

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

Y vamos a utilizar esta función para cambiar el zoom con la rueda:

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

Finalmente añadimos un toque de inercia al mover el ratón (y el zoom), para suavizar los desplazamientos:

1. // Inercia
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. }

Esta pequeña función no cuesta mucho implementarla, pero añade una gran calidad visual a la experiencia del usuario.

Almacenamiento del estado

Además de lo visto, para conseguir una experiencia de usuario mejor vamos a salvar la posición de la ventana y el valor de zoom. Para ello utilizaremos el servicio de localStorage (que guarda pares de claves/valores a largo plazo), ya que retiene los datos después de cerrar el navegador y están accesibles únicamente para el objeto window actual:

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. // recupera los datos
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. }

Animaciones

Para añadir más dinamismo aún a nuestra aplicación vamos a hacer que pulsando doble click sobre una tarjeta, se amplíe y se centre.

Nuestro sistema genera la animación a partir de tres valores: los dos desplazamientos (Y, Y) y el zoom. Para ello vamos a utilizar una función que se encargará de graduar el valor de una variable desde un valor inicial a un valor final en un lapso de tiempo dado:

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

Esta función se utiliza así:

1. // Preparamos los parámetros de la animación
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);

La ventaja de la función AnimationHelper es que es capaz de alterar todos los parámetros que queramos (¡y solo utiliza la función setTimer!)

Manejo de múltiples dispositivos

Finalmente tenemos que cerciorarnos de que nuestra página también se puede ver en tablets e incluso teléfonos móviles.

Para eso utilizamos una funcionalidad de CSS3 llamada media-queries. Con esta tecnología podemos aplicar hojas de estilo dependiendo del resultado de ciertas consultas al sistema, como por ejemplo para averiguar si la pantalla tiene un tamaño concreto:

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

Aquí vemos que si la pantalla es de menos de 480 pixels, se aplicará la siguiente hoja de estilos:

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

Esta hoja reduce el tamaño de la cabecera para que la página siga teniendo una amplia zona visible aunque el ancho del navegador sea inferior a 480 pixels (como ocurre, por ejemplo, en Windows Phone):

Conclusión

TML5 / CSS 3 / JavaScript y Visual Studio 2010 nos permiten desarrollar soluciones eficientes y portables (siempre contando con que los navegadores sean compatibles con HTML5, por supuesto), aprovechando innovaciones como la aceleración de gráficos por hardware.

Este tipo de desarrollos ahora es mucho más sencillo, utilizando ciertos marcos de programación como jQuery.

Además me gusta Javascript de forma especial, y sobre todo ahora que empieza a convertirse en un lenguaje dinámico y de gran potencia. Obviamente, los desarrolladores de C# o VB.NET van a tener que modificar sus hábitos, pero para los desarrolladores de páginas web es una gran noticia.

Resumiendo, creo que lo mejor para convencerse de ello es probarlo.

Para más información

David Catuhe

desarrollador evangelista de Microsoft France

Manual