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





5.00/5 (13投票s)
作为用户界面爱好者,我不能错过使用 HTML5 Canvas 进行开发的这次机会。它为在 Web 上可视化图像和数据开辟了全新的方式。在本教程中,我将指导您如何为您的网站创建一个。
作为用户界面爱好者,我不能错过使用 HTML5 Canvas 进行开发的这次机会。 它为在 Web 上可视化图像和数据开辟了全新的方式。 在本教程中,我将指导您如何为您的网站创建一个。
应用程序概述
我们将创建一个应用程序,允许我们显示《万智牌》©(鸣谢 www.wizards.com/Magic)的卡牌收藏。用户将能够使用鼠标滚动和缩放(例如,类似必应地图)。
注意:图像和数据可视化对硬件要求很高。 了解 HTML5 硬件加速及其重要性。
您可以在这里看到最终结果:http://bolaslenses.catuhe.com
项目源文件可在此处下载:http://www.catuhe.com/msdn/bolaslenses.zip
卡片存储在Windows Azure Storage 中,并使用 Azure 内容分发网络(CDN:一项将数据部署到最终用户附近的服务)以实现最佳性能。ASP.NET 服务用于返回卡片列表(使用JSON 格式)。
工具
为了编写我们的应用程序,我们将使用 Visual Studio 2010 SP1 和Web 标准更新。此扩展为 HTML5 页面添加了 IntelliSense 支持(这非常重要)。
因此,我们的解决方案将包含一个 HTML5 页面,旁边是 .js 文件(这些文件将包含 JavaScript 脚本)。关于调试,可以在 Visual Studio 下直接在 .js 文件中设置断点。尝试使用 Internet Explorer 9 中的F12 开发人员工具。
因此,我们拥有了一个支持 IntelliSense 和调试的现代开发环境。因此,我们已准备好开始,首先,我们将编写 HTML5 页面。
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. }
因此,此样式表负责设置以下显示
样式表是强大的工具,允许无限数量的显示。但是,它们有时很难设置(例如,如果一个标签受到类、ID 及其容器的影响)。为了简化此设置,Internet Explorer 9 的开发人员栏特别有用,因为我们可以使用它来查看应用于标签的样式层次结构。
例如,让我们看一下 waitText
工具提示,使用开发人员栏。为此,您必须在 Internet Explorer 9 中按 F12,然后使用选择器选择工具提示。
选择完成后,我们可以看到样式层次结构。
因此,我们可以看到我们的 div
从 body
标签和样式表中的 .tooltip
条目接收了样式。
使用此工具,可以查看每个样式的效果(可以禁用)。还可以即时添加新样式。
此窗口的另一个重要点是更改 Internet Explorer 9 渲染模式的能力。事实上,我们可以测试例如 Internet Explorer 8 将如何处理同一页面。为此,请转到 [浏览器模式] 菜单,然后选择 Internet Explorer 8 的引擎。此更改将特别影响我们的工具提示,因为它使用了 CSS 3 的 border-radius(圆角)和 box-shadow 功能。
![]() |
![]() |
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 };
整个系统由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)上的性能:
浏览器 | 帧率 (FPS) |
Internet Explorer 9M | 30 |
Firefox 5 | 30 |
Chrome 12 | 17 |
iPad(缩放级别为 0.8) | 7 |
Windows Phone Mango(缩放级别为 0.8) | 20 (!!) |
该网站甚至可以在支持 HTML5 的手机和平板电脑上运行。
这里我们可以看到 HTML5 浏览器强大的功能,它每秒可以处理超过 30 张全屏卡片! 这可以通过 硬件加速实现。
鼠标管理
为了浏览我们的卡片收藏,我们必须管理鼠标(包括其滚轮)。
对于滚动,我们将仅处理 onmouvemove
、onmouseup
和 onmousedown
事件。
Onmouseup
和 onmousedown
事件将用于检测鼠标是否被单击。
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 上)也能正常查看。
结论
HTML5 / CSS 3 / JavaScript 和 Visual Studio 2010 允许开发便携且高效的解决方案(当然,在支持 HTML5 的浏览器范围内),并具有一些出色的功能,例如硬件加速渲染。
使用 jQuery 等框架也可以简化此类开发。
此外,我特别喜欢 JavaScript,因为它被证明是一种非常强大的动态语言。当然,C# 或 VB.NET 开发人员需要改变他们的习惯,但对于网页开发来说,这是值得的。
总之,我认为最好的方法就是尝试!
深入了解
- 了解硬件加速
- Internet Explorer 9 和 10 中的F12 开发人员工具
- Internet Explorer 10 平台预览版
- Internet Explorer 9 开发人员指南
- W3C HTML5 网站