12.18 HTML Canvas访问像素值

直接操作图像的像素数据

虽然调整尺寸,裁剪和变形可用来创建有趣的图像效果,但画布还有另一个更强大的特性:像素处理。通过访问2D渲染上下文的各个像素,我们就能够得到每一个像素的颜色和阿尔法值等信息。我们还能够修改每一个像素的颜色,使之显示出截然不同的效果。

在画布中访问像素的方法是getImageData。这个方法有4个参数:要访问的像素区域原点坐标(x,y)、像素区域的宽度和高度(参见图1)。它可以用代码表示为:

context.getImageData(x, y, width, height); 

调用getImageData,但是它会返回一个2D渲染上下文ImageData 对象,这个ImageData对象包含3个属性:width 表示所访问像素区域的宽度,height 表示像素区域的高度,data 是一个包含所访问区域中全部像素信息的CanvasPixelArray。

图1
调用getImageData方法的示意图

width和height属性不需要多做解释,此处我们真正关注的是data属性。data属性存储的是一个CanvasPixelArray,它是一个JavaScript一维数组,每一个像素用4个整数值表示,范围从0至255,分别表示红(r)、绿(g)、蓝(b)和阿尔法值(a)(参见图2)。所以,数组的前4项(0-3)是第一个像素的颜色值,接下来4项(4-7)是第二个像素的颜色值,以此类推。CanvasPixelArray 在这里是关键,所以一定要正确理解它的工作原理。

图2
3×3区域的CanvasPixelArray

在详细解释之前,先看一个简单示例。我们使用图2所定义的索引数字来访问CanvasPixelArray 中第一个像素的RGBA值。

var numPixels = imageData.width*imageData.height; 
for (var i = 0; i < numPixels; i++)
{
pixels[i*4] = 255; // Red 
pixels[i*4+1] = 0; // Green 
pixels[i*4+2] = 0; // Blue
pixels[i*4+3] = 255; // Alpha 
}; 

CanvasPixelArray 本身绝对不知道所访问的像素区域的尺寸。相反,返回的数组实际上只是一长串RGBA颜色值,它的长度等于所访问区域的像素个数乘以4(每个像素有4个颜色值)。例如,如果访问一个宽度和高度均为3个像素的像素栅格,那么CanvasPixelArray 的长度就是36(3×3×4),宽度和高度为200时,则长度为160 000 (200×200×4),以此类推。

CanvasPixelArray 中的像素排列顺序很简单:左上角像素位于数组开头(从位置0红色到位置3阿尔法值),而右下角像素位于数组末尾。这意味着,在所访问的区域中,每一行像素是从左到右访问的,直至到达行尾,然后再同样从左到右访问下一行(参见图2的栅格)。所以,如果CanvasPixelArray 只是一长串颜色值,而不知道像素区域的尺寸,那么应该如何从数组访问一个具体像素呢?在图2所示的例子中,应该如何访问(x,y)坐标位置为(2,2)的中心像素呢?通过查看图2,我们很容易发现它从数组索引16开始,但是如果没有这个图,我们应该如何确定呢?一些聪明的人已经帮我们计算出一个公式,我们可以用这个公式准确地计算出你需要从CanvasPixelArray中访问的像素,而且它非常简单:

var imageData = context.getImageData(0, 0, 3, 3); // 3x3 grid 
var width = imageData.width; 
var x = 2; 
var y = 2; 
var pixelRed = ((y-1)*(width*4))+((x-1)*4); 
var pixelGreen = pixelRed+1; 
var pixelBlue = pixelRed+2; 
var pixelAlpha = pixelRed+3; 

现在,我们最关注的地方是计算像素红色值索引位置的公式。我们拆解分析这个公式,以了解它的计算原理:

(y-1)

因为我们使用非0坐标值定义像素的(x,y)坐标位置,所以需要将坐标值减1。它的作用只是将画布所使用的坐标系统转换为数组所使用的从0开始的坐标系统。

(width*4)

