使用 HTML 标记来补充 canvas,第 2 部分: 动画和文本渲染

利用分层克服困难

HTML canvas 在很多方面表现卓越,其中包括通过低开销和直接的像素处理带来的卓越性能。然而,canvas 存在一些缺陷,但 HTML 在这些方面表现得非常好:文本渲染、SEO、可访问性和独立于设备的标记。本系列第 1 部分 比较和对比了传统 HTML 模型和 canvas API 的优势,并探讨了 HTML/Canvas 混合应用。在作为本系列收尾之作的第 2 部分中,我们将了解如何实现一个涉及到文本渲染的 canvas 呈现的样例应用程序,还将了解如何创建一个具有丰富的 HTML 用户界面的基于 canvas 的游戏,将两种方法的优势结合起来。

Kevin Moot, 软件开发人员, The Nerdery

Kevin Moot 照片Kevin Moot 从很小的时候就对计算机图形有浓厚的兴趣,那时他在自己的 Apple IIe(具有六种颜色的巨大矩阵以及令人震撼的 280x192 分辨率)上创建游戏。他还将 HTML5 的 Canvas 技术应用于几个前沿网站,HTML/CSS、JavaScript 和 .NET 也是他的专长。Kevin 目前是 The Nerdery 的一名交互软件开发人员。



2013 年 2 月 06 日

简介

常用缩略词

  • CSS:级联样式表
  • DOM:文档对象模型
  • HTML:超文本标记语言
  • UI:用户界面

在这个由两部分组成的 系列文章第 1 部分 中,我们讨论了如何让 canvas 与 HTML 元素强强联手,共同构建丰富的 Internet 应用程序。

在本文中,我们将回顾选择 canvas 或以 HTML 为中心的架构的标准,了解动画考虑事项和克服文本渲染限制的方法,也就是说,通过将 HTML 和 canvas 元素分层,为视频游戏奠定基础,从而利用每一种方法的优势。图 1 显示了本文中太空射击游戏示例的基础。

图 1. 将 HTML 和 canvas 元素相结合的样例应用程序
将 HTML 和 canvas 元素相结合的样例应用程序

您可以 下载 本文中使用的示例的源代码。


架构

在使用大量图形组件、交互式体验和可视化效果构建应用程序时,重要的是要了解可用于该任务的工具。本节将探讨如何使用 HTML 实现 UI 组件,以及如何使用 canvas 实现动画组件。

用户界面

尽管 canvas 可能具有令人印象深刻的图形性能,但它并不总是实现丰富 UI 的最佳选择;HTML 元素可能更适合。很多丰富的 Internet 应用程序包含各种部件,每个部件都有不同的用途和要求。将 canvas 和 HTML 元素强强联合的混合方法可以最有效地实现您的目标。

样例应用程序利用了 图 2 中的分层技术。canvas 主要负责实时图形和动画,而几个 HTML 元素将会覆盖在上面,用于构成各个 UI 组件。

图 2. 将 HTML 元素放在 canvas 上面一层
将 HTML 元素放在 canvas 上面一层

在决定最佳方法时,需要考虑应用程序各部件的需求。一般说来,要求具有较高的用户交互性、但又不需要实时更新的 UI 的组件通常最适合用于 HTML 层。这些元素可能包含文本、超链接和表单元素。

例如,尽管样例应用程序可能主要是一个 canvas ,它拥有一个仅仅使用 HTML 标记的聊天窗口组件。这如图 3 所示。

有了简单的 HTML 标记和 CSS 规则,您可以轻松创建交互式 UI 组件,比如文本框、滚动条和按钮。既然浏览器提供预置的组件,为何还要花费数小时试图模仿 canvas 中 UI 组件的外观和行为?

图 3. 用 HTML 实现的聊天窗口
用 HTML 实现的聊天窗口

在聊天窗口中,不涉及任何动画,对内容的更新相对不频繁(每当有新聊天项目时才更新)。因此,这是一个 HTML 实现的好的候选对象。

相反地,canvas 为以下内容提供一个更好的解决方案:

  • 必须频繁更新(例如,每毫秒更新一次)
  • 需要一个恒定的动画周期
  • 只需要很小的用户交互性

动画

动画内容在网站中变得越来越普遍。无论您只是为了让一个网站有生气而使用简单动画(比如导航转换),还是想要提供更高级的基于浏览器的游戏,都有多种选择任您支配。通常,可以使用 HTML/CSS 模型完成相对简单的动画,使用 canvas 就有些过了。

