第1章 基础知识
1.1 canvas元素
- canvas通过context对象绘制。
- el.getContext('2d')获取2d绘图环境对象。
- 默认canvas大小300 * 150。可以通过指定width和height属性值修改大小。
案例:example
1.1.1 canvas元素的大小与绘图表面的大小
- canvas元素有两套尺寸:元素本身大小和元素绘图表面的大小。css只能改变元素本身大小,这会造成元素绘图表面大小的缩放。而width和height属性可以改变两者的大小。
1.1.2 canvas元素的api
属性 | 描述 |
---|---|
width | 宽度。没有'px'。默认300 |
height | 高度。没有'px'。默认150 |
getContext() | 返回canvas绘图环境对象。参数'2d' |
toDataURL(type, quality) | 返回一个数据地址(data URL)。type为类型,例如image/jpeg或image/png。默认后者。quality是0-1.0之间的值,表示JPEG图片的质量。 |
toBlob(callback, type, args) | 创建一个用于表示此canvas元素图像文件的Blob。浏览器会以一个指向blob的引用为参数调用callback。后面两个参数同上。 |
1.2 Canvas的绘图环境
1.2.1 2d绘图环境
属性 | 简介 |
---|---|
canvas | 指向绘图环境所属的canvas对象。 |
fillStyle | 填充样式。 |
font | 调用fillText()或strokeText()方法时所使用的字型。 |
globalAlpha | 全局透明度设定。0(完全透明)-1(完全不透明)。浏览器会将每个像素的alpha值与该值相乘。 |
globalCompositeOperation | 物体绘制在其他物体之上时所采用的绘制方式。有效参数在2.14节。 |
lineCap | 线段端点样式:butt, round, square。默认butt。 |
lineWidth | 线段宽度。默认1.0。 |
lineJoin | 线段相交时如何绘制焦点。可取的值:bevel, round, miter。默认1.0。 |
miterLimit | 如何绘制miter形式的线段焦点。详情在2.8.7节。 |
shadowBlur | 值越高,阴影延伸越远。该值不是指阴影的像素长度,而是代表高斯模糊方程式中的参数值。高斯模糊:http://zh.wikipedia.org/zh-cn/高斯模糊 |
shadowColor | 阴影颜色。通常采用半透明色。 |
shadowOffsetX | 阴影水平偏移量。 |
shadowOffsetY | 阴影垂直偏移量。 |
strokeStyle | 路径描边样式。 |
textAlign | 决定了以fillText()或strokeText()进行绘制时,所画文本的水平对齐方式。 |
textBaseline | 决定了以fillText()或strokeText()进行绘制时,所画文本的垂直对齐方式。 |
1.2.2 Canvas状态的保存与恢复
- Canvas的API提供了save()和restore()的方法,用来保存及恢复当前canvas绘图环境的所有属性。
function drawGrid(strokeStyle, fillStyle) {
controlContext.save() // Save the context on a stack
controlContext.fillStyle = fillStyle
controlContext.strokeStyle = strokeStyle
// Draw the grid……
controlContext.restore() // Restore the context from the stack
}
1.3 本书程序清单的规范格式
1.4 开始学习HTML5
1.4.1 规范
- 以下三个规范与本书所讲内容有关:
- HTML5 Canvas
- 基于脚本的定时控制动画
- HTML5 视频与音频
1.4.2 浏览器
- 对于IE6\IE7\IE8,有两个选择:explorecanvas和Google Chrome Frame。
1.4.3 控制台与调试器
- 在基于webkit的浏览器中,可以通过console.profile()和console.profileEnd()进行性能分析。
1.4.4 性能
- 性能分析工具
- 性能分析器(profile)
- 时间轴工具(Timeline)
- jsPerf
1.4.4.1 性能分析器与时间轴工具
1.4.4.2 jsPerf
1.5 基本的绘制操作
- 绘制时钟用到的API:
- arc()
- beginPath()
- clearRect()
- fill()
- fillText()
- lineTo()
- moveTo()
- stroke()
案例:clock
1.6 事件处理
1.6.1 鼠标事件
- 注意坐标的转换
案例:spriteSheet
1.6.2 键盘事件
- 键盘事件发生在当前拥有焦点的HTML元素上。假如没有元素拥有焦点,事件会上移至window与document对象。canvas是一个不可获取焦点的元素,所以应该监听document或window。
1.6.3 触摸事件
- 详情第11章
1.7 绘制表面的保存与恢复
- 使用getImageData()与putImageData()方法来保存和恢复绘图环境的绘图表面。
- 绘图系统:
- 立即模式:canvas是立即模式的,并不会维护所绘制对象的列表。
- 保留模式:svg是保留模式的,会维护绘制对象的列表。
1.8 在Canvas中使用HTML元素
案例:rubberBands
1.9 打印Canvas的内容
- 实现步骤:
- 向网页中加入一个不可见的图像元素,然后给该元素设置好id值,但是不要设定其src属性。
- 通过css,调整图像的位置与大小,使其刚好覆盖在canvas之上。
- 向网页中加入一个用于抓取快照的控件。
- 获取指向刚才哪个不可见图像元素的引用。
- 获取指向快照抓取控件的引用。
- 当用户激活控件以抓取快照时:
- 调用toDataURL()方法来获取数据地址。
- 将数据地址设定为不可见图像元素的src属性值。
- 将图像元素设置为可见,将canvas设置为不可见。
- 当用户激活控件以返回到Canvas时:
- 使canvas元素可见,使图像元素不可见。
- 如有必要,则重绘canvas。
1.10 离屏canvas
案例:imageClock
1.11 基础数学知识简介
1.11.1 求解代数方程
1.11.2 三角函数
1.11.3 向量运算
1.11.4 根据计量单位来推导等式
1.12 总结
第2章 绘制
- 将学到的技能
- 对线条、弧形、圆、曲线及多边形进行描边与填充。
- 通过设置绘图环境的属性来改变所绘图形的外观。
- 绘制圆角矩形。
- 绘制并编辑贝塞尔曲线。
- 对2d绘制环境进行扩展,使之可以绘制虚线。
- 使用纯色、渐变色及图案来对图形进行描边及填充。
- 用阴影效果来模拟具有深度的立体图形效果。
- 在不影响背景的情况下,使用“剪辑区域”技术来擦除图形与文本。
- 实现橡皮筋式辅助线技术,以便让用户可以交互式地绘制图形。
- 在canvas中拖动图形对象。
- 坐标系统的变换。
2.1 坐标系统
- Canvas的坐标系默认是原点在左上角。
- Canvas的坐标系不是固定的。可以采用如下方式来变换坐标系统:
- 平移(translate)
- 旋转(rotate)
- 缩放(scale)
- 创建自定义的变换方式,例如切变
2.2 Canvas的绘制模型
- 在向canvas之上绘制图形或图像时,浏览器要按照如下步骤来操作:
- 将图形或图像绘制到一个无限大的透明位图中,在绘制时遵从当前的填充模式、描边模式以及线条样式。
- 将图形或图像的阴影绘制到另外一副位图中,在绘制时使用当前绘图环境的阴影设定。
- 将阴影中每一个像素的alpha分量乘以绘图环境对象的globalAlpha属性值。
- 将绘有阴影的位图与经过剪辑区域剪切过的canvas进行图像合成。在操作时使用当前的合成模式参数。
- 将图形或图像的每一个像素颜色分量,乘以绘图环境对象globalAlpha属性值。
- 将绘有图形或图像的位图,合成到当前经过剪辑区域剪切过的canvas位图之上,在操作时使用 当前的合成操作符(composition operator)。
- 只有在启用阴影效果时才会执行第2-4步。
2.3 矩形的绘制
- 矩形的清除:clearRect(double x, double y, double w, double h)
- 矩形的描边:strokeRect(double x, double y, double w, double h)
- 矩形的填充:fillRect(double x, double y, double w, double h)
- 矩形的路径:rect(double x, double y, double w, double h)
- 描边:stroke()
- 填充:fill()
2.4 颜色与透明度
- 描边样式(颜色、渐变、图案):strokeStyle
- 填充样式(颜色、渐变、图案):fillStyle
2.5 渐变色与图案
2.5.1 渐变色
方法 | 描述 |
---|---|
createLinearGradient(double x0, double y0, double x1, double y1) | 创建线性渐变。传入该方法的参数表示渐变线的两个端点。 |
createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1) | 创建放射渐变。该方法的参数代表位于圆锥形渐变区域两端的圆形。 |
两个方法返回的实例都可以调用addColorStop()添加颜色停止点。
2.5.1.1 线性渐变
2.5.1.2 放射渐变
2.5.2 图案
- 图案可以是以下三种之一:image元素、canvas元素或video元素。
- 可以用createPattern()方法来创建图案,该方法接受两个参数:图案本身,以及重复方式。第二个参数可以取如下的值:repeat、repeat-x、repeat-y或no-repeat。
2.6 阴影
- 指定阴影效果的4个属性值:
- shadowColor:CSS3格式的颜色
- shadowOffsetX:从图形或文本到阴影的水平像素偏移
- shadowOffsetY:从图形或文本到阴影的垂直像素偏移
- shadowBlur:一个与像素无关的值。该值被用于高斯模糊方程之中,以便对阴影进行模糊化处理
案例:innerShadow
2.7 路径、描边和填充
2.7.1 路径与子路径
在某一时刻,canvas之中只能有一条路径存在,Canvas规范将其称为“当前路径(current path).然而,这条路径却可以包含许多子路径(subpath)。
非零环绕规则:从路径中任意区域画一条线到路径外,如果逆时针方向与顺时针方向交点数量相同,则此区域不在路径内部,无需填充,否则填充。
2.7.2 剪纸效果
案例:cutout
- arc()方法可以控制路径方向,而rect()方法则不行。但可以写一个简单的函数实现。
- 如果在当前路径中存在子路径的情况下调用arc()方法,那么此方法就会从子路径的终点向圆弧的起点画一条线。如果不希望这条连线出现,可以在调用arc()方法之前调用beginPath()清除当前路径下的所有子路径。
2.8 线段
- Canvas绘图环境提供了两个可以用来创建线性路径的方法:moveTo()与lineTo()。
- 要使线性路径(或者俗称“线段”)出现在canvas之中,您必须在创建路径之后调用stroke()方法。
2.8.1 线段与像素边界
- 如果在像素边界处绘制1像素宽度的线段,那么实际绘制的结果将是两个像素宽度。解决方法是将绘制位置偏移半个像素。这样路径就在像素点的中间,而不是边界处。
2.8.2 网格的绘制
案例:cutout
2.8.3 坐标轴的绘制
2.8.4 橡皮筋式的线条绘制
2.8.5 虚线的绘制
2.8.6 通过扩展CanvasRenderingContext2D来绘制虚线
- 实现方法:
- 获取指向绘图环境对象中moveTo()方法的引用。
- 向Canvas绘图环境对象中新增一个名为lastMoveToLocation属性。
- 重新定义绘图环境对象的moveTo()方法,将传给该方法的点保存到lastMoveToLocation的属性中。
- 实现dashedLineTo()方法。
- 代码:
const context = document.querySelector('#canvas').getContext('2d'),
moveToFunction = CanvasRenderingContext2D.prototype.moveTo
let CanvasRenderingContext2D.prototype.lastMoveToLocation = {}
CanvasRenderingContext2D.prototype.moveTo = function(x, y) {
moveToFunction.apply(context, [x, y])
this.lastMoveToLocation.x = x
this.lastMoveToLocation.y = y
}
CanvasRenderingContext2D.prototype.dashedLineTo = function(x, y, dashLength) {
dashLength = dashLength === undefined ? 5 : dashLength
let startX = this.lastMoveToLocation.x
let startY = this.lastMoveToLocation.y
let deltaX = x - startX
let deltaY = y - startY
let numDashes = Math.floor(Math.sqrt(deltaX * deltaX + deltaY * deltaY) / dashLength)
for (let i = 0; i < numDashed; ++i) {
this[ i % 2 === 0 ? 'moveTo' : 'lineTo' ](
startX + (deltaX / numDashes) * i,
startY + (deltaY / numDashes) * i
)
}
this.moveTo(x, y)
}
2.8.7 线段端点与连接点的绘制
属性 | 描述 | 取值范围 | 默认值 |
---|---|---|---|
lineWidth | 线宽 | 非零的正数 | 1.0 |
lineCap | 线段端点 | butt、round、square | butt |
lineJoin | 线段连接点 | round、bevel、miter | bevel |
miterLimit | 斜接线长度与二分之一线宽的比值 | 非零的正数 | 10.0 |
2.9 圆弧与圆形的绘制
- arc()和arcTo()
2.9.1 arc()方法的用法
- arc(x, y, radius, startAngle, endAngle, counterClockwise)。counterClockwise为true,是逆时针;为false,是顺时针。
2.9.2 以橡皮筋辅助线来协助用户画圆
2.9.3 arcTo()方法的用法
案例:arcTo
2.9.4 刻度仪表盘的绘制
案例:dial
2.10 贝塞尔曲线
- 二次贝塞尔曲线:quadraticCurveTo(double cpx, double cpy, double x, double y)
2.10.1 二次方贝塞尔曲线
2.10.2 三次方贝塞尔曲线
2.11 多边形的绘制
2.12 高级路径操作
- isPointInPath(). 如果某个点在当前路径中,那么方法就返回true。
2.12.1 拖动多边形对象
ps: 此后,不再写案例
2.12.2 编辑贝塞尔曲线
2.12.3 自动滚动网页,使某段路径所对应的元素显示在视窗中
- scrollPathIntoView(). 该方法会让网页自行滚动,使当前路径所对应的元素显示在视窗中。
2.13 坐标变换
- 可以对Canvas坐标系进行移动、旋转、缩放等操作,方便后续的绘图。
2.13.1 坐标系的平移、缩放与旋转
方法 | 描述 |
---|---|
rotate(double angleInRadians) | 按照给定的角度来旋转坐标系 |
scale(double x, double y) | 在X与Y方向上分别按给定的数值来缩放坐标系 |
translate(double x, double y) | 将坐标系平移到给定的X、Y坐标系 |
2.13.2 自定义的坐标变换
- translate()、rotate()、scale()都是通过操作变换矩阵实现功能的。
- transform()、setTransform()可以直接改变变换矩阵。
- 多次调用transform()方法所造成的变换效果是累积的,而每次只要调用setTransform()方法 ,它就会将上一次的变换矩阵 彻底清除。
2.13.2.1 坐标变换所用的代数方程
- 平移:
x' = x + dx
y' = y + dy
- 缩放:
x' = x * sx
y' = y * sy
- 旋转:
y' = x * cos(angle) - (y * sin(angle))
y' = y * cos(angle) - (x * sin(angle))
2.13.2.2 transform()与setTransform()方法的用法
- transform()与setTransform()方法都需要6个参数:
transform(a, b, c, d, e, f)
setTransform(a, b, c, d, e, f)
// 坐标变换通用公式
x' = ax + cy + e
y' = bx + dy + f
- 实现平移:如果a=1, b=0, c=1, d=0,那么通过参数e和f就可以进行单纯的坐标系平移操作:
x' = x + e
y' = y + f
- 实现缩放:将参数a与d分别设置成坐标系在X与Y轴方向上的缩放系数,并将其他参数都设置为0:
x' = ax
y' = dy
- 实现旋转:a=cos(angle), b=sin(angle), c=-sin(angle), d=cos(angle), e=0, f=0:
x' = cos(angle) * x - sin(angle) * y + 0
y' = sin(angle) * x + cos(angle) * y + 0
2.13.2.4 错切
- 根据通用公式,x可以影响y的变换,y可以影响x的变换。因而可以实现错切(切变)。
// 水平错切
tranform(1, 0, 0.75, 1, 0, 0)
=>
x' = x + 0.75y
y' = y
2.14 图像合成
- globalCompositionOperation属性改变默认的图像合成行为。其可取值叫做Porter-Duff操作符。
- globalCompositionOperation可取值:source-atop、source-in、source-out、source-over、destination-atop、destination-in、destination-out、destination-over、lighter、copy、xor
2.15 剪辑区域
- 剪辑区域(clipping region)是在canvas之中由路径所定义的一块区域,浏览器会将所有的绘图操作都限制在本区域内执行。
- 剪辑区域的默认大小与canvas一致,可以通过创建路径 并调用Canvas绘图环境对象的clip()方法来显式地设定剪辑区域。
- 因为clip()方法会将剪辑区域设置为当前剪辑区域与当前路径的交集,所以对该方法的调用一般都是嵌入save()与restore()方法之间的。否则,剪辑区域将会越来越小。
第3章 文本
- 方法:
- strokeText(text, x, y)
- fillText(text, x, y)
- measureText(text)
- 属性:
- font
- textAlign
- textBaseline
3.1 文本的描边与填充
- fillText()与strokeText()方法还会接受一个可选的第4参数,该参数以像素为单位指定了所绘文本的最大宽度。如果绘制的文本超过了此宽度,那么,Canvas规范则要求浏览器缩小文本的尺寸,使之符合调用方法时所指定的最大宽度。
3.2 设置字型属性
- font属性支持CSS3格式的字型语法,但需要注意,font属性不支持inherit和initial。此外,在Canvas中设置line-height属性时,浏览器将忽略其值,因为规范要求浏览器必须将该值设置为normal。
3.3 文本的定位
3.3.1 水平与垂直定位
- textAlign属性可以取的值:
- start
- center
- end
- left
- right
- textBaseline属性可以取的值:
- top
- bottom
- middle
- alphabetic
- ideographic
- hanging
3.3.2 将文本居中
function drawText() {
context.fillStyle = 'blue'
context.strokeStyle = 'yellow'
context.fillText(text, canvas.width/2, canvas.height/2)
context.strokeText(text, canvas.width/2, canvas.height/2)
}
context.textAlign = 'center'
context.textBaselint = 'middle'
3.3.3 文本的度量
- measureText()方法可以获取文本在当前字型下的宽度。
- 根据规范,measureText()返回的值不一定很精确,但一般情况下不影响。
3.3.4 绘制坐标轴旁边的文本标签
3.3.5 绘制数值仪表盘周围的文本标签
3.3.6 在圆弧周围绘制文本
3.4 实现文本编辑控件
3.4.1 指示文本输入位置的光标
- 文本高度的计算:用measureText()测量‘M’的宽度,再稍微增加一点就可以得出近似的文本高度了。
3.4.1.1 光标的擦除
3.4.1.2 光标的闪烁效果
3.4.2 在Canvas中编辑文本
3.4.3 文本段的编辑
3.5 总结
第4章 图像与视频
- Canvas的绘图环境对象提供了如下4个用于绘制及操作图像的方法:
- drawImage()
- getImageData()
- putImageData()
- createImageData()
4.1 图像的绘制
4.1.1 在canvas之中绘制图像
- drawImage()方法在绘制图像时不会考虑当前路径,然而,它却会将globalAlpha设置、阴影效果、剪辑区域,以及全局图像合成操作符等属性运用到图像的绘制之中。
4.1.2 drawImage()方法的用法
drawImage()方法会将一幅图像绘制到一个canvas中,所绘的图像叫做“源图像”(source image),而绘制到的地方则叫做“目标canvas”(destination canvas).
-
drawImage()方法可以接受以下3套参数:
- drawImage(image, dx, dy)
- drawImage(image, dx, dy, dw, dh)
- drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
4.2 图像的缩放
4.3 将一个Canvas绘制到另一个Canvas之中
4.4 离屏canvas
4.5 操作图像的像素
4.5.1 获取图像数据
4.5.1.1 ImageData对象
- getImageData()方法所返回的ImageData对象包含下列三个属性:
- width:以设备像素(device pixel)为单位的图像数据宽度。
- height:以设备像素为单位的图像数据高度。
- data:包含各个设备像素数值的数组。
- getImageData()方法获取的是设备像素,不是css像素。
4.5.1.2 结合使用putImageData方法与脏矩形技术对图像数据进行局部渲染
- 在使用putImageData()方法向canvas之中绘制图像数据时,诸如globalAlpha与globalCompositeOperation这样的全局canvas属性值,不会影响到所绘的图像。浏览器也不会在绘制时运用图像合成、透明混合或阴影等效果。drawImage()方法与之相反,它会受到上述所有全局属性的影响。
4.5.2 修改图像数据
4.5.2.1 使用createImageData()方法创建ImageData对象
4.5.2.1.1 ImageData对象中的数组
- ImageData对象中的data属性指向一个包含8位二进制整数的数组,这些整数的值位于0-255之间,分别交替表示一个像素的红、绿、蓝及透明度分量。
4.5.2.2 图像数据的遍历方式
4.5.2.3 图像滤镜
负片滤镜会从255之中减去每个像素的红、绿、蓝分量值,再将差值设置回去,这样也就等于“反转”了该像素的颜色。
黑白滤镜会计算出每个像素红、绿、蓝分量值的平均值,然后将三个分量都设置为这一均值,于是,就把图像由彩色变成了黑白。
4.5.2.4 再谈设备像素与CSS像素的区别
4.5.2.5 用工作线程处理图像
4.6 结合剪辑区域来绘制图像
4.7 以图像制作动画
- 在某段时间内持续向一幅图片运用滤镜,就可以实现动画了。
4.8 图像绘制的安全问题
基于安全考量,HTML5 Canvas规范允许绘制不属于自己的(也就是其他域中的)图像,然而,你不能通过Canvas API保存或修改其他域中的图像。
Canvas绘图安全机制原理:每个canvas都有一个名为origin-clean的标志位,它的初始值是true。如果使用drawImage()绘制了一幅其他域中的图像,那么origin-clean的值就会被设置为false。与此类似,如果用drawImage()将另一个origin-clean标志为false的canvas绘制到当前 的canvas中,那么它的origin-clean标志也会被设置为false。如果在origin-clean标志为false的canvas上调用toDataURL()或getImageData()方法,那么此时浏览器则会抛出SECURITY_ERR异常。
浏览器会将用户的文件系统与运行应用程序的环境视为两个不同的域,所以,默认情况下,你不能保存或修改文件系统中的图像。但是,很多浏览器提供了临时绕过它的办法。比如谷歌浏览器可以通过指定“--allow-file-access-from-files”启动参数来获取权限。
4.9 性能
略
4.10 放大镜
4.11 视频处理
var video = document.getElementById('video') // A <video> element
···
context.drawImage(video, 0, 0) // Draw video frame
有了调用drawImage()方法将视频的某一帧绘制到canvas中的功能,我们就可以结合vieo与canvas元素来做即时视频处理了。
4.11.1 视频格式
4.11.2 在Canvas中播放视频
4.11.3 视频处理
第5章 动画
5.1 动画循环
- setTimeout()和setInterval()有很多用途,但它们并不是专门用来实现动画的。实现动画的首选方式是requestAnimationFrame()。
5.1.1 通过requestAnimationFrame()方法让浏览器来自行决定帧速率
-
window.setInterval()或window.setTimeout()制作动画并不理想。这是因为setInterval()与setTimeout()方法具有如下特征:
- 它们都是通用方法,并不是专为制作动画而用。
- 即时向其传递以毫秒为单位的参数值,它们也达不到毫秒级的精确性。
- 没有对调用动画循环的机制作优化。
- 不考虑绘制动画的最佳时机,而只会一味地以某个大致的时间间隔来调用动画循环。
requestAnimationFrame()方法会返回一个long类型的对象,用作标识回调函数身份的句柄(handle)。以后若要取消回调函数的执行,则可将其传给calcelRequsetAnimationFrame()方法。
一般来说,动画都是基于时间的,所以requestAnimationFrame()方法在回调动画函数时,会传递给它一个时间值,该值表示从1970年1月1日到当前所经过的毫秒数。
5.1.2 Internet Explorer 浏览器对requestAnimationFrame()功能的实现
5.1.3 可移植于各浏览器平台的动画循环逻辑
5.2 帧速率的计算
var lastTime = 0
function calculateFps() {
var now = (+new Data),
fps = 1000 / (now - lastTime)
lastTime = now
return fps
}
5.3 以不同的帧速率来执行各种任务
5.4 恢复动画背景
- 处理背景无外乎三种方法:
- 将所有内容都擦除,并重新绘制。
- 仅重绘内容发生变化的那部分区域。
- 从离屏缓冲区中将内容发生变化的那部分背景图像复制到屏幕上。
5.4.1 利用剪辑区域来处理动画背景
- 如果所绘物体不多,那么利用剪辑区域技术要比直接重绘背景要好。但如果物体很多,那么需要绘制背景的次数就增多了,性能会下降。
5.4.2 利用图块复制技术来处理动画背景
这种方法时将整个背景一次性地绘制到离屏canvas中,稍后从离屏canvas中只将修复动画背景所需的那一块图像复制到屏幕上即可。
图块赋值要比使用剪辑区域的速度快,然而它需要一个离屏canvas,这会占据更多的内容。
5.5 利用双缓冲技术绘制动画
假如动画是单缓冲(single buffered)的,那么就意味着其内容会被立即绘制到屏幕cnavas中。这样的话,擦除背景的那一瞬间所造成的空白可能会使动画看起来有些闪烁。
防止闪烁的一种方法就是使用双缓冲(double buffering)。如果用双缓冲,那么就不是将动画内容直接绘制到屏幕canvas中了,而是先将所有东西都绘制到离屏canvas里面,然后把该canvas的全部内容一次性地复制到屏幕canvas中。
开发者不需要自己实现双缓冲,因为浏览器已经内建了对双缓冲技术的支持。
不建议开发者自己实现双缓冲,但有时还是需要用到多缓冲技术。比如图块复制技术处理动画背景可以提高绘制复杂背景图像的效率。
5.6 基于时间的运动
- 想让动画以稳定的速度运行,而不受帧速率的影响,那就要根据物体的速度计算出它在两帧之间所移动的像素数。
5.7 背景的滚动
5.8 视差动画
- 图层由远及近,速度由慢到快。
5.9 用户手势
5.10 定时动画
5.10.1 秒表
5.10.2 动画计时器
5.11 动画制作的最佳指导原则
- 在制作动画时,请牢记下列指导原则:
- 使用类似requestNextAnimationFrame()这样的“polyfill式”方法来保持浏览器兼容性。
- 将业务逻辑的更新与动画的绘制分开。
- 使用“基于时间的运动”来协调动画的播放速度。
- 用剪辑区域或图块复制技术将复杂的背景图像恢复到屏幕上。
- 必要时可使用一个或多个离屏缓冲区以提升背景的绘制速度。
- 不要手工实现传统的双缓冲算法:浏览器会自动实现它的。
- 不要通过CSS指定阴影及圆角效果。
- 不要在Canvas中进行带有阴影效果的绘制操作。
- 不要在播放动画时分配内存。
- 使用性能调试及时间轴工具来监控并改善动画的绘制效率。
5.12 总结
第6章 精灵
- 本章将会实现三种设计模式:策略模式(Strategy)、命令模式(Command)与享元模式(Flyweight)。策略模式用于将精灵与绘制器解耦(decouple),命令模式用于实现精灵的动作,而享元模式则可以让我们用一个实例来表示多个精灵。
6.1 精灵概述
Sprite对象的属性
|属性|描述|
|--|--|
|top|精灵左上角(upper left-hand cornor,简称ulhc)的 Y 坐标|
|left|精灵左上角的 X 坐标|
|width|精灵的宽度|
|height|精灵的高度|
|velocityX|精灵的水平速度|
|velocityY|精灵的垂直速度|
|behaviors|一个包含精灵行为对象的数组,在精灵执行更新逻辑时,该数组中的各行为对象都会被运用于此精灵|
|painter|用于绘制此精灵对象的绘制器|
|visible|表示此精灵是否可见的 boolean 标志|
|animating|表示此精灵是否正在执行动画效果的 boolean 标志|Sprite 构造器接受三个参数:精灵的名称、绘制器及行为数组
精灵对象有两个方法:paint() 与 update()。update() 方法用于执行每个精灵的行为,执行的顺序就是这些行为被加入精灵之中的顺序。paint() 方法则将精灵的绘制代理给绘制器来做,不过仅仅在精灵确实有绘制器并且可见时,此方法才会生效。
6.2 精灵绘制器
Sprite 对象与绘制其内容的绘制器对象之间时解耦的。如此一来,就可以在程序运行时为精灵对象动态地设定绘制器了,这极大地提升了程序的灵活度。比如说,可以实现一个精灵动画制作器,它每隔一段时间就将精灵的绘制器交换一次。
-
Painter 对象只需实现如下这个方法即可:void paint(sprite, context)。所有 Painter 对象都可被归纳为以下三类:
- 描边及填充绘制器
- 图像绘制器
- 精灵表绘制器
描边填充绘制器使用 Canvas 的图形 API 来绘制精灵,而图像绘制器则用于绘制图像。最后,精灵表绘制器用于绘制精灵表中的单个精灵。
绘制器与策略模式:精灵对象不需要自己完成检测,相反,它会将绘制操作代理给另外一个对象来做。从本质上讲,Painter 对象就是一些可以互相交换着使用的绘制算法,在程序运行时,开发者可以将其设定给精灵对象,这种特色表明,绘制器就是策略模式的一个实际用例。
6.2.1 描边与填充绘制器
描边与填充绘制器(stroke and fill painter)会调用包括 stroke() 与 fill() 在内的 Canvas 图形函数来绘制精灵。
使用一个对象来表示多个概念,这就是享元模式。它减少了需要创建的对象数,从而降低了内存占用量,这对于动画和电子游戏的制作来说是极为重要的。
6.2.2 图像绘制器
- 图像绘制器对象含有一个指向图像对象的引用,它会将此图像绘制到经由 paint() 方法所传入的绘图环境对象之上。
6.2.3 精灵表绘制器
- 为了节省磁盘空间、减少下载次数,如果用于制作动画的精灵对象其每帧所用的图像都比较小,那么就可以把它们都放在一张图片中。这张包含动画每一帧图像的图片就叫做精灵表(sprite sheet)。
6.3 精灵对象的行为
其实,只要实现了 execute(sprite, context, time) 方法的对象,都可以叫做“行为”。该方法一般会以某种方式来修改精灵的属性,比如 移动其位置,或是修改其外观。
精灵含有一个行为对象数组,它的 update() 方法会遍历该数组,使每个行为对象得以执行一次。这样的话,我们就可以把行为封装为对象,在程序运行的时候将 它添加到多个精灵之中。
行为对象更够将某种命令封装起来,它是命令模式的一个应用实例。行为对象可以被执行,也可以被存放在某个队列之中,比如精灵对象所含的行为数组就是如此。
6.3.1 将多个行为组合起来
- 精灵含有一个行为对象数组,所有开发者可以根据需要向任何精灵对象之中添加任意数量的行为对象。精灵的 update() 方法会从数组中的第一个行为对象开始,一直遍历到最后一个对象,一次调用其 execute() 方法。
6.3.2 限时触发的行为
6.4 精灵动画制作器
6.5 基于精灵的动画循环
- 动画循环代码要将更新和绘制分开。因为某个精灵对象的更新可能会影响到其他对象。
6.6 总结
第7章 物理效果
- 本章将会讲述在动画中广泛使用的几种基本物理效果:
- 重力
- 非线性运动
- 非线性动画
7.1 重力
- 在现实世界中,地球表面附近的所有物体在下坠时,其加速度都是 9.81m/s^2,也可以表示为 32ft/s^2。
7.1.1 物体的下落
- 垂直速度:V
y
= gt
7.1.2 抛射体弹道运动
7.1.3 钟摆运动
7.2 时间轴扭曲
动画计时器并无特殊之处,它们就是一种能将动画播放时间封装起来的对象,我们可以用其判断动画是否结束。
动画计时器真正派上用场的地方在于:你可以写一份 AnimationTimer.getElapsedTime() 方法的实现代码,让其返回一个与实际播放时间不同的值。这样做就可以实现“时间轴扭曲”(warp time)效果了。
例如,我们可以实现一个 getElapsedTime() 方法,一开始,让它返回大大低于实际播放时间的值。然后随着动画的播放,我们稳步地减少该方法返回值与实际播放时间的差量。如此一来,动画在时间轴上起初会播放的非常缓慢,随后则逐渐提速。这种“起初缓慢,逐渐提速”的算法就叫做缓入效果(ease-in effect)。
7.3 时间轴扭曲函数
7.4 时间轴扭曲运动
7.4.1 没有加速度的线性运动
- f(x) = x
7.4.2 逐渐加速的缓入运动
缓入运动所用的时间扭曲函数是一个幂函数。
f(x) = x^2
7.4.3 逐渐减速的缓出运动
- f(x) = 1 - (1 - x)^2
7.4.4 缓入缓出运动
从本质上讲,缓入缓出运动是一种周期运动,所以我们可以用正弦波来表示它。
f(x) = x - sin(x * 2π)/(2π)
7.4.5 弹簧运动与弹跳运动
-
弹簧运动:
- f(x) = (1 - cos(x * Npasses * π) * (1 - x)) + x
- 除了表示动画播放进度百分比的 x 之外,还有一个变量——Npasses,它表示运动物体穿越中轴的次数。
-
弹跳运动:
- f(x) = (1 - cos(x * π * N
bounces
) * (1 - x)) + x - f(x) = 2 - (((1 - cos(x * π * N
bounces
)) * (1 - x)) + x) - 第一个函数适用于当前被弹起的次数为偶数的情况,第二个函数适用于当前被弹起的次数为奇数的情况。N
bounces
均表示运动物体被弹起的总次数。
- f(x) = (1 - cos(x * π * N
7.5 以扭曲后的帧速率播放动画
- 这里的帧速率是指精灵图片切换的速率。比如0.1秒切换图片和0.3秒切换图片。
7.6 总结
第8章 碰撞检测
有些检测技术比较简单,比如外接图形判别法与光线投射法。但复杂图形则需要“分离轴定理”进行检测。
“分离轴定理”(SAT,又叫“超平面分离定理”)的判断准确度很高,而且适用范围广,可以用于检测二维平面及三维空间中的多边形碰撞。
最小平移量(minimum translation vector, 简称 MTV)。这个值恰好就是将两个物体由碰撞变为不碰撞时所需移动的最下距离。
8.1 外接图形判别法
在二维平面中执行碰撞检测时,通常会根据物体外接图形的面积来判定(在三维空间中则是根据体积)。
这里的“面积”,意思是看两个物体的表面非否发生重合,并不强调具体面积值的计算。
8.1.1 外接矩形判别法
- 外接矩形经常被当作物体的轮廓来参与碰撞检测,此时它也称作“边界框”(bounding box).
8.1.2 外接圆判别法
- 检测两个圆形是否碰撞很简单。如果两个圆心之间的距离小于两圆半径之和,那么两者就发生了碰撞。
8.2 碰到墙壁即被弹回的小球
8.3 光线投射法
- 光线投射法非常简单:画一条与物体的速度向量(velocity vector)相重合的线,然后再从另一个待检测物体出发,绘制第二条线,根据两条线的交点位置来判定是否发生碰撞。
8.4 分离轴(SAT)与最小平移向量(MTV)
外接图形判别法和光线投射法都不适用于检测任意多边形之间的碰撞。
分割轴定理可以检测多边形之间的碰撞,不过也适用于圆形、图像及精灵。
分离轴定理只适用于凸多边形。
8.4.1 使用分离轴定理检测碰撞
从概念上来说,分离轴定理很好理解,可以用通俗的语言将其数学模型表述如下:把受测的两个多边形置于一堵墙前面,用光线照射它们,然后根据其阴影部分是否相交来判断二者有没有相撞。
只要在任何一条轴上的投影不重叠,那么就说明图形没有相碰。
投影轴的数量与每个多边形的边数有关。
8.4.1.1 投影轴
在用分离轴定理检测多边形碰撞时,必须能够根据某个给定的多边形平面(polygon face),找到它的所以投影轴。
轴与多边形的边垂直。
8.4.1.2 投影
8.4.1.3 形状与多边形
8.4.1.4 多边形之间的碰撞
8.4.1.5 圆形与多边形之间的碰撞检测
- 如果要将分离轴定理运用于圆,就会发现一个问题:圆可以近似地看成一个有无数条边的正多边形,而我们不可能按照这些边一一进行投影与测试。我们只需要将圆形投射到一条投影轴上即可,这条轴就是圆心与距其最近的多边形顶点之间的连线。
8.4.1.6 图像与精灵的碰撞检测
8.4.2 根据最小平移向量应对碰撞
8.4.2.1 最小平移向量
最小平移向量是指,如果要让某个物体不再与另外一个物体相撞所需移动的最小距离。
最小平移向量的基本用法就是将两个相互碰撞的物体分开。除此之外,它还有两个用途:可以让两个物体粘连在一起,也可以让它们彼此弹开。