本节书摘来自异步社区《JavaScript高效图形编程(修订版)》一书中的第2章,第2.1节,作者:【美】Raffaele Cecco著,更多章节内容可以访问云栖社区“异步社区”公众号查看
第2章 DHTML基础
在HTML5 Canvas、SVG和Flash等现代浏览器技术的背景下,DHTML今天看起来有点过时。不过,就像龟兔赛跑中的龟,当更令人激动的方法不能保证可用的情况下,DHTML总是那个更可靠的方案。
实际上,很多时候你只需要DHTML就够了;使用其他方法往往是因为开发者“想要”而不是“需要”。休闲游戏、图像缩放和许多其他特效都不需要借助其他“强力工具”就能完美实现。jQuery这样的库还能使其操作起来更简单。熟练的DOM操作技术加上一点点想法就能保证DHTML图形的快速和流畅。
在本章,我们将用vanilla JavaScript和DHTML开发一个快速sprite系统。出于兼容性考虑,我们会避免使用语言的最新特性,而集中于核心JavaScript的有效使用。
2.1 创建DHTML sprite
在计算机图形学中,sprite是可以用软件控制移动的二维比特图对象。在三维多边形图形学之前,视频游戏几乎无一例外的使用sprite来生成可移动的角色。如今,移动设备上的休闲游戏和其他的用户界面效果等,引起了sprite图形的复兴。你可以用DHTML来模拟sprite功能。下面章节中,我们将创建一个用于不同应用的DHTMLSprite对象。尽管创建sprite效果有更新、更快的方法,如HTML5 Canvas元素,但普通的DHTML可以提供不错的浏览器兼容性,在许多情况下作为Adobe Flash的替代方案是完全可行的。
提示:
本章中的sprite和CSS sprite是有区别的。CSS sprite是一个流行的Web设计技术,指的是仅通过改变HTML元素的CSS背景位置,使得元素显示一个大背景图像的一小部分,一般用于实现动画效果。在计算机图形学术语中,这叫做动态纹理坐标。本章提到的sprite,还是取其原意:一个移动的图形对象。同时我们也将用到CSS sprite技术来改变其图像。
DHTMLSprite应该足够灵活以用在不同应用中,并提供下列功能:
- 用一个简单的函数调用和图像索引(index)来改变其图像(动画)。
- 在内部管理自身的DOM元素。
- 不改变DOM的情况下隐藏和显示自己。
- 移除其DOM元素并进行必要的清理。
2.1.1 图像动画
没有动画的sprite很没劲,因此我们需要一个简洁的方法来改变sprite中所用的图像。尽管img元素似乎是一个很明显的选择,但它需要对每个动画帧载入不同的图像文件。有一个更好的办法可以使用少量的图像文件,而处理多个sprite图像。
CSS的background-position(背景位置)属性使得HTML元素(如一个div)可以显示图像的一小部分。因此一个大图像可以作为许多小sprite图像的容器。要使用这些sprite图像,我们必须定义background-position属性在div内的水平和垂直位移,以及宽和高。但这种动画方式并不直接,而需要技巧。最好是通过简单的索引就能引用到sprite图像。比如在图2-1中,组成一个齿轮动画的5幅图像可以用索引0、1、2、3和4表示。而第一个正方形用索引5表示,依此类推。
我们需要将索引转化为容器图像内的像素位移。一种方法是手动创建一个表格来记录sprite图像索引和对应的像素位移。尽管这个方法很有效,但手动输入和更新这些位移将很枯燥。更好的方法是通过计算得到这些位移。
将索引转换为水平和垂直像素位移只需要很简单的算术。在图2-1中,容器图像是256像素宽,每个sprite图像(底层除外)是64像素的正方形。像素位移可以用JavaScript这样计算:
注意计算出的值是负数。想象div元素是在对准第一个齿轮图像(索引为0)、宽与高各64像素的正方形。为了显示索引为1的下一张图像,容器图像必须向左移64像素(负水平位移)。如果要显示索引为4的最后一个齿轮图像,容器图像必须向上移64像素(负垂直位移)。
如何处理不同大小的sprite呢?在图2-1中,在容器图像底部有一些更小的32像素宽、高的sprite图像。
决定像素位移的计算和之前一样,不同的是sprite大小改为32像素:
考虑到现在的sprite大小是32像素,图2-1中第一个32像素的sprite图像(底行第一个小黑圈)的索引为32。只要sprite图像的边缘坐标是它们大小的倍数,就可以使用索引计算的方式。
提示:
图2-1中的容器图像是一个32位PNG文件,支持百万颜色和一个用于透明度的alpha通道。不过,32位PNG不适用于IE6,因为透明区域会变成不透明的灰色。一个解决方案是将图像存为8位的调色板PNG。这可以在IE6中正确显示,不过半透明区域会完全消失并显示粗糙的边缘。
2.1.2 封装和画图抽象
将所有DOM操作细节,封装在DHTMLSprite中,隐藏在使用它的应用之外,会使代码更简单更易维护;应用可以集中于应用逻辑而不是画图细节。由于应用逻辑和画图细节的分离,将应用转为另一个画图方法如HTML5 Canvas元素或SVG变得更简单,甚至可以使应用程序根据浏览器能力选择合适的画图方法。
2.1.3 最小化DOM插入和删除
重复的增删和销毁DOM元素对性能会有不利的影响。为了降低性能影响,可以初始化一个隐藏的sprite列表。当需要sprite时,你可以将其从列表中取出并使其可见,而不是真的在DOM中插入新的东西。当sprite不再需要时,你可以将其隐藏并放回列表中。在DHTMLSprite中提供一个show和hide方法将使应用实现这项技术。
如果要永久地移除一个DHTMLSprite,应移去其DOM元素并进行相关的其他清理工作。
2.1.4 sprite代码
与其将若干单独的参数传给sprite,不如将所有设置参数放入叫做params的对象传入。除了避免参数次序的麻烦之外,还使从DHTMLSprite继承的其他对象,可以将它们自己的设置参数加入params中。任何使用params的对象都可以忽略跟它不相关的参数。表2-1显示了params对象中的参数。
下面,我们将params属性复制为局部变量。通过局部变量访问参数比通过params对象的属性要快。如此定义的局部变量是私有的,只能从DHTMLSprite内的方法访问。
接下来,我们在params.$drawTaget指定的DOM元素后加上一个sprite div元素。$element保存了一个对sprite div的引用。变量和属性名前的$符号用做提醒它们指向jQuery对象。elemStyle直接引用了sprite div的style属性,用于快速更新其CSS属性。
现在我们要给sprite div元素设置初始CSS属性。因为我们只进行一次初始化,因此可以使用方便的jQuery css()函数,尽管这也许不是改变属性最快的方式。
下面我们要在that中创建并保存一个DHTMLSprite对象。它包含了所有的sprite方法,注意that的方法可以访问前面定义的局部变量。这个that对象创建了一个闭包,它能永久访问前面DHTMLSprite函数里定义的变量。
draw方法更新sprite div元素的位置:
changeImage()方法改变显示的sprite图像。将索引转为像素位移的方法和前面描述的一样,但有些小的优化:
- 局部变量mathFloor()指向Math.floor()函数,我们通过前者来调用后者。
- index变量只乘一次。
然后,我们定义隐藏、显示和移除sprite div元素的方法:
2.1.5 一个简单的sprite应用程序
下面是一个基本的HTML页面,它初始化并显示了两个sprite。
为了创建sprite,我们需要一个包含初始化参数的对象:
下面创建两个sprite。因为两个sprite大小相等并使用同一个DOM画图区域,所以不需要改变任何参数。第一个sprite使用默认索引值0,而第二个sprite的图像索引值为5。
最后画出这两个sprite。图2-2显示了输出结果。
这个应用中没有移动也没有动画,让我们在下一个例子中“动”起来。
2.1.6 一个更动态的sprite应用程序
下面的应用展示了sprite的存在价值:动画和移动。之前我们画了两个sprite,而没有控制它们移动。这个例子中我们定义一个新对象:bouncySprite,一个会反弹的DHTMLSprite。实现方法之一是在bouncySprite中创建一个DHTMLSprite,并将其作为单独的实例控制。更简洁的方法是让bouncySprite继承所有DHTMLSprite的能力,并添加自己额外的能力。在JavaScript中这种继承和增强很简单:
为了提高速度,我们用局部变量保存设置参数。这里的params对象也包含DHTMLSprite的参数,但这些和bouncySprite无关。表2-2显示了传入的参数。
animIndex保存了当前动画图像索引:
我们在that中创建和引用一个DHTMLSprite。params对象包含了其设置参数。
接着给that引用的DHTMLSprite实例加一个moveAndDraw方法,实际上就是创建一个bouncySprite实例:
通过增加xDir和yDir变量来移动sprite的x和y位置:
下面的代码根据xDir方向对animIndex变量进行增或减,接着用取余操作(%)将其维持在−4到+4之间。如果animIndex是负的,纠正到对应的正索引。
接着检查bouncySprite是否超过了maxX和maxY定义的范围。如果超过,对移动的方向取负,使bouncySprite弹回。
更新bouncySprite动画索引,并将其画到新位置:
返回在that中引用的bouncySprite实例,供应用程序使用:
定义了bouncySprite对象后,我们可以初始一些对象,并在setInterval()或settTimeout()控制下调用它们的 moveAndDraw()方法。更好的方法是创建一个对象可以初始化和处理任意数量的bouncySprite。这个对象可以叫做bouncyBoss。bouncyBoss可以传入两个参数,如表2-3所示。
创建指定数目的bouncySprite,并放入bouncys数组中。每个bouncySprite给一个随机起始位置和移动方向(xDir和yDir),并根据$drawTarget的宽和高计算最大范围。
现在我们定义moveAll方法,它调用了bouncys数组中每个bouncySprite的moveAndDraw方法。每次移动,它创建一个setTimeOut来调用自己,实现连续的循环。
下面是使用新bouncyBoss对象的页面布局:
一个bouncyBoss创建了50个bouncySprite对象,并连续调用它们的moveAndDraw方法。图2-3显示了输出结果。