像 jQuery 等许多库都提供了便捷的工具来提供一致的跨浏览器输出。将这些库中的工具与一些 CSS 知识结合起来,您就可以大幅减少完成动画的工作,即使这些动画实际上是相当复杂的。

在示例中,图 4 中的太空飞船根据玩家的控制进行移动和旋转。这一行为只能使用 HTML/CSS 动画实现。不要求用户对 canvas 有任何了解。

图 4. 动态的太空飞船元素
黑色背景上动态的太空飞船元素

当需要扩展时,HTML/CSS 动画模型开始分解。同时动态化大量元素会给浏览器带来很重的负担,这会降低应用程序的整体性能。

每次重新定位一个 DOM 元素时(对于平滑的动画来说,这肯定每秒发生多次),浏览器的布局引擎需要大量开销才能重新计算和绘制 DOM 层次结构中的元素。增至数十、数百或上千元素之后,即使在现代计算机上也会带来相当大的性能消耗。


canvas 上的文本

渲染文本到屏幕上对于任何网站来说都是一项非常基本的任务,我们可能都不太注意其后台运作。当您希望屏幕上包含一些文本时,只需在两个 HTML 元素间标记(可能还有一点 CSS)之间输入文本即可,其余工作都会由浏览器接管。但是对于 canvas 来说,情况却并非如此。

当您希望使用 canvas 渲染文本时,有几个基本的工具可用。它们几乎支持工作所需的一切基本功能,但在开发 canvas 应用程序时,却没有提供太多的易用性。

基本原理

canvas context 对象提供了您在渲染文本时可以设置的各种属性。它还提供了一个功能来执行实际渲染。这些属性包括:

  • context.font

    设置 context.font 的值能够让您控制要渲染文本的字体类型、大小、粗细和样式。分配的值是各种选项拼凑在一块、由空格分隔的一个字符串。

    这里的输入格式有点儿难办。例如,每次更新值时,都必须在该字符串中提供字体类型。清单 1 显示了小字体的设置。

    清单 1. 设置小字体
    context.font = 'italic 8px Arial';
    context.fillText('Variety is the spice of life!', 0, 50);

    清单 2 显示了大字体的设置。

    清单 2. 设置大字体
    context.font = 'italic 20px Arial';
    context.fillText('Variety is the spice of life!', 0, 50);

    用户不可能仅设置字体一次,稍后,用户可以根据希望渲染到屏幕上的文本来调整其他选项。修复这一问题的方法会在稍后加以讨论。

  • context.fillStyle

    context.fillStyle 用于各种 canvas 操作;对文本设置该值是为了控制要渲染到屏幕上的字体颜色。指定值的输入格式遵从 CSS 输入格式。以下全部是有效的输入示例:

    • 基本颜色:'red'、'blue'、'green',等等
    • 16 进制值:'#rrggbb'
    • 'rgb(r, g, b)'
    • 'rgba(r, g, b, a)'

    例如,要设置 fillStyle,请使用 context.fillStyle = 'red';

  • context.fillText()

    调用这一函数可将文本渲染到 canvas 上。它接受以下参数:

    • (string) text:要绘制到 canvas 上的文本。
    • (float) x:绘制文本的 x 的大小。
    • (float) y:绘制文本的 y 的大小。
    • [optional] (float) maxWidth:试图控制文本所需的最大宽度。如果可能的话,请将使用一个水平浓缩程度更高的字体,或者更小的字体。

其他工具

您可以使用的另一个重要工具是上下文对象的一个函数,名为 measureText(),它接受一个字符串参数。结果是一个包含所提供字符串的度量尺寸的对象,如 清单 3 所示。

清单 3. 确定文本字符串的宽度
context.font = '30px Arial';
var dim = context.measureText(
    'Hello, world!'
);
 
alert(
    'width: ' + dim.width + '\n' +
    'height: ' + dim.height
);

清单 3 显示了一个警报,其输出类似于 清单 4

清单 4. 警报
    width: 164
    height: undefined

注意 height 后面的 undefined。令人费解的是,所有浏览器都总是会返回一个未定义的结果,因此使用 measureText() 函数实际上不可能确定具体的文本高度。有几项技术可用于确定一个颇具代表性的值。对于许多字体,某些字母相当方正,比如字母 M。您可以度量这些字符中某个字符的宽度,并将该值作为字体高度的近似测量。

