Monday, November 4, 2024

Create a Visual Image Library Using HTML5 Canvas: Cards Loading & Display and Cache Handling

written by David Catuhe

Last week in our tutorial on How To Create a Visual Library of Images in HTML5 Canvas, we showed you the HTML5 code from the project, and discussed the data gathering process. This week we will tell you about cards loading and display, as well as how cache is handled in our application.

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: 480×680 without compression (.jpg suffix)
  • Medium definition: 240×340 with standard compression (.50.jpg suffix)
  • Low definition: 120×170 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. this.load = function (card) {
  5. var localCache = this;
  6. if (this[card.ID] != undefined)
  7. return;
  8. var img = new Image();
  9. localCache[card.ID] = { image: img, isLoaded: false };
  10. currentDownloads++;
  11. img.onload = function () {
  12. localCache[card.ID].isLoaded = true;
  13. currentDownloads–;
  14. };
  15. img.onerror = function() {
  16. currentDownloads–;
  17. };
  18. img.src = “http://az30809.vo.msecnd.net/” + card.Path + extension;
  19. };
  20. this.getReplacementFromLowerCache = function (card) {
  21. if (replacementCache == undefined)
  22. return backImage;
  23. return replacementCache.getImageForCard(card);
  24. };
  25. this.getImageForCard = function(card) {
  26. var img;
  27. if (this[card.ID] == undefined) {
  28. this.load(card);
  29. img = this.getReplacementFromLowerCache(card);
  30. }
  31. else {
  32. if (this[card.ID].isLoaded)
  33. img = this[card.ID].image;
  34. else
  35. img = this.getReplacementFromLowerCache(card);
  36. }
  37. return img;
  38. };
  39. }

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. if (visuControl.zoom <= 0.8)
  5. return imagesCache50;
  6. return imagesCacheFull;
  7. }

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. stats.html(currentDownloads + ” card(s) currently downloaded.”);
  4. if (currentDownloads == 0 && statsVisible) {
  5. statsVisible = false;
  6. stats.slideToggle(“fast”);
  7. }
  8. else if (currentDownloads > 1 && !statsVisible) {
  9. statsVisible = true;
  10. stats.slideToggle(“fast”);
  11. }
  12. }
  13. 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. if (listOfCards == undefined) {
  3. drawWaitMessage();
  4. return;
  5. }
  6. mainCanvas.width = document.getElementById(“center”).clientWidth;
  7. mainCanvas.height = document.getElementById(“center”).clientHeight;
  8. totalCards = listOfCards.length;
  9. var localCardWidth = cardWidth * visuControl.zoom;
  10. var localCardHeight = cardHeight * visuControl.zoom;
  11. var effectiveTotalCardsInWidth = colsCount * localCardWidth;
  12. var rowsCount = Math.ceil(totalCards / colsCount);
  13. var effectiveTotalCardsInHeight = rowsCount * localCardHeight;
  14. initialX = (mainCanvas.width – effectiveTotalCardsInWidth) / 2.0 – localCardWidth / 2.0;
  15. initialY = (mainCanvas.height – effectiveTotalCardsInHeight) / 2.0 – localCardHeight / 2.0;
  16. // Clear
  17. clearCanvas();
  18. // Computing of the viewing area
  19. var initialOffsetX = initialX + visuControl.offsetX * visuControl.zoom;
  20. var initialOffsetY = initialY + visuControl.offsetY * visuControl.zoom;
  21. var startX = Math.max(Math.floor(-initialOffsetX / localCardWidth) – 1, 0);
  22. var startY = Math.max(Math.floor(-initialOffsetY / localCardHeight) – 1, 0);
  23. var endX = Math.min(startX + Math.floor((mainCanvas.width – initialOffsetX – startX * localCardWidth) / localCardWidth) + 1, colsCount);
  24. var endY = Math.min(startY + Math.floor((mainCanvas.height – initialOffsetY – startY * localCardHeight) / localCardHeight) + 1, rowsCount);
  25. // Getting current cache
  26. var imageCache = getCorrectImageCache();
  27. // Render
  28. for (var y = startY; y < endY; y++) {
  29. for (var x = startX; x < endX; x++) {
  30. var localX = x * localCardWidth + initialOffsetX;
  31. var localY = y * localCardHeight + initialOffsetY;
  32. // Clip
  33. if (localX > mainCanvas.width)
  34. continue;
  35. if (localY > mainCanvas.height)
  36. continue;
  37. if (localX + localCardWidth < 0)
  38. continue;
  39. if (localY + localCardHeight < 0)
  40. continue;
  41. var card = listOfCards[x + y * colsCount];
  42. if (card == undefined)
  43. continue;
  44. // Get from cache
  45. var img = imageCache.getImageForCard(card);
  46. // Render
  47. try {
  48. if (img != undefined)
  49. drawingContext.drawImage(img, localX, localY, localCardWidth, localCardHeight);
  50. } catch (e) {
  51. $.grep(listOfCards, function (item) {
  52. return item.image != img;
  53. });
  54. }
  55. }
  56. };
  57. // Scroll bars
  58. drawScrollBars(effectiveTotalCardsInWidth, effectiveTotalCardsInHeight, initialOffsetX, initialOffsetY);
  59. // FPS
  60. computeFPS();
  61. }

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. function drawWaitMessage() {
  3. pointCount++;
  4. if (pointCount > 200)
  5. pointCount = 0;
  6. var points = “”;
  7. for (var index = 0; index < pointCount / 10; index++)
  8. points += “.”;
  9. $(“#waitText”).html(“Loading…Please wait<br>” + points);
  10. }
  • 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. drawingContext.fillStyle = “rgb(0, 0, 0)”;
  5. drawingContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
  6. }
  • 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. // Render
  4. try {
  5. if (img != undefined)
  6. drawingContext.drawImage(img, localX, localY, localCardWidth, localCardHeight);
  7. } catch (e) {
  8. $.grep(listOfCards, function (item) {
  9. return item.image != img;
  10. });
  • 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. // Vertical
  5. var totalScrollHeight = effectiveTotalCardsInHeight + mainCanvas.height;
  6. var scaleHeight = mainCanvas.height – 20;
  7. var scrollHeight = mainCanvas.height / totalScrollHeight;
  8. var scrollStartY = (-initialOffsetY + mainCanvas.height * 0.5) / totalScrollHeight;
  9. roundedRectangle(mainCanvas.width – 8, scrollStartY * scaleHeight + 10, 5, scrollHeight * scaleHeight, 4);
  10. // Horizontal
  11. var totalScrollWidth = effectiveTotalCardsInWidth + mainCanvas.width;
  12. var scaleWidth = mainCanvas.width – 20;
  13. var scrollWidth = mainCanvas.width / totalScrollWidth;
  14. var scrollStartX = (-initialOffsetX + mainCanvas.width * 0.5) / totalScrollWidth;
  15. roundedRectangle(scrollStartX * scaleWidth + 10, mainCanvas.height – 8, scrollWidth * scaleWidth, 5, 4);
  16. }
  • 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. for (var id = 0; id < previous.length – 1; id++) {
  9. sum += previous[id + 1] – previous[id];
  10. }
  11. var diff = 1000.0 / (sum / previous.length);
  12. $(“#cardsCount”).text(diff.toFixed() + ” fps. “ + listOfCards.length + ” cards displayed”);
  13. }

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!

In our next segment of this tutorial, we will delve into mouse management and state storage.

This article was reprinted with permission from Microsoft Corporation. The original is available here. This site does business with Microsoft Corporation.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured