requestAnimationFrame
很长时间以来,计时器和定时执行都是JavaScript动画最先进的工 具。虽然CSS过渡和动画方便了Web开发者实现某些动画,但 JavaScript动画领域多年来进展甚微。Firefox 4率先在浏览器中为 JavaScript动画增加了一个名为 mozRequestAnimationFrame() 方法的API。这个方法会告诉浏览器要执行动画了,于是浏览器可以 通过最优方式确定重绘的时序。自从出现之后,这个API被广泛采 用,现在作为 requestAnimationFrame() 方法已经得到各大浏 览器的支持。
早期定时动画
以前,在JavaScript中创建动画基本上就是使用 setInterval() 来控制动画的执行。
(function() {
function updateAnimations() {
doAnimation1();
doAnimation2();
// 其他任务
}
setInterval(updateAnimations, 100);
})();
setInterval() 和 setTimeout() 的不精确是个大问题。
requestAnimationFrame
浏览器知道CSS过渡和动画应该什么时候开始,并据此计算出正确的时间间隔,到时间就去刷新用户界面。但对 于JavaScript动画,浏览器不知道动画什么时候开始。他给出的方案是 创造一个名为 mozRequestAnimationFrame() 的新方法,用以通 知浏览器某些JavaScript代码要执行动画了。这样浏览器就可以在运行 某些代码后进行适当的优化。目前所有浏览器都支持这个方法不带前 缀的版本,即 requestAnimationFrame() 。
requestAnimationFrame() 方法接收一个参数,此参数是 一个要在重绘屏幕前调用的函数。这个函数就是修改DOM样式以反映 下一次重绘有什么变化的地方。为了实现动画循环,可以把多个 requestAnimationFrame() 调用串联起来,就像以前使用 setTimeout() 时一样
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>requestAnimationFrame</title>
<style>
body {
width: 98%;
text-align: center;
}
#status {
background-color: #1890ff;
height: 20px;
border-radius: 1000px;
}
</style>
</head>
<body>
<div id="status" style="width:0;background-color: #1890ff;border-radius: 1000px;">
</div>
<button onclick="fun()">request</button>
</body>
<script>
function updateProgress() {
var div = document.getElementById("status");
console.log('div.style.width ==', div.style.width);
div.style.width = (parseInt(div.style.width) + 1) + "%";
if (parseInt(div.style.width) <= 100) {
requestAnimationFrame(updateProgress);
}
}
function fun() {
console.log('fun')
requestAnimationFrame(updateProgress);
}
</script>
</html>
cancelAnimationFrame
与 setTimeout() 类似, requestAnimationFrame() 也 返回一个请求ID,可以用于通过另一个方法 cancelAnimationFrame() 来取消重绘任务。
let requestID = window.requestAnimationFrame(()=> {
console.log('Repaint!');
});
window.cancelAnimationFrame(requestID);
canvas基本的画布功能
创建 元素时至少要设置其 width 和 height 属 性,这样才能告诉浏览器在多大面积上绘图。
出现在开始和结束标签 之间的内容是后备数据,会在浏览器不支持 元素时显 示。
<canvas id="drawing" width="200" height="200">Adrawing of something.</canvas>
getContext() 获取绘图上下文
要在画布上绘制图形,首先要取得绘图上下文。使用 getContext() 方法可以获取对绘图上下文的引用。对于平面图 形,需要给这个方法传入参数 “2d” ,表示要获取2D上下文对象。
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 其他绘制代码
}
使用 元素时,最好先测试一下 getContext() 方 法是否存在。有些浏览器对HTML规范中没有的元素会创建默认 HTML元素对象。这就意味着即使 drawing 包含一个有效的元素引 用, getContext() 方法也未必存在。
2D绘图上下文
2D绘图上下文提供了绘制2D图形的方法,包括矩形、弧形和路 径。2D上下文的坐标原点(0, 0)在 元素的左上角。所有坐 标值都相对于该点计算。默认情况下, width 和 height 表示两个方向上像 素的最大值。
填充fillStyle和描边strokeStyle
2D上下文有两个基本绘制操作:填充和描边。填充以指定样式 (颜色、渐变或图像)自动填充形状,而描边只为图形边界着色。大 多数2D上下文操作有填充和描边的变体,显示效果取决于两个属 性: fillStyle 和 strokeStyle 。
这两个属性可以是字符串、渐变对象或图案对象,默认值都 为 “#000000” 。字符串表示颜色值,可以是CSS支持的任意格式: 名称、十六进制代码、 rgb 、 rgba 、 hsl 或 hsla 。
context.strokeStyle = "red";
context.fillStyle = "#0000ff";
绘制矩形
矩形是唯一一个可以直接在2D绘图上下文中绘制的形状。与绘制 矩形相关的方法有3个: fillRect() 、 strokeRect() 和 clearRect() 。这些方法都接收4个参数:矩形 x 坐标、矩形 y 坐标、矩形宽度和矩形高度。这几个参数的单位都是像素。
fillRect() 填充矩形
fillRect() 方法用于以指定颜色在画布上绘制并填充矩形。 填充的颜色使用 fillStyle 属性指定。
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制半透明蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)";
context.fillRect(30, 30, 50, 50);
strokeRect() 描边(轮廓)矩形
strokeRect() 方法使用通过 strokeStyle 属性指定的颜色 绘制矩形轮廓。
// 绘制红色轮廓的矩形
context.strokeStyle = "#ff0000";
context.strokeRect(10, 10, 50, 50);
// 绘制半透明蓝色轮廓的矩形
context.strokeStyle = "rgba(0,0,255,0.5)";
context.strokeRect(30, 30, 50, 50);
clearRect() 方法可以擦除画布中某个区域
使用 clearRect() 方法可以擦除画布中某个区域。该方法用于 把绘图上下文中的某个区域变透明。通过先绘制形状再擦除指定区 域,可以创建出有趣的效果,比如从已有矩形中开个孔。
let drawing =document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
/*
* 引自MDN文档
*/
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制半透明蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)";
context.fillRect(30, 30, 50, 50);
// 在前两个矩形重叠的区域擦除一个矩形区域
context.clearRect(40, 40, 10, 10);
}
绘制路径
2D绘图上下文支持很多在画布上绘制路径的方法。通过路径可以 创建复杂的形状和线条。
beginPath()开始绘制
要绘制路径,必须首先调用 beginPath()
方法以表示要开始绘制新路径。然后,再调用下列方法来绘制路径。
closePath()返回起点的线
下面这个例子使用前面提到的方法绘制了一个不带数字的表盘:
let drawing =
document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d");
// 创建路径
context.beginPath();
// 绘制外圆
context.arc(100, 100, 99, 0, 2 * Math.PI,
false);
// 绘制内圆
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI,
false);
// 绘制分针
context.moveTo(100, 100);
context.lineTo(100, 15);
// 绘制时针
context.moveTo(100, 100);
context.lineTo(35, 100);
// 描画路径
context.stroke();
}
这个例子使用 arc() 绘制了两个圆形,一个外圆和一个内圆, 以构成表盘的边框。外圆半径99像素,原点为(100,100),也就是画布 的中心。要绘制完整的圆形,必须从0弧度绘制到2π弧度(使用数学常 量 Math.PI )。而在绘制内圆之前,必须先把路径移动到内圆上的 一点,以避免绘制出多余的线条。第二次调用 arc() 时使用了稍小 一些的半径,以呈现边框效果。然后,再组合运用 moveTo() 和 lineTo() 分别绘制分针和时针。最后一步是调用 stroke()
绘制文本
文本和图像混合也是常见的绘制需求,因此2D绘图上下文还提供 了绘制文本的方法,即 fillText() 和 strokeText() 。这两个 方法都接收4个参数:要绘制的字符串、 x 坐标、 y 坐标和可选的最大像素宽度。
这些属性都有相应的默认值,因此没必要每次绘制文本时都设置 它们。 fillText() 方法使用 fillStyle 属性绘制文本,而 strokeText() 方法使用 strokeStyle 属性。
fillText填充文本
strokeText描边文本
measureText()
由于绘制文本很复杂,特别是想把文本绘制到特定区域的时候, 因此2D上下文提供了用于辅助确定文本大小的 measureText() 方 法。这个方法接收一个参数,即要绘制的文本,然后返回一个 TextMetrics 对象。这个返回的对象目前只有一个属性 width , 不过将来应该会增加更多度量指标。 measureText() 方法使用 font 、 textAlign 和 textBaseline 属性当前的值计算绘制指定文本后的大小。
变换
上下文变换可以操作绘制在画布上的图像。2D绘图上下文支持所 有常见的绘制变换。在创建绘制上下文时,会以默认值初始化变换矩 阵,从而让绘制操作如实应用到绘制结果上。对绘制上下文应用变 换,可以导致以不同的变换矩阵应用绘制操作,从而产生不同的结 果。 以下方法可用于改变绘制上下文的变换矩阵。
绘制图像
2D绘图上下文内置支持操作图像。如果想把现有图像绘制到画布 上,可以使用 drawImage() 方法。这个方法可以接收3组不同的参 数,并产生不同的结果。最简单的调用是传入一个HTML的 元素,以及表示绘制目标的 x 和 y 坐标,结果是把图像绘制到指定位置。
let image = document.images[0];
context.drawImage(image, 10, 10);
以上代码获取了文本中的第一个图像,然后在画布上的坐标(10, 10)处将它绘制了出来。绘制出来的图像与原来的图像一样大。如果想 改变所绘制图像的大小,可以再传入另外两个参数:目标宽度和目标 高度。这里的缩放只影响绘制的图像,不影响上下文的变换矩阵。
context.drawImage(image, 50, 10, 20, 30);
还可以只把图像绘制到上下文中的一个区域。此时,需要给 drawImage() 提供9个参数:要绘制的图像、源图像 x 坐标、源图像 y 坐标、源图像宽度、源图像高度、目标区域 x 坐标、目标区域 y 坐标、目标区域宽度和目标区域高度。这个重载后的 drawImage() 方法可以实现最大限度的控制
context.drawImage(image, 0, 10, 50, 50, 0, 100,40, 60);
最终,原始图像中只有一部分会绘制到画布上。这一部分从(0, 10)开始,50像素宽、50像素高。而绘制到画布上时,会从(0, 100)开 始,变成40像素宽、60像素高。
阴影
2D上下文可以根据以下属性的值自动为已有形状或路径生成阴 影。
这些属性都可以通过 context 对象读写。只要在绘制图形或路 径前给这些属性设置好适当的值,阴影就会自动生成。
let context = drawing.getContext("2d");
// 设置阴影
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
渐变 CanvasGradient
渐变通过 CanvasGradient 的实例表示,在2D上下文中创建和 修改都非常简单。要创建一个新的线性渐变,可以调用上下文的 createLinearGradient() 方法。这个方法接收4个参数:起点 x 坐标、起点 y 坐标、终点 x 坐标和终点 y 坐标。调用之后,该方法会以指定大小创建一个新的 CanvasGradient 对象并返回实例。
有了 gradient 对象后,接下来要使用 addColorStop() 方 法为渐变指定色标。这个方法接收两个参数:色标位置和CSS颜色字 符串。色标位置通过0~1范围内的值表示,0是第一种颜色,1是最后 一种颜色。
let gradient = context.createLinearGradient(30,30, 70, 70);
gradient.addColorStop(0,"white");
gradient.addColorStop(1,"black");
这个 gradient 对象现在表示的就是在画布上从(30, 30)到(70, 70)绘制一个渐变。渐变的起点颜色为白色,终点颜色为黑色。可以把 这个对象赋给 fillStyle 或 strokeStyle 属性,从而以渐变填 充或描画绘制的图形
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
图案
图案是用于填充和描画图形的重复图像。要创建新图案,可以调 用 createPattern() 方法并传入两个参数:一个HTML 元素和一个表示该如何重复图像的字符串。第二个参数的值与CSS的 background-repeat 属性是一样的,包 括 “repeat” 、 “repeat-x” 、 “repeat-y” 和 “no-repeat” 。
let image = document.images[0],
pattern = context.createPattern(image,
"repeat");
// 绘制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150);
跟渐变一样,图案的起点实际上是画布的原点(0, 0)。将填 充样式设置为图案,表示在指定位置而不是开始绘制的位置显示图 案。
图像数据
2D上下文中比较强大的一种能力是可以使用 getImageData() 方法获取原始图像数据。这个方法接收4个参数:要取得数据中第一个 像素的左上角坐标和要取得的像素宽度及高度。例如,要从(10, 5)开 始取得50像素宽、50像素高的区域对应的数据
let imageData = context.getImageData(10, 5, 50,50);
返回的对象是一个 ImageData 的实例。每个 ImageData 对象 都包含3个属性: width 、 height 和 data ,其中, data 属性 是包含图像的原始像素信息的数组。每个像素在 data 数组中都由4 个值表示,分别代表红、绿、蓝和透明度值。换句话说,第一个像素 的信息包含在第0到第3个值中
let data = imageData.data,
red = data[0],
green = data[1],
blue = data[2],
alpha = data[3];
这个数组中的每个值都在0~255范围内(包括0和255)。对原始 图像数据进行访问可以更灵活地操作图像。例如,通过更改图像数据 可以创建一个简单的灰阶过滤器
let drawing =
document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
let context = drawing.getContext("2d"),
image = document.images[0],
imageData, data,
i, len, average,
red, green, blue, alpha;
// 绘制图像
context.drawImage(image, 0, 0);
// 取得图像数据
imageData = context.getImageData(0, 0,
image.width, image.height);
data = imageData.data;
for (i=0, len=data.length; i < len; i+=4) {
red = data[i];
green = data[i+1];
blue = data[i+2];
alpha = data[i+3];
// 取得RGB平均值
average = Math.floor((red + green + blue) /
3);
// 设置颜色,不管透明度
data[i] = average;
data[i+1] = average;
data[i+2] = average;
}
// 将修改后的数据写回ImageData并应用到画布上显示出来
imageData.data = data;
context.putImageData(imageData, 0, 0);
}
这个例子首先在画布上绘制了一个图像,然后又取得了其图像数 据。 for 循环遍历了图像数据中的每个像素,注意每次循环都要给 i 加上4。每次循环中取得红、绿、蓝的颜色值,计算出它们的平均 值。然后再把原来的值修改为这个平均值,实际上相当于过滤掉了颜 色信息,只留下类似亮度的灰度信息。之后将 data 数组重写回 imageData 对象。最后调用 putImageData() 方法,把图像数据 再绘制到画布上。结果就得到了原始图像的黑白版。 当然,灰阶过滤只是基于原始像素值可以实现的其中一种操作。 要了解基于原始图像数据还可以实现哪些操作,可以参考Ilmari Heikkinen的文章“Making Image Filters with Canvas”。
toDataURL() 导出图像
可以使用 toDataURL() 方法导出 元素上的图像。 这个方法接收一个参数:要生成图像的MIME类型(与用来创建图形 的上下文无关)。例如,要从画布上导出一张PNG格式的图片,可以 这样做:
let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
// 取得图像的数据URI
let imgURI = drawing.toDataURL("image/png");
// 显示图片
let image = document.createElement("img");
image.src = imgURI;
document.body.appendChild(image);
}
浏览器默认将图像编码为PNG格式,除非另行指定。Firefox和 Opera还支持传入 “image/jpeg” 进行JPEG编码。因为这个方法是 后来才增加到规范中的,所以支持的浏览器也是在后面的版本实现 的,包括IE9、Firefox 3.5和Opera 10。