65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用 HTML5 Canvas 创建图像的视觉库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2011 年 10 月 26 日

CPOL

11分钟阅读

viewsIcon

69174

作为用户界面爱好者,我不能错过使用 HTML5 Canvas 进行开发的这次机会。它为在 Web 上可视化图像和数据开辟了全新的方式。在本教程中,我将指导您如何为您的网站创建一个。

作为用户界面爱好者,我不能错过使用 HTML5 Canvas 进行开发的这次机会。  它为在 Web 上可视化图像和数据开辟了全新的方式。  在本教程中,我将指导您如何为您的网站创建一个。

应用程序概述

我们将创建一个应用程序,允许我们显示《万智牌》©(鸣谢 www.wizards.com/Magic)的卡牌收藏。用户将能够使用鼠标滚动和缩放(例如,类似必应地图)。

注意:图像和数据可视化对硬件要求很高。  了解 HTML5 硬件加速及其重要性

Library-Images-Canvas/image002.png

您可以在这里看到最终结果:http://bolaslenses.catuhe.com

项目源文件可在此处下载:http://www.catuhe.com/msdn/bolaslenses.zip

卡片存储在Windows Azure Storage 中,并使用 Azure 内容分发网络(CDN:一项将数据部署到最终用户附近的服务)以实现最佳性能。ASP.NET 服务用于返回卡片列表(使用JSON 格式)。

Library-Images-Canvas/image003.png

工具

为了编写我们的应用程序,我们将使用 Visual Studio 2010 SP1 和Web 标准更新。此扩展为 HTML5 页面添加了 IntelliSense 支持(这非常重要)。

因此,我们的解决方案将包含一个 HTML5 页面,旁边是 .js 文件(这些文件将包含 JavaScript 脚本)。关于调试,可以在 Visual Studio 下直接在 .js 文件中设置断点。尝试使用 Internet Explorer 9 中的F12 开发人员工具

Library-Images-Canvas/image005.png

使用 Visual Studio 2010 进行调试

Library-Images-Canvas/image006.png

使用 Internet Explorer 9(F12/开发人员栏)进行调试

因此,我们拥有了一个支持 IntelliSense 和调试的现代开发环境。因此,我们已准备好开始,首先,我们将编写 HTML5 页面。

Library-Images-Canvas/image007.png

HTML5 页面

我们的页面将围绕一个 HTML5 canvas 构建,它将用于绘制卡片

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="https://codeproject.org.cn/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>

如果我们剖析这个页面,我们可以注意到它分为两部分

  • 标题部分,包含标题、徽标和特别说明
  • 主部分(section)包含 canvas 和用于显示应用程序状态的工具提示。还有一个隐藏的图像(backImage),用作尚未加载的卡片的源。

为了构建页面布局,将应用一个样式表(full.css)。样式表是用于更改标签样式的机制(在 HTML 中,样式定义了标签的整个显示选项)。

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

因此,此样式表负责设置以下显示

Library-Images-Canvas/image008.png

样式表是强大的工具,允许无限数量的显示。但是,它们有时很难设置(例如,如果一个标签受到类、ID 及其容器的影响)。为了简化此设置,Internet Explorer 9 的开发人员栏特别有用,因为我们可以使用它来查看应用于标签的样式层次结构。

例如,让我们看一下 waitText 工具提示,使用开发人员栏。为此,您必须在 Internet Explorer 9 中按 F12,然后使用选择器选择工具提示。

Library-Images-Canvas/image009.png

选择完成后,我们可以看到样式层次结构。

Library-Images-Canvas/image010.png

因此,我们可以看到我们的 divbody 标签和样式表中的 .tooltip 条目接收了样式。

使用此工具,可以查看每个样式的效果(可以禁用)。还可以即时添加新样式。

此窗口的另一个重要点是更改 Internet Explorer 9 渲染模式的能力。事实上,我们可以测试例如 Internet Explorer 8 将如何处理同一页面。为此,请转到 [浏览器模式] 菜单,然后选择 Internet Explorer 8 的引擎。此更改将特别影响我们的工具提示,因为它使用了 CSS 3 的 border-radius(圆角)和 box-shadow 功能。