另一种方法就是将提供的字体大小作为实际高度的基础。如果上述示例中提供了一个 30px 的值,那么您可以添加几个垂直填充像素,并将产生的值用作近似高度。

构建块

要提高开发的效率,您可以使用之前提到过的工具以一种友好的方式执行,创建一个简单的 wrapper 类。该类会自动的设置和置换各个上下文属性,最终将想要的文本渲染到 canvas 上。您可以使用 context.font 属性自动解决该问题。清单 5 中的示例在本系列的 第 1 部分 使用过。

清单 5. 在 canvas 中使用动态样式渲染文本
context.font = '18px Arial';
context.fillStyle = 'green';
context.fillText('Variety', 0, 50);
context.translate(60, 0);  //move 60 pixels to the right (a)

context.font = '12px Arial';
context.fillStyle = 'blue';
context.fillText('is the', 0, 50);
context.translate(35, 0); //move 35 pixels to the right (b)

context.font = 'italic bold 12px Arial';
context.fillStyle = 'red';
context.fillText('spice of life!', 0, 50); // (c)

图 5 显示清单 5 中在 canvas 中渲染文本的三个步骤。

图 5. 在 canvas 中使用动态样式渲染文本
在 canvas 中使用动态样式渲染文本

要使用原生 canvas API,则需要编写大量代码。如果您可以简化上述示例所需的代码,那么可以提高将文本渲染到 canvas 上的效率。例如,您可以使用清单 6 中的代码,而不是编写多行的原生代码。

清单 6. 增强 canvas 文本渲染的工作流的概念
var myText = new CanvasText();
myText
    .family('Arial')
    .size('18px')
    .weight('bold')
    .color('green')
    .append('Variety')
    
    .size('12px')
    .weight('normal')
    .color('blue')
    .append('is the')
    
    .style('italic')
    .color('red')
    .append('spice of life!')
    
    .render();

图 6 显示了 清单 6 中的代码。该示例使用了链接 (chaining),链接是 jQuery 和 jQuery 插件中常用的一个干净而又简单的语法。

图 6. 增强 canvas 文本渲染
增强 canvas 文本渲染

流程通过这种方式得到简化。尽管代码行数几乎一样,但每行的代码复杂性降低了。而且您不再需要手动定位样式的文本块。如果您稍后需要调整文本块的一些属性,则无需手动重新定位以下构建块。

让我们来回忆一下,canvas.font 属性是多个属性的组合,可决定如何渲染文本。在 Variety 与后续构建块之间,无需指定字体。在 'is the''spice of life!' 块之间,也不需要使用字体大小和粗细的技术。这里使用了一个简单的机制来记住之前应用过的属性,使您无需重新配置之前应用文本的预期属性。请参阅 参考资料,以获得有关 CanvasText 类的一个示例。

为了得到上述结果,只需对希望渲染的文本样式属性进行分组,相应地指定样式属性,然后调用闭合组的 CanvasText 对象的 append() 函数,并存储它供日后使用。渲染文本时,需要遍历这些组,然后指定样式并应用它。在迭代组时,保留之前的样式状态的工作记录,并在必要时覆盖这些记录。这些操作实现了文本存储,并降低了将文本渲染到 canvas 的技术难度。这只是使用 wrapper 类或构建自动化 canvas 文本渲染过程的一个优势。

自动换行

自动换行是很多人需要的一个功能。当一个 HTML 元素包含因太长而无法放在一行的内容时,这个功能就起作用了。我们无需考虑文本的长度、容器的宽度等。然而,对于 canvas,换行并不是这么简单。HTML canvas 元素目前不包含管理这些事情的内置功能,因此您必须使用之前提到过的工具和方法以编程方式实现。

例如,要让文本在超过容器宽度时换行,则必须知道所用容器的宽度、希望渲染的文本宽度、文本行高度。您还需要创建虚拟容器,提供指定该容器宽度的方法。在创建这样的流程后,我们准备开始探索在 canvas 中实现自动换行所需的逻辑。

清单 7 详尽说明了 清单 6,并提供一些要传递给 CanvasText 类的构造函数的参数。

清单 7. 将函数传递到 CanvasText 类的构造函数中
var myText = new CanvasText(
    {x: 50, y: 50},
    {width: 100, height: 200}
);
myText
    .family('Arial')
    .size('18px')
    .weight('bold')
    .color('green')
    .append('Variety')
    
    .size('12px')
    .weight('normal')
    .color('blue')
    .append('is the')
    
    .style('italic')
    .color('red')
    .append('spice of life!')
    
    .render();