这会得到图像中每一行的颜色值个数。通过将(y-1)的结果与这个数相乘,就能够得到所访问行的开头位置的数组索引值(y坐标位置)。在这个例子中,索引值是12,这对应图2第二行。

(x-1)*4

这里我们对y坐标位置重复相同的计算一一将它转换成从0开始的坐标系统。然后,将列(x位置)乘以4,得到所访问列的前一行颜色值个数。

将列索引值与行索引值相加,最终可以得到所访问像素的第一个颜色(红色)的索引值。在这个例子中,它应该是16(参见图3)。

图3
访问CanvasPixelArray中的像素

一旦得到红色像素的索引值,其他部分就很简单了。只需要给红色索引值分别加上1、2或3,就可以得到其他三种颜色——绿、蓝和阿尔法值。

下面来创建一个有趣的颜色拾取器。

var image = new Image();
image.src = "example.jpg"; 
$(image).load(function()
{
context.drawImage(image, 0, 0, 500, 333);
});

canvas.click(function(e) 
{
var canvasOffset = canvas.offset();
var canvasX = Math.floor(e.pageX-canvasOffset.left); 
var canvasY = Math.floor(e.pageY-canvasOffset.top); 
var imageData = context.getImageData(canvasX, canvasY, 1, 1); 
var pixel = imageData.data;
var pixelColor = "rgba("+pixel[0]+", "+pixel[1]+", "+pixel[2]+", "+pixel[3]+")"; 
$("body").css("backgroundColor", pixelColor);
}); 

我们要关注的是jQuery的click 方法,它是在指定元素上发生鼠标点击事件时调用的。在这里,元素就是画布。click方法中的回调函数会传递给你一个包含事件信息的参数,这里是e。这个参数包含了相对于整个浏览器窗口的鼠标点击位置的(x,y)坐标,它可用来处理画布上发生的点击事件。

通过使用jQuery的offset 方法,我们就能够得到画布与浏览器窗口顶部和左边的像素距离。然后,用鼠标点击位置的x坐标(pageX)减去画布的左侧偏移量,就可以得到点击位置在画布上的x坐标。如果对鼠标点击位置y坐标和顶部偏移量进行相同的计算,将得到鼠标点击位置相对于画布原点的(x,y)坐标值(参见图4)。

图4
找到鼠标点击位置在画布中的(x,y)坐标值

现在,我们得到了点击位置在画布中的(x,y)位置,下一步是查询该点的颜色值。为此,我们将canvasXcanvasY 传入getImageData方法。我们只需要一个像素的数据,这就是把getImageData调用的宽度和高度都设为1的原因,这样可以保持数据尽可能小。

一旦得到ImageData对象,就可以将它保存在一个变量中,然后访问data属性中的CanvasPixelArray。由于只得到一个像素的数据,所以检索颜色值就简单到只需访问CanvasPixelArray中的前4个索引。我们将修改整个网页的css背景,所以要用这些值创建一个表示CSS RGBA颜色值的字符串。

最后一步是将这个css颜色值传递给jQuery的css方法,它可以修改HTML body元素的background-color CSS属性。如果一切正常,这会把画布的背景颜色设置为你在图像中点击的那个像素的颜色,参见图5。

根据画布像素颜色修改背景颜色

安全问题

如果在自己的计算机上操作这些例子,而不是将它上传到Web服务器上,那么你可能不会看到任何结果或者会遇到一个安全错误。 这是因为,如果图像与控制画布的JavaScript不在同一个位置(域名)下,那么画布对于访问这个图像的像素级数据会有严格的限制。 这主要是为了防范跨站脚本攻击(XSS),试想一下如果一个不受你的网站控制的脚本能操控你网站用户的资源,这会导致多大的安全问题。 解决这个问题的最简单方法就是将这些例子上传到一个WEB服务器上(如Linux的Apache或Nginx),或者配置一个本地开发环境,如Mac的MAMP或Windows的XAMPP。 这样JavaScript和所访问的图像将位于相同的域名。