Library-Images-Canvas/image011.png Library-Images-Canvas/image012.png
Internet Explorer 9 Internet Explorer 8

我们的页面提供了优雅降级,即使浏览器不支持所有必需技术,它仍然可以正常工作(没有令人讨厌的视觉差异)。

现在我们的界面已准备就绪,我们将查看数据源以检索要显示的卡片。

数据收集

服务器以JSON 格式在此 URL 上提供卡片列表。

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

它接受一个参数(colorString)来选择特定的颜色(0 = 全部)。

在 JavaScript 开发中,有一个很好的习惯(在其他语言中也很好,但在 JavaScript 中尤其重要):我们必须问自己,我们想开发的东西是否已经在现有框架中完成过了。

确实,JavaScript 周围有大量的开源项目。其中之一是jQuery,它提供了许多便捷的服务。

因此,在我们的案例中,要连接到服务器 URL 并获取卡片列表,我们可以通过 XmlHttpRequest 进行,然后自己解析返回的 JSON。或者我们可以使用 jQuery。

因此,我们将使用 getJSON 函数,它会为我们处理所有事情。

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

正如我们所见,我们的函数将卡片列表存储在 listOfCards 变量中,并调用两个 jQuery 函数。

  • text,用于更改标签文本。
  • slideToggle,用于通过动画高度来隐藏(或显示)标签。

listOfCards 列表包含格式如下的对象:

  • ID:卡的唯一标识符。
  • Path:卡的相对路径(不含扩展名)。

应注意的是,服务器 URL 调用时带有“?jsoncallback=?”后缀。事实上,Ajax 调用在安全方面受到限制,只能连接到调用脚本的同一地址。但是,有一个名为 JSONP 的解决方案,它允许我们进行一次协调的服务器调用(当然,服务器必须意识到该操作)。幸运的是,jQuery 仅通过添加正确的后缀就可以单独处理这一切。

一旦我们有了卡片列表,我们就可以设置图片加载和缓存。

卡片加载和缓存处理

我们应用程序的主要技巧是仅绘制屏幕上实际可见的卡片。显示窗口由缩放级别和整个系统中的偏移量(x、y)定义。

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

Library-Images-Canvas/image013.png

整个系统由14819 张卡片组成,这些卡片分布在200 列和75 行上。

此外,我们必须意识到每张卡片都有三个版本:

  • 高分辨率:480x680,无压缩(.jpg 后缀)。
  • 中等分辨率:240x340,标准压缩(.50.jpg 后缀)。
  • 低分辨率:120x170,强压缩(.25.jpg 后缀)。

因此,根据缩放级别,我们将加载正确的版本以优化网络传输。

为此,我们将开发一个函数,该函数将为给定卡片提供图像。此函数将被配置为下载特定质量级别的图片。此外,它将与较低质量的级别链接,以便在当前级别的卡片尚未上传时返回它。

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

通过提供关联的后缀和底层缓存来构建 ImageCache

在这里,您可以看到两个重要函数:

  • load:此函数将加载正确的图片并将其存储在缓存中(msecnd.net URL 是卡片的 Azure CDN 地址)。
  • getImageForCard:如果卡片图片已加载,此函数将从缓存中返回该图片。否则,它会请求底层缓存返回其版本(依此类推)。

因此,为了处理我们的 3 个缓存级别,我们必须声明三个变量。

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

选择正确的封面仅取决于缩放。

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

为了给用户反馈,我们将添加一个计时器来管理一个工具提示,该工具提示显示当前加载的图片数量。

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

再次注意到 jQuery 的使用简化了动画。

现在我们将讨论卡片的显示。

卡片显示

为了绘制我们的卡片,我们需要实际使用其 2D 上下文填充 canvas(只有在浏览器支持 HTML5 canvas 时才存在)。

1.  var mainCanvas = document.getElementById("mainCanvas");

2.  var drawingContext = mainCanvas.getContext('2d');