其他参数包含要渲染的文本位置的 xy 坐标、文本大小和容器大小。请注意,这里指定了一个显式宽度来限制内容换行。

有了要遵从的容器大小后,您可以创建一些功能来实现预期结果了。除了遍历不同的样式外,您还需要遍历每一个单词,以获取一些数据,了解经过渲染的文本实际需要多宽。有了这些信息,就可以跟踪已经渲染文本的宽度,并确定下一个词是否会呈现在预期范围之外。幸运的是,measureText() 函数正好提供了所需的信息。

清单 8 显示使用 CanvasText 类实现自动换行所需的代码。

清单 8. 包含自动换行功能
// use measure text 
var currentWordWidth = context.measureText(currentWord).width

// word wrap code here
if (textAdjustment.x + currentWordWidth > this._size.x || textToDraw == '\n') {
    textAdjustment.x = 0;
    textAdjustment.y += parseInt(previousFontOptions.size, 10);
}

最终结果如 图 7 中所示。

图 7. 自动换行结果
结果

请参阅 参考资料,以获得有关 CanvasText 类的自动换行示例。

使用一个相对简单的 wrapper 类,就可以在 canvas 中创建自动化样式和自动换行。每当您需要使用canvas渲染文本时,都可使用这个 wrapper 类。


尝试将所有组件汇总在一起

我们往往将可用的 UI 看得理所当然。从 HTML 到 Flash 再到 Silverlight,它们都以文本、菜单、滚动条和表单元素的形式提供一组基本的 UI 组件。

下一个示例是一个简单的太空射击游戏的基本设计。您要创建的是:可围绕游戏区飞行的一个太空船组件、一个聊天窗口和一个商店。一些组件需要动态化,而另一些组件提供可能需要使用频繁更新或渲染的文本信息。

请参阅 参考资料,以获得完整的太空射击游戏示例。

HTML 方法

第一步是尝试使用基本 HTML 标记创建每个游戏组件。只需少许 CSS 知识,就可以快速创建商铺和聊天系统这样的 UI 组件。

下面到了一个有趣的部分:太空船。对于 HTML 实现,可以创建一个简单的 DIV 元素,为太空船附加一个背景图像,添加元素来显示玩家姓名和健康状况等信息。到目前为止,用于这些组件的代码完全源于示例的 HTML 和 CSS。

清单 9 显示了太空飞船所需的一些 CSS 和显示在其下面的文本。您稍后要在 DOM 渲染函数中更新包含 player 类的 DIV 位置来完成动画。

清单 9. 样式化玩家的太空船、姓名和健康状况的 CSS
.player {
    position: absolute;
    width: 100px;
    height: 100px;
}

.text-under-ship {
    position: absolute;
    top: 100px;
    left: 0;
    width: 100px;
}

.name {
    font-family: Georgia;
    font-size: 15px;
    font-weight: bold;
    color: red;
}

.health {
    font-family: Georgia;
    font-size: 10px;
    color: yellow;
}

此时您可以开始使用处理游戏逻辑所涉及的代码了,如 清单 10 中所示。代码主要集中在 JavaScript 的 Game 对象中。它会负责处理用户输入,并调用相关组件的 update 和 render 函数。

清单 10. 用于驱动应用程序的 gameLoop 函数
gameLoop: function() {

    // calculate time elapsed since last update
    var currentTime = new Date().getTime();
    var elapsed = currentTime - this._previousTime;

    // call updates
    Ship.update(elapsed);

    // call renders
    if (this._canvasRendering) {
        CanvasManager.render();
        Ship.renderCanvas();
    } else {
        Ship.renderDOM();
    }

    // store current time as the previous update time
    this._previousTime = currentTime;
}

示例的一个重要组成部分是 Ship 类的 update 函数。这些函数负责管理和更新渲染代码所使用的太空船的速度、方向和位置。

到目前为止,不管渲染方法是什么,创建的大部分内容都是可重用的。清单 11 进一步探索了 DOM 与 canvas rendering 函数之间的区别。

