在网络时代的早期,Web 只是静态文本和链接的集合,人们越来越关注为其他类型的内容提供支持。 1993 年,Mosaic 浏览器(后来发展为 Netscape Navigator)的创建者 Marc Andreessen 提出将 IMG 标记作为在页面上的文本中嵌入图像的标准。 此后不久,IMG 标记成为向网页中添加图形资源的事实上的标准—至今仍在使用这一标准。 甚至可以说,由于 Web 应用已经从 Web 文档发展到 Web 应用程序,因此 IMG 标记比以往更加重要。
一般而言,媒体肯定比以往更加重要,尽管 Web 上的媒体需求在过去 18 年中不断演变,但图像一直是静态的。 Web 作者越来越希望能够在其网站和应用程序中使用动态媒体(例如音频、视频和交互式动画),直到最近,主要的解决方案仍然是 Flash 或 Silverlight 之类的插件。
现在有了 HTML5,浏览器中的媒体元素大受青睐。 您可能听说过新的 Audio 和 Video 标记,二者均允许这些类型的内容充当浏览器中的第一类对象,不需要任何插件。 下个月的文章将深入介绍这两个元素及其 API。您可能还听说过 canvas 元素,它是一个绘图表面,包含一组丰富的 JavaScript API,这些 API 使您能够动态创建和操作图像及动画。 IMG 对静态图形内容起到了哪些作用,canvas 就可能对可编写脚本的动态内容起到哪些作用。
尽管 canvas 元素令人兴奋,但它也面临一些认知问题。 由于画布功能强大,它通常通过复杂动画或游戏来展示,尽管这些动画或游戏确实可以传达能够实现的功能,但它们也可能让您产生误解,认为画布使用起来非常复杂和困难,只应该尝试将其用于复杂情况(如动画或游戏)。
在本月的文章中,我将撇开画布华而不实的功能和复杂性,介绍它的一些简单的基本用法,目标是将画布定位为在 Web 应用程序中进行数据可视化的一个功能强大的选项。 明确这一点后,我将重点讨论您如何开始使用画布,以及如何绘制简单的线条、形状和文本。 然后,我将讨论您如何在形状中使用渐变,以及如何向画布中添加外部图像。 最后,按照我在本系列中的一贯做法,我会简要探讨一下对较早版本浏览器的“填充代码”画布支持。
HTML5 Canvas 简介
根据 W3C HTML5 规范 (w3.org/TR/html5/the-canvas-element.html),canvas 元素“为脚本提供取决于分辨率的位图画布,该画布可用于动态呈现图形、游戏图形或其他可视图像。”Canvas 实际是在两个 W3C 规范中定义的。 第一个规范是 HTML5 核心规范的一部分,其中详细定义了该元素本身。 此规范介绍如何使用 canvas 元素、如何获取其绘图上下文、用于导出画布内容的 API 以及浏览器供应商的安全注意事项。 第二个规范是 HTML Canvas 2D Context (w3.org/TR/2dcontext),我稍后再介绍该规范。
开始使用画布非常简单,只需在 HTML5 标记中添加 <canvas> 元素即可,如下所示:
- <!DOCTYPE html>
- <html lang=“en”>
- <head>
- <meta charset=“utf-8” />
- <title>My Canvas Demo </title>
- <link rel=“stylesheet” href=“style.css” />
- </head>
- <body>
- <canvas id=“chart” width=“600” height=“450”></canvas>
- </body>
- </html>
尽管现在我已经在 DOM 中包含 canvas 元素,但在页面上放置此标记不会产生任何效果,因为在您添加内容之前,canvas 元素中没有任何内容。 这就是绘图上下文应运而生的原因。 为了说明我的空白画布所在的位置,我可以使用 CSS 设置其样式,因此我将在空白元素周围添加一条蓝色虚线。
- canvas {
- border-width: 5px;
- border-style: dashed;
- border-color: rgba(20, 126, 239, 0.50)
- }
在 Internet Explorer 9+、Chrome、Firefox、Opera 或 Safari 中打开我的页面时的结果如图 1 所示。
图 1 已设置样式的空白 Canvas 元素
使用画布时,您将在 JavaScript 中执行大多数工作,可通过 JavaScript 利用画布绘图上下文公开的 API 来操作图面的每个像素。 要获取画布绘图上下文,您需要从 DOM 获得您的 canvas 元素,然后调用该元素的 getContext 方法。
- var _canvas = document.getElementById(‘chart’);
- var _ctx = _canvas.getContext(“2d”);
GetContext 返回一个对象,其中包含可用于在相关画布上绘图的 API。 该方法的第一个参数(在本例中为“2d”)指定我们要用于画布的绘图 API。 “2d”指的是我之前提到的 HTML Canvas 2D Context。 您可能已经猜到,2D 表示这是一个二维绘图上下文。 到撰写本文时为止,2D Context 是唯一受到广泛支持的绘图上下文,我们将在本文中使用该上下文。 围绕 3D 绘图上下文的工作和试验正在进行当中,因此将来画布应该能够为我们的应用程序提供更多功能。
绘制线条、形状和文本
现在页面上已经有了 canvas 元素,并且我们已经在 JavaScript 中获取了其绘图上下文,我们可以开始添加内容了。 因为我想重点介绍数据可视化,所以我将使用画布绘制一个条形图来表示一个虚构的体育用品商店当月的销售数据。 本练习将需要为轴绘制轴线;为条绘制形状和填充;以及为每个轴和条上的标签绘制文本。
让我们从 x 和 y 轴的轴线开始。 在画布上下文中绘制直线(或路径)分两个步骤进行。 首先,使用一系列 lineTo(x, y) 和 moveTo(x, y) 调用在图面上“描摹”直线。 每种方法都会获取画布对象(从左上角开始)上的 x 坐标和 y 坐标(而非屏幕本身上的坐标)以便在执行操作时使用。 moveTo 方法将移至所指定的坐标,lineTo 将在当前坐标与您指定的坐标之间描摹一条直线。 例如,以下代码将在图面上描摹我们的 y 轴:
- // Draw y axis.
- _ctx.moveTo(110, 5);
- _ctx.lineTo(110, 375);
如果您向脚本中添加此代码并在浏览器中运行它,您会注意到什么也不会发生。 因为这第一步只是一个描摹步骤,并未在屏幕上绘制任何内容。 描摹仅指示浏览器记录将在以后某个时刻刷新到屏幕上的路径操作。 当我准备好在屏幕上绘制路径时,我可以选择设置我的上下文的 strokeStyle 属性,然后调用 stroke 方法,该方法将填充不可见线条。 结果如图 2 所示。
- // Define Style and stroke lines.
- _ctx.strokeStyle = “#000”;
- _ctx.stroke();
图 2 画布上的一条直线
因为定义线条(lineTo、moveTo)和绘制线条 (stroke) 是相对独立的,所以您实际可以批量处理一系列 lineTo 和 moveTo 操作,然后将它们同时输出到屏幕上。 我将通过此方法绘制 x 轴和 y 轴并完成在每个轴的一端绘制箭头的操作。 用于绘制轴的完整函数如图 3 所示,结果如图 4 所示。
图 3 drawAxes 函数
- function drawAxes(baseX, baseY, chartWidth) {
- var leftY, rightX;
- leftY = 5;
- rightX = baseX + chartWidth;
- // Draw y axis.
- _ctx.moveTo(baseX, leftY);
- _ctx.lineTo(baseX, baseY);
- // Draw arrow for y axis.
- _ctx.moveTo(baseX, leftY);
- _ctx.lineTo(baseX + 5, leftY + 5);
- _ctx.moveTo(baseX, leftY);
- _ctx.lineTo(baseX – 5, leftY + 5);
- // Draw x axis.
- _ctx.moveTo(baseX, baseY);
- _ctx.lineTo(rightX, baseY);
- // Draw arrow for x axis.
- _ctx.moveTo(rightX, baseY);
- _ctx.lineTo(rightX – 5, baseY + 5);
- _ctx.moveTo(rightX, baseY);
- _ctx.lineTo(rightX – 5, baseY – 5);
- // Define style and stroke lines.
- _ctx.strokeStyle = “#000”;
- _ctx.stroke();
- }
图 4 完成的 X 轴和 Y 轴
绘制完两个轴之后,我们可能要为其添加标签使其更加有用。 2D 画布上下文指定用于向 canvas 元素添加文本的 API,因此您不需要摆弄杂乱的操作,例如使文本在 canvas 元素上浮动。 尽管如此,画布文本并不提供方框模型,也不接受为页面范围的文本定义的 CSS 样式,等等。 API 提供与 CSS 字体规则工作方式相同的字体属性 (Attribute)—以及 textAlign 和 textBaseline 属性 (Property),以便您能够对相对于所提供的坐标的位置进行某种控制—但除此之外,在画布上绘制文本实际上就是在画布上为您所提供的文本选取一个确切的点。
X 轴表示我们虚构的体育用品商店中的商品,所以我们应相应地为该轴添加标签:
- var height, widthOffset;
- height = _ctx.canvas.height;
- widthOffset = _ctx.canvas.width/2;
- _ctx.font = “bold 18px sans-serif”;
- _ctx.fillText(“Product”, widthOffset, height – 20);
在此代码段中,我将设置可选字体属性并提供一个要绘制在图面上的字符串,以及要用作字符串起始位置的 x 坐标和 y 坐标。 在此例中,我将在画布中间、底部上方 20 个像素的位置绘制“Product”一词,从而为我的条形图上的每种商品的标签留出空间。 我将对 y 轴标签(包含每种商品的销售数据)执行类似操作。 结果如图 5所示。
图 5 包含文本的画布
现在我们已经有了图表框架,可以添加条了。 让我们为条形图创建一些虚拟销售数据,我将其定义为对象文字的 JavaScript 数组。
- var salesData = [{
- category: “Basketballs”,
- sales: 150
- }, {
- category: “Baseballs”,
- sales: 125
- }, {
- category: “Footballs”,
- sales: 300
- }];
有了这些数据后,我们可以使用 fillRect 和 fillStyle 在图表上绘制条。
fillRect(x, y, width, height) 将使用您指定的宽度和高度,在画布上的 x 和 y 坐标位置绘制一个矩形。 务必注意,除非您指定负宽度和高度值(在这种情况下,填充将按相反方向向外伸出),否则 fillRect 绘制的形状将从左上角开始向外伸出。 对于绘制图表之类的绘图任务,这意味着我们将自上而下(而非自下而上)绘制条。
要绘制条,我们可以遍历销售数据数组,并使用相应的坐标调用 fillRect:
- var i, length, category, sales;
- var barWidth = 80;
- var xPos = baseX + 30;
- var baseY = 375;
- for (i = 0, length = salesData.length; i < length; i++) {
- category = salesData[i].category;
- sales = salesData[i].sales;
- _ctx.fillRect(xPos, baseY – sales-1, barWidth, sales);
- xPos += 125;
- }
在此代码中,每个条的宽度是标准的,而高度是从数组中每种商品的销售属性中获取的。 图 6 显示了此代码的结果。
图 6 用作条形图数据的矩形
现在,我们有了一个图表,它在技术上是准确的,但这些纯黑色的条还有待改进。 我们可以通过填充某种颜色使其更加清晰明了,然后添加渐变效果。
使用颜色和渐变
调用绘图上下文的 fillRect 方法时,上下文将使用当前 fillStyle 属性在绘制矩形时设置其样式。 默认样式为纯黑色,因此我们的条看上去才如图 6 所示。 fillStyle 接受指定的十六进制和 RGB 颜色,因此我们可以添加一些功能以便在绘制每个条之前设置其样式:
- // Colors can be named hex or RGB.
- colors = [“orange”, “#0092bf”, “rgba(240, 101, 41, 0.90)”];
- …
- _ctx.fillStyle = colors[i % length];
- _ctx.fillRect(xPos, baseY – sales-1, barWidth, sales);
首先,我们需要创建一个颜色数组。 然后,在遍历每种商品时,我们将使用其中一种颜色作为该元素的填充样式。 结果如图 7 所示。
图 7 使用 fillStyle 设置形状的样式
效果有所改善,但 fillStyle 非常灵活,允许您使用线性和径向渐变而非只使用纯色。 2D 绘图上下文指定两个渐变函数:createLinerGradient 和 createRadialGradient,二者均可通过平滑颜色过渡来改善您的形状的样式。
对于此示例,我将定义一个 createGradient 函数,它将接受渐变的 x 和 y 坐标、宽度和要使用的原色:
- function createGradient(x, y, width, color) {
- var gradient;
- gradient = _ctx.createLinearGradient(x, y, x+width, y);
- gradient.addColorStop(0, color);
- gradient.addColorStop(1, “#efe3e3”);
- return gradient;
- }
使用我的起点和终点坐标调用 createLinearGradient 后,我将向绘图上下文返回的渐变对象中添加两个颜色断点。 addColorStop 方法将沿渐变添加颜色过渡;该方法可以调用任意多次,但第一个参数值需介于 0 和 1 之间。 在设置我的渐变后,我将从函数返回它。
渐变对象随后可在我的上下文中设置为 fillStyle 属性,来代替我在上一示例中指定的十六进制和 RGB 字符串。我将使用这些相同的颜色作为起点,然后使其逐渐变为浅灰色。
- colors = [“orange”, “#0092bf”, “rgba(240, 101, 41, 0.90)”];
- _ctx.fillStyle = createGradient(xPos, baseY – sales-1, barWidth, colors[i % length]);
- _ctx.fillRect(xPos, baseY – sales-1, barWidth, sales);
渐变方法的结果如图 8 所示。
图 8 在画布中使用渐变
处理图像
此时,我们得到了一个非常漂亮的图表,我们已经能够使用数十行 JavaScript 在浏览器中呈现该图表。 我可以就此打住,但我仍想介绍一个与处理图像相关的基本画布 API。 借助画布,您不仅可以将静态图像替换为基于脚本的交互式内容,而且可以使用静态图像增强您的画布的可视化效果。
对于本演示,我想将图像用作条形图上的条。 不只是图像,还包括项目本身的图片。 抱着这一目标,我的网站中有一个文件夹包含每种商品的 JPG 图像—在本例中为 basketballs.jpg、baseballs.jpg 和 footballs.jpg。 我只需要正确放置每个图像并设置其大小。
2D 绘图上下文定义一个带有三个重载的 drawImage 方法,接受三个、五个或九个参数。 第一个参数始终是要绘制的 DOM 元素图像。 DrawImage 的最简单版本还接受画布上的 x 和 y 坐标,并在该位置按原样绘制图像。 您也可以将宽度和高度值作为最后两个参数提供,这会将图像缩放为该大小,然后再在图面上绘制它。最后,drawImage 的最复杂用法允许您将图像裁剪为定义的矩形,将其缩放为一组给定的维度,最终再在画布上指定的坐标位置绘制它。
由于我的源图像是在我的网站上的其他位置使用的大比例图像,因此我将采用后一种方法。 在此例中,我不在遍历 salesData 数组时为每个项目调用 fillRect,而是创建一个 Image DOM 元素,将其来源设置为我的商品图像之一,并将该图像裁剪后的版本呈现在我的图表上,如图 9 所示。
图 9 在画布上绘制图像
- // Set outside of my loop.
- xPos = 110 + 30;
- // Create an image DOM element.
- img = new Image();
- img.onload = (function(height, base, currentImage, currentCategory) {
- return function() {
- var yPos, barWidth, xPos;
- barWidth = 80;
- yPos = base – height – 1;
- _ctx.drawImage(currentImage, 30, 30, barWidth, height, xPos, yPos,
- barWidth, height);
- xPos += 125;
- }
- })(salesData[i].sales, baseY, img, salesData[i].category);
- img.src = “images/” + salesData[i].category + “.jpg”;
因为我将动态创建这些图像,而不是在设计时手动将其添加到我的标记,所以我不应认为我可以设置图像来源,然后立即将该图像绘制到我的画布上。 为确保我仅在每个图像完全加载后才绘制该图像,我会将我的绘图逻辑添加到图像的 onload 事件中,然后将该代码打包到自调用函数中,该函数会创建一个闭包,其中包含指向正确的商品类别的变量、销售变量和定位变量。 结果如图 10 所示。
图 10 在画布上使用图像
使用画布填充代码
您可能已经知道,Internet Explorer 9 之前的版本以及其他浏览器的较早版本不支持 canvas 元素。 通过在 Internet Explorer 中打开演示项目并按 F12 打开开发人员工具,您可以亲自验证这一点。 通过 F12 工具,您可以将“浏览器模式”更改为 Internet Explorer 8 或 Internet Explorer 7 并刷新页面。 您可能会看到一个包含以下消息的 JavaScript 异常:“对象不支持 getContext 属性或方法”。2D 绘图上下文不可用,canvas 元素本身也不可用。 还务必要知道,即使在 Internet Explorer 9 中,除非您指定 DOCTYPE,否则画布也不可用。 正如我在本系列第一篇文章 (msdn.microsoft.com/magazine/hh335062) 中提到的,最好在所有 HTML 页面的顶部使用 <!DOCTYPE html> 以确保可使用浏览器中的最新功能。
对于其浏览器不支持画布的用户来说,可以采取的最简单的措施是使用后备元素,例如图像或文本。 例如,要向用户显示后备图像,您可以使用类似如下的标记:
- <canvas id=”chart”>
- <img id=”chartIMG” src=”images/fallback.png”/>
- </canvas>
您放在 <canvas> 标记内的任何内容都只在用户的浏览器不支持画布时才会呈现。 这意味着,您可以在画布内放置图像或文本,作为用户的简单的零检查后备图像。
如果您要进一步探索后备支持,好消息是,存在各种针对画布的填充代码解决方案,因此,只要您仔细审查潜在解决方案并了解给定填充代码的限制,就可以放心地将其与较早浏览器结合使用。 如我在本系列的其他文章中所述,您查找任何 HTML5 技术的填充代码的切入点应该是 GitHub 上 Modernizr wiki 中的“HTML5 跨浏览器填充代码”页 (bit.ly/nZW85d)。 到撰写本文时为止,市场上已经有多个画布填充代码,包括两个回退到 Flash 和 Silverlight 的填充代码。
在本文的可下载演示项目中,我使用 explorercanvas (code.google.com/p/explorercanvas) 以及 canvas-text (code.google.com/p/canvas-text),前者使用 Internet Explorer 支持的矢量标记语言 (VML) 创建近似的画布功能,后者为在较早浏览器中呈现文本提供额外支持。
如前面的文章所示,您可以使用 Modernizr 通过调用 Modernizr.canvas 对画布(和 canvastext)支持进行功能检测,然后使用 Modernizr.load 在需要时异步加载 explorercanvas。 有关详细信息,请参阅 modernizr.com。
如果您不想使用 Modenrizr,则可以使用另一种方法按条件为较早版本的 IE 添加 explorercanvas: 条件注释:
- <!–[if lt IE 9]>
- <script src=”js/excanvas.js”></script>
- <script src=”js/canvas.text.js”></script>
- <![endif]–>
当 Internet Explorer 8 或较早版本遇到按此方式设置格式的注释时,它们会将该代码块作为 if 语句执行,并包括 explorercanvas 和 canvas-text 脚本文件。 其他浏览器(包括 Internet Explorer 10)会将整个代码块视为注释并完全忽略它。
在评估您的应用程序的潜在填充代码时,请务必确定给定填充代码支持多少个 2D 绘图上下文。 只有极少 2D 绘图上下文完全支持每种用法,但几乎所有上下文都可以处理我们在本文中讨论的基本情形。
虽然在这里我无法面面俱到,但您还可以对画布执行更多操作,从响应 Click(和其他)事件和更改画布数据,到为绘图图面设置动画、逐像素呈现和操作图像、保存状态以及将整个图面导出为其自己的图像,均包括在内。 事实上,市场上已经有介绍画布的完整书籍。 您不必是游戏开发人员即可体验画布的强大功能,我希望通过在本文中介绍其基本功能,能够向您证实这一点。 建议您亲自阅读这些规范,全身心投入到这一振奋人心的新图形技术的研究当中。