绘图将由 processListOfCards 函数执行(每秒调用 60 次)。

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

此函数围绕许多关键点构建:

  • 如果卡片列表尚未加载,我们将显示一个工具提示,指示下载正在进行中:
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.}
  • 随后,我们定义显示窗口的位置(按卡片和坐标),然后继续清理 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.  }
  • 然后我们遍历卡片列表,并调用 canvas 上下文的 drawImage 函数。当前图像由活动缓存(取决于缩放)提供。
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.});
  • 我们还需要使用 RoundedRectangle 函数绘制滚动条,该函数使用二次曲线。
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.}
  • 最后,我们需要计算每秒帧数。
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.}

卡片绘制在很大程度上依赖于浏览器加速 canvas 渲染的能力。作为参考,这是我的机器在最小缩放级别(0.05)上的性能:

Library-Images-Canvas/image014.png

浏览器 帧率 (FPS)
Internet Explorer 9M 30
Firefox 5 30
Chrome 12 17
iPad(缩放级别为 0.8) 7
Windows Phone Mango(缩放级别为 0.8) 20 (!!)

该网站甚至可以在支持 HTML5 的手机平板电脑上运行。

这里我们可以看到 HTML5 浏览器强大的功能,它每秒可以处理超过 30 张全屏卡片!  这可以通过 硬件加速实现。

鼠标管理

为了浏览我们的卡片收藏,我们必须管理鼠标(包括其滚轮)。

对于滚动,我们将仅处理 onmouvemoveonmouseuponmousedown 事件。

Onmouseuponmousedown 事件将用于检测鼠标是否被单击。

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

onmousemove 事件连接到 canvas,用于移动视图。

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

此函数(onMouseMove)计算当前位置,并提供前一个值,以便移动显示窗口的偏移量。

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

请注意,jQuery 也提供了管理鼠标事件的工具。

对于滚轮的管理,我们将不得不适应不同的浏览器,它们在此问题上的行为方式不同。

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

我们可以看到每个人都有自己的做法。

注册此事件的函数是

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

我们将使用此函数通过滚轮更改缩放。

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

最后,我们将在移动鼠标(和缩放)时添加一点惯性,以提供某种平滑感。

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

这种小程序实现的成本不高,但大大提升了用户体验的质量。

状态存储

为了提供更好的用户体验,我们还将保存显示窗口的位置和缩放。为此,我们将使用 localStorage 的服务(它长期保存键/值对(数据在浏览器关闭后仍然保留)并且仅可由当前窗口对象访问)。

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

动画

为了增加应用程序的动态性,我们将允许用户双击卡片以放大并聚焦于它。

我们的系统应该动画化三个值:两个偏移量(X、Y)和缩放。为此,我们将使用一个函数,该函数将负责以给定的持续时间将变量从源值动画化到目标值。

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

此函数的使用方式是:

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

AnimationHelper 函数的优点是它能够动画化任意数量的参数(而且仅使用 setTimer 函数!)。

多设备处理

最后,我们将确保我们的页面也能在平板电脑甚至手机上显示。

为此,我们将使用 CSS 3 的一项功能:媒体查询。使用这项技术,我们可以根据某些查询(例如特定的显示尺寸)应用样式表。

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

在这里,我们看到如果屏幕宽度小于480 像素,将添加以下样式表:

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

此样式表将减小标题的大小,以使网站即使在浏览器宽度小于 480 像素时(例如,在 Windows Phone 上)也能正常查看。

Library-Images-Canvas/image015.png

结论

HTML5 / CSS 3 / JavaScriptVisual Studio 2010 允许开发便携且高效的解决方案(当然,在支持 HTML5 的浏览器范围内),并具有一些出色的功能,例如硬件加速渲染。

使用 jQuery 等框架也可以简化此类开发。

此外,我特别喜欢 JavaScript,因为它被证明是一种非常强大的动态语言。当然,C# 或 VB.NET 开发人员需要改变他们的习惯,但对于网页开发来说,这是值得的。

总之,我认为最好的方法就是尝试!

深入了解

© . All rights reserved.