清单 11. HTML rendering 函数
renderDOM: function() {
    var player = jQuery('#player');

    player .css({
        left: this._position.x,
        top: this._position.y
    });

    var rotationTransform = 'rotate(' + (this._rotation / 100 * 360) + 'deg)';
    var ship = player.find('#ship')
        .css('transform', rotationTransform )
        .css('-webkit-transform', rotationTransform )
        .css('-moz-transform', rotationTransform )
        .css('-ms-transform', rotationTransform )
        .css('-o-transform', rotationTransform );
}

清单 11 中的代码最少,因为它使用 jQuery 来定位太空船并设置旋转。要实现真正的跨浏览器兼容性,必须设置各供应商风格的 transform 属性。

HTML 方法最终实现起来相对简单,不怎么复杂,且只需少量代码。它的劣势在于,鉴于渲染 DOM 元素的浏览器的开销大且实现可接受帧速的可能性较低。

canvas 方法

在使用 canvas 之前,您需要引用要使用的 canvas 对象。在此之后,您需要初始化想要使用的上下文。这是本文之前讨论过的上下文,是使用 canvas 元素的手段。

使用一个简单的 canvas manager 类,如 清单 12 中所示。

清单 12. canvas manager
var CanvasManager = {
    canvas: null,
    context: null,
    _size: null,

    init: function() {
        this.canvas = document.getElementById('canvas-game-area');
        this.context = this.canvas.getContext('2d');

        this._size = {
            x: this.canvas.width,
            y: this.canvas.height,
        }
    },

    render: function() {
        this.context.clearRect(0, 0, this._size.x, this._size.y);
    }
}

在大多数应用程序中,每当您想要更新 canvas 上渲染的内容时,必须首先清除之前渲染的内容,获得一个干净的显示区。您可以使用 clearRect() 函数实现这一点,如 清单 13 中所示。

清单 13. canvas 渲染函数
renderCanvas: function() {
    var context = CanvasManager.context;
    
    // save the context to prepare for our 
    // upcoming translation and rotation
    context.save();

    // translate the canvas to the ship's center position
    context.translate(
        this._position.x + 50, 
        this._position.y + 50
    );

    // rotate the canvas to show the angle the ship is pointing
    context.rotate(this.getRotationInRadians());

    // draw the ship with an offset of half the height
    // and width to center the image
    context.drawImage(
        this._displayImage, 
        -50,
        -50
    );

    // restore the context
    context.restore();

    this._playerName.render();
}

清单 13 中的代码显示了 canvas 方法的一些优势和劣势。主要优势是能提高性能。当您有多个组件要渲染时,会扩大效果,如 图 8 中所示。

图 8. 增加要渲染的太空船的数量以扩大性能差异
增加要渲染的太空船的数量以扩大性能差异

示例目前在渲染相互叠加的 50 艘太空船(参见 参考资料 中的工作示例)。显然,HTML 版本滞后不少。切换到 canvas 版本能够显著提高性能。

两个方法的代码量明显不同。对于 HTML 版本,我们使用 jQuery 通过一个函数调用来完成太空船的定位。而在使用 canvas 方法时,就需要多用一些代码。

清单 13 中,还有另外一个用于绘制玩家姓名的调用。在 HTML 方法中不需要这个调用,因为您要安置一个同时包含太空船和附加文本的元素。不过在 canvas 中,就没有这么方便。为了完成玩家姓名的渲染,该示例使用了 CanvasText 类,有效地将渲染太空船及其附加文本的所有工作都移出 DOM,然后将这些工作移入 canvas。


结束语

在这个由两部分组成的 系列文章 中,我们探讨了选择 canvas 还是以 HTML 为中心的架构的标准。在本文中,我们了解了动画考虑事项,以及如何克服文本渲染限制。本文中的一些示例展示了如何将这些概念集中起来,提供了对 canvas-HTML 混合架构的不同方法的洞察。


下载

描述名字大小
本文的源代码canvashtmlpt2sourcecode.zip20KB

参考资料

学习

获得产品和技术

  • jQuery:是一个流行的 JavaScript 库,可简化 HTML 文档遍历、事件处理、动画和 Ajax 交互,从而加速 Web 开发。
  • Kibo:是专为即时跨浏览器键盘事件处理而编写的另一个流行的库。
  • IBM 产品评估版:下载或 在线试用 IBM SOA Sandbox,并开始使用来自 DB2、Lotus、Rational、Tivoli 和 WebSphere 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Open source
ArticleID=856151
ArticleTitle=使用 HTML 标记来补充 canvas,第 2 部分: 动画和文本渲染
publish-date=02062013